miti.sh/posts/2025-06-22-test-nginx-conf-directives.md
Catalin Constantin Mititiuc 766c9bad62 Use '_' instead of '.' for numbering cosmo escapes
because it gets syntax highlighted weird because '.' is not valid for
like a variable name so the syntax highlighter is splitting the escape
phrase at the '.' which means that phrase won't match when unescaped so
it fails to get unescaped

for example, this would fail:

```
 $$ct
```
2025-06-25 21:10:30 -07:00

8.6 KiB

$index

Introduction

We'll need nginx and luarocks. Buildpack has luarocks installed.

docker pull openresty/openresty:bookworm-buildpack

$ docker run --rm -it -p 80:80 openresty/openresty:bookworm-buildpack

Visit localhost in browser. Should see OpenResty splash page.

OpenResty default nginx index page

https://openresty.org/en/getting-started.html#prepare-directory-layout

$ mkdir -p logs/ conf/conf.d/ html/

https://github.com/openresty/docker-openresty?tab=readme-ov-file#nginx-config-files

$ docker run --rm -it -w /opt -v $PWD:/opt openresty/openresty:bookworm-buildpack
cp /etc/nginx/conf.d/default.conf /opt/conf.d/

edit default.conf change root /usr/local/openresty/nginx/html; to:

root /var/www;

html/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title></title>
  </head>
  <body>
    hello world!
  </body>
</html>

Start nginx:

$ docker run --rm -it -p 80:80 \
-v $PWD/conf/conf.d:/etc/nginx/conf.d -v $PWD/html:/var/www \
openresty/openresty:bookworm-buildpack

Then, in another console:

$ curl -v localhost
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.88.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: openresty/1.27.1.2
< ...
< 
<!DOCTYPE html>
<html lang="en">
  ...
  <body>
    hello world!
  </body>
</html>

If we wanted to write a test for that, we need some packages from luarocks.

Dockerfile

FROM openresty/openresty:bookworm-buildpack

WORKDIR /opt/app

# needed for testing
RUN luarocks install busted
RUN luarocks install luajit-curl
RUN luarocks install luasocket # needed for testing nginx reverse proxy
$ docker build -t test-nginx .
$ mkdir spec

spec/nginx_spec.moon

http = require "luajit-curl-helper.http"

req = (url) ->
  request = http.init url
  st = request\perform!
  error request\lastError! if not st
  request

describe "http://localhost", ->
  it "sends /index.html", ->
    request = req "http://localhost"
    assert.same request\statusCode!, 200
    assert.same request\statusMessage!, "OK"
    assert.same request\body!\match("<body>%s+(.-)%s+</body>"), "hello world!"

Start the test server:

$ ct=$(docker run --rm -d \
-v $PWD/conf/conf.d:/etc/nginx/conf.d \
-v $PWD/html:/var/www \
-v $PWD:/opt/app \
test-nginx)

Run the tests.

$ docker exec -t $ct busted
1 success / 0 failures / 0 errors / 0 pending : 0.008246 seconds

Stop the test server.

$ docker exec -t $ct openresty -s stop

Edit hosts

Instead of localhost we'd like to use an actual domain name. We can do this with the --add-host option. But before we do that, we want to make sure our container does not have access to the internet, otherwise we might unintentionally get a response from a domain's server on the internet rather than from our test server.

$ docker network create --internal no-internet

Now we can start the test server with our host:

$ ct=$(docker run --rm -d \
-v $PWD/conf/conf.d:/etc/nginx/conf.d \
-v $PWD/html:/var/www \
-v $PWD:/opt/app \
--network no-internet \
--add-host=domain.abc=127.0.0.1 \
test-nginx)

Update our test

request = req "http://localhost"

to

request = req "http://domain.abc"

Run the tests.

$ docker exec -t $ct busted
1 success / 0 failures / 0 errors / 0 pending : 0.008246 seconds

Stop the test server.

$ docker exec -t $ct openresty -s stop

Add a test to make sure the test server is offline

describe "test environment", ->
  it "can't connect to the internet", ->
    assert.has_error (-> req "http://example.org"),
      "Couldn't resolve host name"

Create a Makefile

Let's create a Makefile to make running all these commands easier.

Makefile

image = test-nginx
loopback = 127.0.0.1

image-build:
	docker build -t $(image) .

image-rm:
	docker image rm $(image)

