Migrating Nginx to a Docker Container

This is a write-up of how I migrated my Nginx web server from running as a standard service to running inside a Docker container. We will also see how to customize logging and network options, including configuring docker for IPv6

Preparing nginx.conf

For me, I find it easier having a single nginx.conf file, appending the single flat file with vhosts generated from a template. For example, for additional vhosts, you could use this template

The main things you'll need to comment out any access_log or error_log entries, created on a per-vhost basis. For now, I'm just using Dockers built in syslog functionality.

If you desire to use the /var/log/nginx files, that should be possible, using a volume mount.

Creating an additional docker bridge

docker network create 
  --driver bridge \
  --subnet 10.128.64.0/24 \
  --opt com.docker.network.bridge.name=docker1  \
  --opt com.docker.network.bridge.enable_icc=false \
  --opt "com.docker.network.bridge.host_binding_ipv4=$floatingip" \
docker1

Let's break down the options.

  • network.bridge.name is the device name of the bridge created on the host.
    If you don't manually set it, you'll get a name like "br-78c40ed9122e"
  • enable_icc refers to "Inter-container Communications'.
    If you intend on using nginx as a reverse proxy, you'd want that to be set as true.
  • com.docker.network.bridge.host_binding_ipv4 address for published ports
    e.g. -p 80:80 by default redirects port 80 along any interface, in this case we pick a particular IP address. Omit this if unneeded for you.

Stop and disable the host/nginx service

Currently port 80 and 443 are occupied by the nginx service running on the host, which will result in the container failing to start. Therefore, we need to stop the nginx service

sudo systemctl stop nginx
sudo systemctl disable nginx

Creating the docker container

docker create \
    --network docker1 \
    --hostname nginx_prod \
    --ip 10.128.64.128 \
    --name nginx-production \
    --volume /var/www/html:/var/www/html:ro \
    --volume /etc/ssl:/etc/ssl:ro \
    --restart=on-failure \
    -p 80:80 -p 443:443 \
nginx:latest

Copying over the nginx.conf file

 sudo docker cp /etc/nginx/nginx.conf nginx-production:/etc/nginx
 sudo docker restart nginx-production

Checking the container is operational

The simple way is to use docker ps which lists all containers

Let's look at how we can query the status of the container in more depth via the docker inspect JSON interface

docker inspect nginx-production | jq -r .[0].State.Status
running

Then try visiting your website to check nginx is actually working.

Query the system logs of he container

$ docker logs nginx-production  

Where are these logs actually stored?

NGINX_LOG=$(docker inspect nginx-production | jq .[0].LogPath | tr -d \")

In case you want to use a tool like goaccess to process your logs

Creating a systemd unit to auto-start the container at boot

Paste the following into the file /etc/systemd/system/nginx-docker.servce

[Unit]
Description=Nginx-Docker
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/bin/docker start nginx-production

First, we stop the running container.

This is to confirm the systemd unit is actually working at starting the container

sudo docker stop nginx-production

Now start and enable the systemd unit

sudo systemctl start nginx-docker
docker inspect nginx-production | jq -r .[0].State.Status

Restricting container access to the outside world

Restricting egress network access is a great way to improve the security of a web server as it makes the attackers job significantly harder as they can't download their tools or phone home to spawn a reverse shell.

We create iptables rules in the DOCKER-ISOLATION chain so that our nginx server is only allowed to contact the server needed for OCSP stapling (see my tls tutorial if you want to know what that is)

# By default, drop all egress traffic from the nginx container 
sudo iptables \
--insert DOCKER-ISOLATION \
--in-interface docker1 \
--src 10.128.64.128 \
--jump DROP

# Allow the container to contact the OCSP stapling server
sudo iptables \
--insert DOCKER-ISOLATION \
--in-interface docker1 \
--src 10.128.64.128 \
--dst ocsp.comodoca.com \
--proto tcp --dport 80 \
--jump ACCEPT

Setting it up with IPv6

Delete the networks we created in the above steps (if they exist)

sudo docker rm nginx-production
sudo docker network rm docker1

Create a docker network.

Give it the IPv6 subnet allotted by your cloud provider for additional addresses.

The main IPv6 address on the server is 2001:db8:420:d0::d08:a001/64 with a gateway address at 2001:db8:420:d0:1

My provider lets me add 2001:db8:420:d0::d08:a000 to 2001:db8:420:d0::d08:a00f (10 additional addresses). In IPv6 CIDR this is a /124 network.

docker network create \
  --driver bridge \
  --subnet 10.128.64.0/24 \
  --ipv6 \
  --subnet 2001:db8:420:d0::d08:a000/124 \
  --opt com.docker.network.bridge.name=docker1  \
  --opt com.docker.network.bridge.enable_icc=false \
  --opt "com.docker.network.bridge.host_binding_ipv4=$floatingip" \
docker1

Enable proxy NDP

sudo sysctl net.ipv6.conf.eth0.proxy_ndp=1
sudo ip -6 neigh add proxy 2001:db8:420:d0::d08:a00a dev eth0

Creating the container

sudo docker run \
--network docker1 \
--ip 10.128.64.128 \
--ip6 2001:db8:420:d0::d08:a00a \
--name nginx-production \
--volume /var/www/html:/var/www/html:ro \
--volume /etc/ssl:/etc/ssl:ro \
--restart=always \
-p 80:80 -p 443:443 \
nginx:latest

Now just copy the new nginx.conf (uncommented IPv6 lines)

sudo docker cp /etc/nginx/nginx.conf nginx-production:/etc/nginx
sudo docker start nginx-production

Testing it works

Go onto an IPv6 capable client and see if you can access the web server

$ curl --ipv6 https://etherarp.net/robots.txt
User-agent: *
Sitemap: http://etherarp.net/sitemap.xml
Disallow: /ghost/