DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

Supercharge Nginx with Lua Scripts: A Hands-On Tutorial with Examples

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a first of its kind tool for helping you automatically index API endpoints across all your repositories. LiveAPI helps you discover, understand and use APIs in large tech infrastructures with ease.

Nginx is a powerhouse for serving web content, but did you know you can extend its functionality with Lua scripts? The OpenResty bundle, which embeds Lua into Nginx via the ngx_lua module, lets you write custom logic to handle requests, manipulate responses, and more. This tutorial dives into practical examples to show you how to use Lua scripts in Nginx, complete with working code and clear explanations.

If you're new to this, don’t worry—I’ll keep it straightforward and easy to understand. By the end, you’ll know how to set up Lua with Nginx, write scripts, and apply them to real-world scenarios. Let’s get started.

Why Lua with Nginx? Understanding the Power Combo

Lua is a lightweight, embeddable scripting language, and when paired with Nginx through OpenResty, it unlocks dynamic request processing without sacrificing performance. Unlike traditional CGI or FastCGI, Lua runs directly in Nginx’s event loop, keeping things fast.

Here’s what you can do:

  • Rewrite URLs dynamically based on logic.
  • Authenticate requests using external APIs or databases.
  • Modify headers or content on the fly.
  • Rate-limit or cache responses with custom rules.
Feature Benefit Example Use Case
Dynamic Rewriting Alter URLs based on conditions Redirect users by geo-location
Content Modification Inject or transform response data Add custom analytics scripts
Access Control Validate requests API key verification

To follow along, you’ll need OpenResty installed. It’s a superset of Nginx with the Lua module baked in. Check the OpenResty installation guide for setup steps.

Setting Up OpenResty for Lua Scripting

First, let’s get your environment ready. Install OpenResty (available for Linux, macOS, and Windows via WSL). On Ubuntu, it’s as simple as:

sudo apt-get update
sudo apt-get install -y openresty
Enter fullscreen mode Exit fullscreen mode

Verify it’s running:

openresty -v
# Output: openresty/1.x.x
Enter fullscreen mode Exit fullscreen mode

Next, configure Nginx to recognize Lua scripts. OpenResty uses the ngx_lua module, which provides directives like lua_code_cache and content_by_lua_block. Here’s a minimal Nginx config to test:

# /usr/local/openresty/nginx/conf/nginx.conf
http {
    server {
        listen 80;
        server_name localhost;

        location /hello {
            content_by_lua_block {
                ngx.say("Hello, Lua!")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Restart OpenResty:

sudo openresty -s reload
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost/hello in your browser. You should see Hello, Lua!. This confirms Lua is working.

Your First Lua Script: Dynamic Response

Let’s write a simple Lua script to return a dynamic response based on query parameters. This example grabs a name parameter from the URL and responds with a greeting.

# /usr/local/openresty/nginx/conf/nginx.conf
http {
    server {
        listen 80;
        server_name localhost;

        location /greet {
            content_by_lua_block {
                local name = ngx.var.arg_name or "Guest"
                ngx.say("Hello, " .. name .. "!")
                -- Output: Hello, [name]! (e.g., Hello, Alice! if ?name=Alice)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Reload OpenResty and visit http://localhost/greet?name=Alice. You’ll see Hello, Alice!. Without a name parameter, it defaults to Hello, Guest!.

Key points:

  • ngx.var.arg_name accesses query parameters.
  • ngx.say sends output to the client.
  • The .. operator concatenates strings in Lua.

Rewriting URLs with Lua Logic

URL rewriting is a common task. Let’s create a script that redirects users based on their User-Agent. If the request comes from a mobile device, we’ll redirect to a mobile site.

http {
    server {
        listen 80;
        server_name localhost;

        location / {
            rewrite_by_lua_block {
                local ua = ngx.var.http_user_agent or ""
                if string.match(ua, "Mobile") then
                    ngx.redirect("/mobile", 302)
                end
                -- No output; redirects if mobile, else continues
            }
        }

        location /mobile {
            content_by_lua_block {
                ngx.say("Welcome to the mobile site!")
                -- Output: Welcome to the mobile site!
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Test it by curling with a mobile User-Agent:

curl -A "Mozilla/5.0 (iPhone)" http://localhost/
# Output: Welcome to the mobile site!
Enter fullscreen mode Exit fullscreen mode

Key points:

  • rewrite_by_lua_block runs during the rewrite phase.
  • ngx.redirect(url, status) sends a redirect response.
  • Use string.match for pattern matching in Lua.

Authenticating Requests with Lua

Let’s build an API key authentication system. This script checks for a valid X-API-Key header before allowing access.

http {
    server {
        listen 80;
        server_name localhost;

        location /api {
            access_by_lua_block {
                local key = ngx.var.http_x_api_key
                if key ~= "secret123" then
                    ngx.status = 403
                    ngx.say("Invalid API key")
                    ngx.exit(403)
                    -- Output: Invalid API key (if key is wrong)
                end
            }
            content_by_lua_block {
                ngx.say("Access granted!")
                -- Output: Access granted! (if key is correct)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Test with curl:

curl -H "X-API-Key: secret123" http://localhost/api
# Output: Access granted!

curl -H "X-API-Key: wrong" http://localhost/api
# Output: Invalid API key
Enter fullscreen mode Exit fullscreen mode

Key points:

  • access_by_lua_block runs during the access phase.
  • ngx.exit(status) stops processing after setting a response.
  • The ~= operator checks for inequality in Lua.

For production, you’d likely query a database or external service for key validation. The lua-resty-http library can help with HTTP requests.

Modifying Response Content Dynamically

Let’s inject content into responses. This example adds a custom footer to HTML responses.

http {
    server {
        listen 80;
        server_name localhost;

        location /page {
            content_by_lua_block {
                local html = [[
                    <html>
                        <body>
                            <h1>Welcome</h1>
                        </body>
                    </html>
                ]]
                ngx.say(html)
                -- Output: HTML content (before footer)
            }
            body_filter_by_lua_block {
                local data = ngx.arg[1] or ""
                local footer = "<footer>Powered by Lua</footer></html>"
                if string.match(data, "</body>") then
                    data = string.gsub(data, "</body>", footer)
                end
                ngx.arg[1] = data
                -- Output: HTML with footer added
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost/page. You’ll see the HTML with Powered by Lua in a footer.

Key points:

  • body_filter_by_lua_block modifies response bodies.
  • ngx.arg[1] holds the response chunk; modify it to change output.
  • string.gsub replaces substrings in Lua.

Rate Limiting with Lua and Shared Memory

Rate limiting is crucial for APIs. Let’s implement a simple rate limiter using Lua’s shared memory dictionary.

http {
    lua_shared_dict rate_limit 10m; # Define shared memory
    server {
        listen 80;
        server_name localhost;

        location /limit {
            access_by_lua_block {
                local dict = ngx.shared.rate_limit
                local key = ngx.var.remote_addr
                local limit = 5 -- Requests per minute
                local window = 60 -- Seconds

                local count = dict:get(key) or 0
                if count >= limit then
                    ngx.status = 429
                    ngx.say("Rate limit exceeded")
                    ngx.exit(429)
                    -- Output: Rate limit exceeded (if limit hit)
                end

                dict:incr(key, 1, 0, window)
                -- No output; allows request if under limit
            }
            content_by_lua_block {
                ngx.say("Request allowed")
                -- Output: Request allowed
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Test by sending multiple requests:

for i in {1..6}; do curl http://localhost/limit; done
# Output: "Request allowed" 5 times, then "Rate limit exceeded"
Enter fullscreen mode Exit fullscreen mode

Key points:

  • lua_shared_dict creates a shared memory zone.
  • dict:incr(key, value, init, expiry) increments a counter with expiration.
  • ngx.var.remote_addr gets the client’s IP.

Debugging and Best Practices for Lua Scripts

Debugging Lua in Nginx can be tricky since errors go to Nginx’s error log. Enable debug logging in your config:

error_log logs/error.log debug;
Enter fullscreen mode Exit fullscreen mode

Best practices:

  • Cache Lua code: Keep lua_code_cache on (default) for performance.
  • Handle errors: Use pcall in Lua to catch runtime errors.
  • Test locally: Use a staging environment before deploying.
  • Keep scripts small: Large scripts can block Nginx’s event loop.

Here’s an example with error handling:

http {
    server {
        listen 80;
        server_name localhost;

        location /safe {
            content_by_lua_block {
                local ok, err = pcall(function()
                    local result = some_function() -- Assume this might fail
                    ngx.say(result)
                end)
                if not ok then
                    ngx.status = 500
                    ngx.say("Error: " .. err)
                    -- Output: Error: [error message]
                end
                -- Output: [result] (if no error)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Check /usr/local/openresty/nginx/logs/error.log for issues.

Where to Go Next with Lua and Nginx

You’ve now seen how to handle dynamic responses, rewrites, authentication, content modification, and rate limiting with Lua in Nginx. To level up, try these:

  • Explore libraries: Use lua-resty-redis for Redis integration or ` lua-resty-mysql for MongoDB querying.
  • Scale with OpenResty: Combine Lua with OpenResty’s built-in libraries for caching or proxying.
  • Contribute: Check out OpenResty’s GitHub for community scripts and modules.

Experiment with these examples in a local setup. Lua’s simplicity and Nginx’s speed make them a killer combo for custom web logic. Start small, test thoroughly, and you’ll be scripting like a pro in no time.

Top comments (0)