Test nginx Configuration Directives
- Introduction
- Setup
- Get
nginx
running - Test an HTTP request
- Create a
Makefile
- Configure the domain name
- Test an HTTP redirect
- Test reverse proxy a subdomain request to a Unix socket
- Configure
nginx
- Add subdomain to SSL/TLS certs
- Add subdomain as a host
- Add a test socket server
- Write the test
- Conclusion
Introduction
nginx
configuration can contain any number of important directives (redirects
and rewrites, for example) that need to be verified for correctness. We
can write tests for directives and run them against a test server to
ensure they are correct.
To do this, we’ll use…
- MoonScript and (by extension) Lua programming languages
nginx
we’ll get from OpenResty, a web platform created by Chinese developer, Yichun Zhang- the Busted testing framework
- the Lua package manager, LuaRocks
- a fantastic little library,
luajit-curl
, from Japanese developer SENA Networks, Inc - another great library, written by volunteers, LuaSocket
- our favorite container manager, Docker Engine
Setup
Since we require LuaRocks, we’ll use a Buildpack tag, which comes with it already installed.
$ docker pull openresty/openresty:bookworm-buildpack
Start a server on localhost
:
$ docker run --rm -it -p 80:80 openresty/openresty:bookworm-buildpack
We can visit localhost
in our browser and we should see
the OpenResty splash page.

Get nginx
running
First, let’s prepare the directory layout.
$ mkdir -p logs/ conf/conf.d/ html/
Next, we copy over the
default nginx
config file.
$ docker run --rm -it -w /opt -v $PWD:/opt openresty/openresty:bookworm-buildpack \
cp /etc/nginx/conf.d/default.conf /opt/conf.d/
Then, we update the root directive in default.conf
:
conf/conf.d/default.conf
location / {
- root /usr/local/openresty/nginx/html;
+ root /var/www;
index index.html index.htm;
Now, let’s add an index file.
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>
Last, we 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, this should output our index file.
$ 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>
Test an HTTP request
If we want to write a test for that request, we need some packages
from LuaRocks. Let’s add a Dockerfile
to build an image
with those packages installed.
Add a Dockerfile
FROM openresty/openresty:bookworm-buildpack
WORKDIR /opt/app
RUN luarocks install moonscript
RUN luarocks install busted
RUN luarocks install luajit-curl
RUN luarocks install luasocket
Now let’s build our image:
$ docker build -t test-nginx .
Write the test
Let’s first make a new directory where our tests will live.
$ mkdir spec
Our test makes a cURL request against our test server:
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!"
Run the test suite
Start the test server. We’re going to use text-nginx
,
the image we just built.
$ 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)
Start the test run:
$ docker exec -t $ct busted
●
1 success / 0 failures / 0 errors / 0 pending : 0.008246 seconds
Stop the test server.
$ docker exec $ct openresty -s stop
Create a Makefile
We now have a number of long docker
commands, let’s
create a Makefile
to make running them easier.
Makefile
image = test-nginx
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 \
$(image)); \
docker exec -t $$ct busted; \
docker exec $$ct openresty -s stop
Now we can run the test suite with the command
make test
.
Configure the domain name
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.
Ensure the test container is offline
We need to create a network that has no external access.
$ docker network create --internal no-internet
Now we need to update our Makefile
to add the test
container to our internal-only network:
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 \
$(image)); \
And now let’s add a test in spec/nginx_spec.moon
to make
sure our test environment is offline:
describe "test environment", ->
it "can't connect to the internet", ->
assert.has_error (-> req "http://example.org"),
"Couldn't resolve host name"
Let’s run our tests:
$ make test
●●
2 successes / 0 failures / 0 errors / 0 pending : 0.020207 seconds
Replace
localhost
with a custom domain
To use a custom domain name instead of localhost
, we
will need to use the --add-host
option for the
docker run
command. Again, we edit
Makefile
:
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=127.0.0.1 \
$(image)); \
Let’s update our test to use the custom domain name:
spec/nginx_spec.moon
-describe "http://localhost", ->
+describe "http://domain.abc", ->
it "sends /index.html", ->
- request = req "http://localhost"
+ request = req "http://domain.abc"
assert.same request\statusCode!, 200
Verify our tests still pass.
$ make test
●●
2 successes / 0 failures / 0 errors / 0 pending : 0.0224 seconds
Test an HTTP redirect
We want our server to redirect all http
requests to
https
.
Write the test
Let’s practice a bit of test-driven development and write our test first.
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/"
We should now have one failing test.
$ 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
Configure nginx
We’re going to add the redirect directives, as well as a server name for our domain and the directives for the SSL certificates we will generate.
+server {
+ listen 80;
+ return 301 https://$host$request_uri;
+}
server {
- listen 80;
+ listen 443 ssl;
+ server_name domain.abc;
+ ssl_certificate /etc/ssl/certs/domain.abc.pem;
+ ssl_certificate_key /etc/ssl/private/domain.abc.pem;
location / {
root /var/www;
index index.html index.htm;
}
Generate self-signed SSL/TLS certs for testing
Add a command to our Dockerfile
to generate self-signed
certificates:
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"
Rebuild the image:
$ make image-rm image-build
We need to update our previous test to use HTTPS instead of HTTP.
spec/nginx_spec.moon
-describe "http://domain.abc", ->
+describe "https://domain.abc", ->
it "sends /index.html", ->
- request = req "http://domain.abc"
+ request = req "https://domain.abc"
Run tests:
$ make test
●●●
3 successes / 0 failures / 0 errors / 0 pending : 0.017065 seconds
Test reverse proxy a subdomain request to a Unix socket
Let’s say we have a running service that connects to a Unix socket.
We want to proxy the requests through nginx
so that our
service can respond to https
requests but can leave
handling SSL/TLS to nginx
.
Configure nginx
We’ll add another server block to
conf/conf.d/default.conf
for our subdomain,
git.domain.abc
, with the proxy directives:
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 SSL/TLS certs
Next, we need to add our subdomain to the generated SSL certs in the
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"
+ -addext "subjectAltName=DNS:domain.abc,DNS:git.domain.abc"
Add subdomain as a host
Let’s assign the loopback address to a variable and then add our
subdomain as a host in our Makefile
:
+loopback = 127.0.0.1
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=127.0.0.1 \
+ --add-host=domain.abc=$(loopback) \
+ --add-host=git.domain.abc=$(loopback) \
$(image)); \
Add a test socket server
We need to start up a mock socket server for our test to ensure our request is being proxied correctly. This is why we needed the LuaSocket library.
Copied and modified from here, this should suit our purposes:
spec/unixstreamsrvr.moon
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
Write the test
And now we can add our test:
spec/nginx_spec.moon
describe "https://git.domain.abc", ->
it "reverse-proxy's a subdomain request to a 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}" -- wait for server to start
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"
Because we modified the Dockerfile
, we need to rebuild
our image:
$ make image-rm image-build
And if all went well, our test should pass.
$ make test
●●●●
4 successes / 0 failures / 0 errors / 0 pending : 0.131619 seconds
Conclusion
These are just a few examples of how to test nginx
directives. Using these tools, we can verify that changes to our server
configuration are working the way we intended.