luaguides

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:

DirectivePhaseCommon Use
init_by_lua_blockhttp initLoad shared data at startup
init_worker_by_lua_blockworker initSet up per-worker timers
rewrite_by_lua_blockrewriteRedirects, modify URI
access_by_lua_blockaccessAuthentication, rate limiting
content_by_lua_blockcontentGenerate the response
header_filter_by_lua_blockheader filterModify response headers
log_by_lua_blocklogCustom 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.