Introduction

Welcome to the first lesson in our "Extending NGINX with Lua and OpenResty" course! In this lesson, we'll take our first steps in combining the power of NGINX with the flexibility of Lua scripting through OpenResty. By the end of this lesson, you'll understand how to embed Lua code directly in NGINX configuration files and use it to generate dynamic responses, control access, and create custom logging.

Why Lua in NGINX?

Lua is a lightweight, fast scripting language designed to be embedded into other programs. OpenResty is an NGINX-based distribution that embeds Lua into the NGINX request lifecycle (via the Lua module and a set of supporting libraries), so you can run Lua code inside NGINX worker processes while handling requests.

NGINX is excellent at serving static content and proxying requests, but its configuration language is primarily declarative—great for routing and policies, less ideal for custom logic. With OpenResty + Lua, we can add that missing “programmable layer” directly at the edge, without introducing a separate application server for many use cases.

This combination enables:

  • Dynamic content generation without external applications
  • Custom authentication and authorization logic
  • Request transformation and validation
  • Advanced logging and metrics collection
  • High performance, because Lua executes within the NGINX worker process
Understanding OpenResty Directives and Execution Phases

OpenResty provides several directives that let us inject Lua code at different stages of request processing. Think of NGINX request handling as a series of phases, and each directive gives us access to a specific phase—with different capabilities and restrictions depending on when it runs.

The three main directives we'll work with are:

  • access_by_lua_block: Runs during the access phase, before content generation; perfect for authentication and authorization
  • content_by_lua_block: Executes during the content phase to generate responses
  • log_by_lua_block: Executes during the log phase after the response is sent; ideal for logging and metrics

Execution order (high level):

  1. access_by_lua_block: Runs early, before content generation
  2. content_by_lua_block: Generates the response body
  3. log_by_lua_block: Runs after the response is sent

It’s important to remember that each phase has restrictions on which OpenResty API functions you can use. For example, output functions like ngx.say() only work in phases where you're building the response, and won’t work in the log phase since the response has already been sent.

Setting Up the Basic Structure

Let's start with the foundation of our NGINX configuration. Every OpenResty configuration begins with standard NGINX directives:

This sets up NGINX to listen on port 3000. Now we're ready to add our first Lua-powered location.

Your First Lua Block: Generating Dynamic Content

Let's create our first endpoint that uses Lua to generate a response. We'll use content_by_lua_block to output text:

Here's what happens:

  • default_type text/plain: Sets the response content type
  • content_by_lua_block: Opens a Lua code block
  • ngx.say(): Outputs text followed by a newline; we can pass multiple arguments and they'll be concatenated
  • ngx.now(): Returns the current timestamp in seconds (including fractional milliseconds)

When someone visits /hello, NGINX executes this Lua code and returns the generated content.

Accessing Request Information

OpenResty provides access to NGINX variables through ngx.var. This lets us inspect and use request details in our Lua code:

The ngx.var table gives us access to standard NGINX variables:

  • request_method: HTTP method (GET, POST, etc.)
  • uri: The request URI path
  • args: Query string parameters
  • remote_addr: Client's IP address

This is powerful because we can use these values in our Lua logic to make decisions or customize responses.

Controlling Access with Lua

Sometimes we need custom access control logic that goes beyond what standard NGINX directives offer. The access_by_lua_block directive runs before content generation and can deny access:

Let's break down this access control:

  • os.date("%H"): Gets the current hour (0-23) as a string
  • tonumber(): Converts the string to a number
  • ngx.exit(403): Terminates the request with a 403 Forbidden status

If the current hour is outside business hours (9 AM to 5 PM), the request is rejected before reaching the content phase. Otherwise, NGINX continues processing and returns the success message.

Custom Logging

The log_by_lua_block directive runs after the response has been sent to the client, making it perfect for logging and analytics:

This location demonstrates request timing:

  • content_by_lua_block: Generates the response first
  • log_by_lua_block: Executes after the response is sent
  • ngx.req.start_time(): Returns when the request started
  • ngx.log(ngx.INFO, ...): Writes an informational message to the NGINX error log

The difference between ngx.now() and ngx.req.start_time() gives us the total request duration. Note that we cannot use ngx.say() in the log phase since the response has already been sent.

Conclusion and Next Steps

We've covered the fundamentals of embedding Lua code in NGINX configuration through OpenResty. We learned how to generate dynamic content with content_by_lua_block, implement custom access control with access_by_lua_block, and add sophisticated logging with log_by_lua_block. These three directives form the foundation for building powerful, dynamic web services directly in NGINX.

Now it's time to put this knowledge into practice! Get ready to write your own Lua-powered NGINX configurations and see these concepts come to life.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal