Getting Started with OpenResty
What Is OpenResty?
If you’ve used Nginx, you know it’s typically configured with static files and simple directives. OpenResty takes Nginx and bundles it with LuaJIT—a fast just-in-time compiler for Lua—plus a collection of useful Lua libraries. The result is a web platform that can execute Lua code during request processing, letting you build dynamic behavior without leaving Nginx.
Think of OpenResty as giving Nginx a scripting brain. Instead of just serving files, it can make routing decisions, talk to databases, aggregate responses from multiple backends, and implement access controls—all while keeping Nginx’s reputation for handling thousands of concurrent connections.
The current stable version is OpenResty 1.29.2.1, which includes Nginx 1.29.2 and supports LuaJIT 2.0 or 2.1. Note that standard Lua (the PUC-Rio interpreter) isn’t supported—OpenResty specifically requires LuaJIT.
Installing OpenResty
Getting OpenResty running depends on your operating system. Here’s how to set it up on common platforms.
Linux
Use the official pre-built packages. On Ubuntu or Debian:
# Add the OpenResty repository
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y "deb http://openresty.org/package/debian bookworm openresty"
sudo apt-get update
sudo apt-get install -y openresty
On CentOS or RHEL:
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
macOS
If you have Homebrew, installation is straightforward:
brew tap openresty/brew
brew install openresty
Windows
OpenResty doesn’t run natively on Windows. Use WSL (Windows Subsystem for Linux) or Docker. Install WSL, then follow the Linux instructions above inside your WSL distribution.
Docker
For quick experimentation, Docker works well:
docker pull openresty/openresty
docker run -d -p 8080:80 openresty/openresty
Verifying Your Installation
After installation, check that everything is working:
# Check Nginx version (should show OpenResty)
nginx -v
# Check LuaJIT version
luajit -v
You should see output indicating OpenResty and LuaJIT 2.0 or 2.1.
How Lua Integrates with Nginx
When a request arrives, Nginx passes it through several processing stages. At each stage, specific directives run in order. OpenResty adds *_by_lua_block directives that let Lua code execute during those phases.
The main directives you’ll encounter:
| Directive | Phase | Common Use |
|---|---|---|
init_by_lua_block | http init | Load shared data at startup |
init_worker_by_lua_block | worker init | Set up per-worker timers |
rewrite_by_lua_block | rewrite | Redirects, modify URI |
access_by_lua_block | access | Authentication, rate limiting |
content_by_lua_block | content | Generate the response |
header_filter_by_lua_block | header filter | Modify response headers |
log_by_lua_block | log | Custom logging |
Use *_by_lua_block for inline code and *_by_lua_file for external Lua files. The plain *_by_lua variants without _block or _file are deprecated—don’t use them.
Hot Reloading
During development, reload your configuration without restarting Nginx:
nginx -s reload
This sends Nginx a reload signal, and worker processes will pick up the new configuration gracefully.
The ngx API
When your Lua code runs inside OpenResty, it communicates with Nginx through the ngx table. This is your primary interface for reading request data, sending responses, and controlling the request lifecycle.
Sending Output
Two functions handle output:
-- ngx.say adds a newline after output (like print with \n)
ngx.say("Hello, ", "World") -- prints: Hello, World\n
-- ngx.print outputs without adding newline
ngx.print("No newline here")
Reading Request Data
Access Nginx variables through ngx.var:
-- Get client IP address
local client_ip = ngx.var.remote_addr
-- Get the request URI
local uri = ngx.var.uri
-- Get HTTP method (GET, POST, PUT, etc.)
local method = ngx.var.request_method
-- Get a specific request header
local user_agent = ngx.var.http_user_agent
For more complex request handling, use the ngx.req object:
-- Must read body before accessing it
ngx.req.read_body()
-- Get request body as string
local body = ngx.req.get_body_data()
-- Get all headers as a table
local headers = ngx.req.get_headers()
Handling Different Request Body Types
OpenResty is commonly used for API gateways, so you’ll receive various body formats. Here’s how to handle them:
ngx.req.read_body()
local headers = ngx.req.get_headers()
local content_type = headers["Content-Type"] or ""
local body = ngx.req.get_body_data()
if string.match(content_type, "application/json") then
-- Parse JSON request body
local cjson = require "cjson"
local decoded = cjson.decode(body)
ngx.say("Received JSON: ", decoded.name)
elseif string.match(content_type, "application/x-www-form-urlencoded") then
-- Parse form data
local args = ngx.req.get_post_args()
ngx.say("Form field: ", args.field_name)
else
-- Plain text or unknown
ngx.say("Body: ", body)
end
The cjson library is built into OpenResty and handles JSON encoding and decoding.
Controlling Response
Set status codes and redirect:
-- Set response status code
ngx.status = ngx.HTTP_OK
-- Redirect to a different URL (302 by default)
ngx.redirect("/new-location")
-- Or use ngx.exit to terminate the request
ngx.exit(ngx.HTTP_FORBIDDEN) -- Returns 403 Forbidden
Subrequests
OpenResty can make internal requests to other locations—this is powerful for aggregation:
-- Single subrequest
local res = ngx.location.capture("/api/users")
-- Check response status
if res.status == 200 then
ngx.say(res.body)
end
-- Multiple subrequests in parallel
local res1, res2, res3 = unpack(ngx.location.capture_multi({
{ "/api/users" },
{ "/api/posts" },
{ "/api/comments" }
}))
Logging
Write to Nginx’s error log:
ngx.log(ngx.INFO, "Request processing started")
ngx.log(ngx.ERR, "Failed to connect to database: ", err)
ngx.log(ngx.DEBUG, "Debug information here")
Log levels, from most to least severe: ngx.EMERG, ngx.ALERT, ngx.CRIT, ngx.ERR, ngx.WARN, ngx.INFO, ngx.DEBUG.
Your First OpenResty Application
Let’s build a simple HTTP server that returns “Hello, World!” with some dynamic information.
Create a file called nginx.conf:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
default_type text/html;
# This Lua code runs for every request to /
content_by_lua_block {
local method = ngx.var.request_method
local uri = ngx.var.uri
local ip = ngx.var.remote_addr
ngx.say("<html><body>")
ngx.say("<h1>Hello, World!</h1>")
ngx.say("<p>Method: ", method, "</p>")
ngx.say("<p>URI: ", uri, "</p>")
ngx.say("<p>Your IP: ", ip, "</p>")
ngx.say("</body></html>")
}
}
}
}
Start Nginx with this configuration:
nginx -p $(pwd)/ -c nginx.conf
Test it with curl:
curl http://localhost:8080/
You should see HTML output showing the request method, URI, and your client IP. This demonstrates the core pattern: Nginx handles the networking, Lua handles the dynamic content.
Working with External Libraries
OpenResty includes many lua-resty-* libraries for connecting to external services. These live in the resty namespace.
Here’s how to connect to Redis using resty.redis:
local redis = require "resty.redis"
-- Create a new Redis connection
local red = redis:new()
-- Set a timeout (in milliseconds)
red:set_timeout(1000)
-- Connect to Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect: ", err)
return ngx.exit(500)
end
-- Make a request
local value, err = red:get("my_key")
if err then
ngx.log(ngx.ERR, "failed to get key: ", err)
return ngx.exit(500)
end
-- Use the value
ngx.say("Redis returned: ", value)
-- Return connection to pool for reuse
red:set_keepalive(0, 100)
The connection pooling is important—creating a new TCP connection for every request is slow. The set_keepalive call returns the connection to a pool so the next request can reuse it.
Other useful resty modules include resty.mysql for MySQL, resty.http for HTTP clients, resty.dns for DNS queries, and resty.websocket for WebSocket connections.
Installing Additional Libraries
Use OpenResty’s package manager opm to install additional lua-resty libraries:
opm get openresty/lua-resty-limit-traffic
You can also use luarocks:
luarocks install lua-resty-http
HTTPS and SSL/TLS
For production deployments, you’ll need to configure SSL. Here’s a basic HTTPS setup:
http {
server {
listen 443 ssl;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# Recommended SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
content_by_lua_block {
ngx.say("Secure connection established")
}
}
}
}
For local development, generate a self-signed certificate:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Then update your config to use cert.pem and key.pem.
Practical Examples
Blocking IPs in the Access Phase
Implement IP-based access control using the access phase:
server {
listen 8080;
location / {
access_by_lua_block {
-- List of blocked IP addresses
local blocked = {
["192.168.1.100"] = true,
["10.0.0.50"] = true
}
local client_ip = ngx.var.remote_addr
if blocked[client_ip] then
ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
-- Your normal location handler
content_by_lua_block {
ngx.say("Welcome!")
}
}
}
The access_by_lua_block runs before the content phase, so blocked IPs never reach your application code.
Simple In-Memory Cache
Use shared memory to cache responses:
http {
-- Declare a shared memory zone (10 megabytes)
lua_shared_dict my_cache 10m;
server {
listen 8080;
location /api/data {
content_by_lua_block {
local cache = ngx.shared.my_cache
local key = ngx.var.arg_key or "default"
-- Check cache first
local cached = cache:get(key)
if cached then
ngx.say("cached: ", cached)
return
end
-- Simulate fetching fresh data
local fresh_data = os.date("%H:%M:%S")
-- Store in cache with 60 second TTL
cache:set(key, fresh_data, 60)
ngx.say("fresh: ", fresh_data)
}
}
}
}
Shared memory is shared across all worker processes, but each worker has its own Lua VM. Data written from one worker is visible to others because Nginx manages the shared memory directly.
Dynamic Routing
Route requests to different backends based on the URI:
http {
upstream backend_v1 {
server 127.0.0.1:8001;
}
upstream backend_v2 {
server 127.0.0.1:8002;
}
server {
listen 8080;
location / {
content_by_lua_block {
local path = ngx.var.uri
-- Route based on API version in path
if string.match(path, "^/api/v1/") then
ngx.var.backend = "backend_v1"
elseif string.match(path, "^/api/v2/") then
ngx.var.backend = "backend_v2"
else
ngx.exit(ngx.HTTP_NOT_FOUND)
end
-- Internal redirect to proxy location
ngx.exec("@proxy")
}
}
location @proxy {
proxy_pass http://$backend;
}
}
}
This pattern lets you implement sophisticated routing logic in Lua before passing requests to upstream servers.
JSON API Response
Here’s a complete example of building a JSON API response:
content_by_lua_block {
local cjson = require "cjson"
-- Read request body if present
ngx.req.read_body()
local args = ngx.req.get_get_args()
local headers = ngx.req.get_headers()
-- Build response table
local response = {
status = "ok",
data = {
message = "Hello from OpenResty",
timestamp = os.time(),
method = ngx.var.request_method,
uri = ngx.var.uri,
client_ip = ngx.var.remote_addr
}
}
-- Set JSON content type
ngx.header["Content-Type"] = "application/json"
-- Encode and send response
ngx.say(cjson.encode(response))
}
Common Pitfalls
Several issues trip up newcomers to OpenResty:
Cosockets aren’t available everywhere. Cosockets (the network socket API used by resty libraries) work in most phases but not in set_by_lua_block or timer callbacks. If you try to connect to Redis in the wrong phase, you’ll get an error about cosockets not being available.
Lua variables and Nginx variables are different. Setting a Lua variable doesn’t create an Nginx variable:
-- WRONG: This doesn't do what you expect
set_by_lua_block $foo {
local x = "bar"
-- x is a Lua variable, not accessible as $foo
}
-- CORRECT: Return the value
set_by_lua_block $foo {
return "bar" -- The return value sets $foo
}
Request body must be explicitly read. Unlike some frameworks, OpenResty doesn’t automatically read the request body:
-- REQUIRED before accessing body data
ngx.req.read_body()
local body = ngx.req.get_body_data()
Avoid blocking operations. Never use os.execute, io.popen, or standard Lua file I/O in request handlers—they block the entire worker process. Use cosockets or ngx.location.capture instead.
Each worker is isolated. Every Nginx worker runs its own Lua VM. Module-level variables persist across requests within one worker but aren’t shared between workers. Use ngx.shared.DICT for cross-worker data sharing.
Summary
OpenResty extends Nginx with Lua scripting capabilities, letting you build dynamic request handling directly inside Nginx. The key concepts to remember:
- Nginx phases determine when your Lua code runs—use the right directive for your use case
- The ngx API is your interface to request data, responses, and Nginx internals
- resty libraries connect to external services like Redis, MySQL, and HTTP backends
- Shared memory caches data across all workers
- Subrequests aggregate multiple backend calls into one response
- cjson handles JSON encoding and decoding for API work
- Use SSL/TLS for production deployments
This tutorial covered the fundamentals. The next in this series explores advanced topics like rate limiting, complex routing patterns, and optimizing OpenResty for production workloads.