Mark Ku's Blog

Complete Guide to Debugging Lua in VSCode (with OpenResty Example)

Why learn Lua debugging?

Lua is widely used in embedded systems, games, and high-performance scenarios like Nginx/OpenResty. Lua is lightweight to write, but bug-hunting without good tools is painful. This article walks through setting up a modern Lua debugging environment in VSCode — set breakpoints, step through code, inspect variables in real time. Big efficiency boost!


Must-install VSCode extensions

  • EmmyLua: Lua intellisense and breakpoint debugging

Best practice: VSCode + Docker for Lua debugging (using OpenResty)

This project actually uses VSCode + Docker + EmmyLua for Lua breakpoint debugging. Here's the complete workflow and configuration:

1. Project structure and key files

  • docker-compose.yml: defines the openresty service, port mapping, volume mounts
  • conf/nginx.conf: Nginx and OpenResty configuration
  • lua/myapp.lua: main Lua business logic and debug entry point
  • lua/.vscode/launch.json: VSCode debug configuration

2. Docker port and volume setup

docker-compose.yml should include:

version: '3.8'
services:
  openresty:
    build: .
    container_name: openresty-dev
    ports:
      - "8080:80"      # HTTP
      - "8081:443"     # HTTPS
      - "9966:9966"    # EmmyLua Debugger (Lua debugging)
    volumes:
      - ./conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
      - ./lua:/usr/local/openresty/nginx/lua
      - ./logs:/usr/local/openresty/nginx/logs
      - ./db/GeoLite2-City.mmdb:/usr/local/openresty/nginx/lua/GeoLite2-City.mmdb:ro
    environment:
      TZ: Asia/Taipei
    restart: unless-stopped 
  • 8080:80: external HTTP service
  • 8081:443: external HTTPS service (if SSL is configured)
  • 9966:9966: Lua debugging (EmmyLua Debugger port)
  • TZ: Asia/Taipei: container timezone — useful for log alignment
  • restart: unless-stopped: auto-restart container unless manually stopped
  • Adjust other volume mounts to match your project paths

2.5 Sample Dockerfile

Below is the Dockerfile for OpenResty + Lua debugging used in this project — bundled with lua-resty-maxminddb, EmmyLua Debugger, GeoLite2-City database, etc.:

FROM openresty/openresty:alpine-fat

# Install required packages and tools
RUN apk add --no-cache \
    git \
    build-base \
    cmake \
    libmaxminddb-dev \
    perl \
    libmaxminddb \
    wget \
    tar \
    unzip \
    luarocks

# Install lua-resty-maxminddb
RUN luarocks install lua-resty-maxminddb

# Set timezone
ENV TZ=Asia/Taipei

# Create directory structure
RUN mkdir -p /usr/local/openresty/nginx/lua \
    && mkdir -p /usr/local/openresty/nginx/logs \
    && mkdir -p /usr/local/openresty/nginx/db

# Download latest MaxMind GeoLite2-City database
RUN wget -O /tmp/GeoLite2-City.tar.gz "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=<YOUR_LICENSE_KEY>&suffix=tar.gz" \
    && tar -xzf /tmp/GeoLite2-City.tar.gz -C /tmp \
    && find /tmp -name "GeoLite2-City.mmdb" -exec cp {} /usr/local/openresty/nginx/db/ \; \
    && rm -rf /tmp/GeoLite2-City.tar.gz /tmp/GeoLite2-City_*

# = Download and build emmy_core.so =
WORKDIR /tmp
RUN git clone https://github.com/EmmyLua/EmmyLuaDebugger.git \
    && cd EmmyLuaDebugger \
    && mkdir build && cd build \
    && cmake .. -DCMAKE_BUILD_TYPE=Release -DLUA_INCLUDE_DIR=/usr/local/openresty/luajit/include/luajit-2.1 \
    && make \
    && find . -name emmy_core.so -exec cp {} /usr/local/openresty/lualib/emmy_core.so \; \
    && cd / && rm -rf /tmp/EmmyLuaDebugger

# Set Lua module paths
ENV LUA_PATH="/usr/local/openresty/lualib/?.lua;;"
ENV LUA_CPATH="/usr/local/openresty/lualib/?.so;;"

# Default startup
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

Note:

  • For license_key=<YOUR_LICENSE_KEY>, register at MaxMind to get your dedicated key.
  • This key is personal — don't expose it publicly or commit to version control.
  • Without a valid key, the GeoLite2-City database can't be downloaded.

3. Enable debugging in your Lua code

At the top of lua/myapp.lua, add:

local dbg = require("emmy_core")
dbg.tcpListen("0.0.0.0", 9966)  -- have the in-container debugger listen on all interfaces
dbg.waitIDE()                   -- wait for the IDE to connect before continuing
dbg.breakHere()                 -- enter breakpoint

4. VSCode debug configuration

Add to lua/.vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "emmylua_new",
            "request": "attach",
            "name": "Attach by process id",
            "pid": 0,
            "processName": "",
            "captureLog": false,
            "host": "localhost",
            "port": 9966,
            "cwd": "${workspaceFolder}/lua",
            "ext": [".lua", "lua.txt", ".lua.bytes"]
        }
    ]
}

Set host to localhost since we've already mapped port 9966 from the container to the host.

5. Startup and debugging flow

  1. Restart the openresty container (docker-compose restart openresty).
  2. Start debugging in VSCode (F5), select the attach config from above.
  3. Trigger the corresponding HTTP request (e.g., http://localhost:8080/); execution will stop at the breakpoint.
  4. Happy step-debugging!

[Worked example] Debugging with myapp.lua

Using lua/myapp.lua as an example, here's how to set breakpoints and inspect variables:

local dbg = require("emmy_core")
dbg.tcpListen("0.0.0.0", 9966)
dbg.waitIDE()
dbg.breakHere()

local cjson = require 'cjson'
local geo = require 'resty.maxminddb'
geo.init("/usr/local/openresty/nginx/lua/GeoLite2-City.mmdb")

-- Suppose this function looks up the IP location
local function get_country(ip)
    local res, err = geo.lookup(ip)
    if not res then
        ngx.log(ngx.ERR, "Geo lookup error: ", err)
        return nil
    end
    return res
end

local ip = ngx.var.arg_ip or ngx.var.remote_addr
local country_info = get_country(ip)
ngx.say(cjson.encode(country_info))

Just insert the debug code at the top of myapp.lua, and you can step through every variable with VSCode breakpoints!


[Nginx config example] Routing requests to myapp.lua

Next, configure nginx.conf to route requests through OpenResty to myapp.lua:

location /lua {
    default_type 'text/plain';
    content_by_lua_file /usr/local/openresty/nginx/lua/myapp.lua;
}

With this config, when you visit http://localhost:8080/lua, Nginx routes the request to myapp.lua and returns the result.


When a client (browser, curl) requests http://localhost:8080/lua, Nginx hands the request to myapp.lua and returns the result. You can also adjust routing as needed.

Debug screenshot
Debug screenshot

GitHub sample project

For complete sample code, see: https://github.com/markku636/openresty-deubug


Common Lua debugging techniques

  1. Check the Error Log
    /usr/local/openresty/nginx/logs/error.log
    First place to look for issues.
  2. Enable Debug Log
    • OpenResty: set error_log ... debug; in nginx.conf
  3. Add log calls in Lua scripts
    ngx.log(ngx.ERR, "Debug info: ", cjson.encode(var))
  4. API testing
    Use Postman/curl to call APIs and inspect responses.
  5. VSCode debug attach failing?
    • Confirm dbg.tcpListen("0.0.0.0", 9966), not localhost.
    • Verify 9966:9966 is in docker-compose.yml.
    • VSCode's host should be set to localhost.
    • Check whether firewall or security software is blocking the port.
  6. Logs not visible?
    • Check /usr/local/openresty/nginx/logs/error.log.
    • Lua scripts can use ngx.log(ngx.ERR, "debug info") as an aid.

Remote debugging in containers (Docker)

MethodCommand / Description
Get inside container & tail logsdocker exec -it openresty-dev /bin/sh
tail -f /usr/local/openresty/nginx/logs/error.log
View container logs directlydocker logs -f openresty-dev
Mount local directorydocker run -v /your/local/logs:/usr/local/openresty/nginx/logs ... openresty
Capture trafficsudo tcpdump -i docker0 port 8080
VSCode RemoteEdit and debug files inside the container via VSCode

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11