test:
	@ct=$(shell docker run --rm -d \
	   -v $(PWD)/conf/conf.d:/etc/nginx/conf.d \
	   -v $(PWD)/html:/var/www \
	   -v $(PWD):/opt/app \
	   --network no-internet \
	   --add-host=domain.abc=$(loopback) \
	   $(image)); \
	docker exec -t $$ct busted; \
	docker exec $$ct openresty -s stop

Now we can run tests by running make test.

$ make test
●●
2 successes / 0 failures / 0 errors / 0 pending : 0.008812 seconds

SSL

We want our server to redirect all http requests to https.

Our test:

describe "http://domain.abc", ->
  it "redirects to https", ->
    request = req "http://domain.abc"
    assert.same request\statusCode!, 301
    assert.same request\statusMessage!, "Moved Permanently"
    assert.same request\header!.Location, "https://domain.abc/"
$ make test
●●◼
2 successes / 1 failure / 0 errors / 0 pending : 0.010449 seconds

Failure → .../luajit/lib/luarocks/rocks-5.1/busted/2.2.0-1/bin/busted @ 3
http://domain.abc redirects to https
spec/nginx_spec.moon:24: Expected objects to be the same.
Passed in:
(number) 301
Expected:
(number) 200

Make self-signed certs in Dockerfile:

RUN openssl req -x509 -newkey rsa:4096 -nodes \
      -keyout /etc/ssl/private/domain.abc.pem \
      -out /etc/ssl/certs/domain.abc.pem \
      -sha256 -days 365 -subj '/CN=domain.abc' \
      -addext "subjectAltName=DNS:domain.abc"

Edit default.conf:

server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  domain.abc;
    ssl_certificate /etc/ssl/certs/domain.abc.pem;
    ssl_certificate_key /etc/ssl/private/domain.abc.pem;

Rebuild the image:

$ make image-rm
$ make image-build

Run tests:

$ make test
●◼●
2 successes / 1 failure / 0 errors / 0 pending : 0.009618 seconds

Failure → .../luajit/lib/luarocks/rocks-5.1/busted/2.2.0-1/bin/busted @ 3
http://domain.abc sends /index.html
spec/nginx_spec.moon:17: Expected objects to be the same.
Passed in:
(number) 200
Expected:
(number) 301

Fix test:

describe "https://domain.abc", ->
  it "sends /index.html", ->
    request = req "https://domain.abc"

Run tests:

$ make test
●●●
3 successes / 0 failures / 0 errors / 0 pending : 0.017065 seconds

Reverse proxy a subdomain to a Gitea unix socket

Add to default.conf:

server {
    listen 443 ssl;
    server_name git.domain.abc;

    location / {
        client_max_body_size 1024M;
        proxy_pass http://unix:/run/gitea/gitea.socket;
        proxy_set_header Connection $http_connection;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Add subdomain to certs in Dockerfile:

      -addext "subjectAltName=DNS:domain.abc,DNS:git.domain.abc"

Add a test socket server:

spec/unixstreamsrvr.moon

-- modified from
-- https://github.com/lunarmodules/luasocket/blob/4844a48fbf76b0400fd7b7e4d15d244484019df1/test/unixstreamsrvr.lua
socket = require "socket"
socket.unix = require "socket.unix"
u = assert socket.unix.stream!
assert u\bind "/run/gitea/gitea.socket"
assert u\listen!
assert u\settimeout 1
c = assert u\accept!

while true
  m = assert c\receive!
  break if m == ""
  print m

Add a spec:

describe "https://git.domain.abc", ->
  it "reverse-proxy's request to a gitea unix socket", ->
    socket = fname: "unixstreamsrvr.moon", dir: "/run/gitea", owner: "nobody"
    basepath = debug.getinfo(1).short_src\match"^(.*)/[^/]*$" or "."
    seconds = 0.1

    os.execute "install -o #{socket.owner} -d #{socket.dir}"
    cmd = "su -s /bin/bash -c 'moon %s' %s"
    server = io.popen cmd\format "#{basepath}/#{socket.fname}", socket.owner
    os.execute "sleep #{seconds}"
    f = io.popen "find #{socket.dir} -type s -ls", "r"
    result = with f\read "*a"
      f\close!
    assert.truthy result\match "nobody%s+nogroup.+#{socket.dir}/gitea.socket"

    req "https://git.domain.abc"

    reqheader = with server\read "*a"
      server\close!

    assert.truthy reqheader\match "Host: git.domain.abc"

Edit Makefile:

--add-host=git.domain.abc=$(loopback) \

Rebuild image:

$ make image-rm image-build

Run tests:

$ make test
●●●●
4 successes / 0 failures / 0 errors / 0 pending : 0.131619 seconds

Conclusion