Replacing Nginx with Caddy in Docker Compose
Posted on 2023-11-09 in Software
The main server in my homelab runs a bunch of services, but the heart of it is a Docker Compose configuration, several of them exposed to the Internet via a reverse proxy webserver.
Before I was using three Docker images to run Nginx, proxy traffic to other services and have it generate Letsencrypt certificates for the necessary domains:
- nginx with the actual Nginx server running.
- nginx-proxy to generate Nginx configurations.
- docker-letsencrypt-nginx-proxy-companion: to handle LetsEncrypt certificate provisioning and renewal.
The relevant configuration in compose.yml looked like this:
services:
nginx:
image: nginx
labels:
com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
UFW_MANAGED: "true"
container_name: ${NGINX_WEB:-nginx}
restart: always
ports:
- "${DOCKER_HTTP:-80}:80"
- "${DOCKER_HTTPS:-443}:443"
volumes:
- ${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d
- ${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d
- ${NGINX_FILES_PATH}/html:/usr/share/nginx/html
- ${NGINX_FILES_PATH}/certs:/etc/nginx/certs:ro
- ${NGINX_FILES_PATH}/htpasswd:/etc/nginx/htpasswd:ro
logging:
driver: ${NGINX_WEB_LOG_DRIVER:-json-file}
options:
max-size: ${NGINX_WEB_LOG_MAX_SIZE:-4m}
max-file: ${NGINX_WEB_LOG_MAX_FILE:-10}
nginx-gen:
image: nginxproxy/docker-gen
command: -notify-sighup ${NGINX_WEB:-nginx} -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
container_name: ${DOCKER_GEN:-nginx-gen}
restart: always
volumes:
- ${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d
- ${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d
- ${NGINX_FILES_PATH}/html:/usr/share/nginx/html
- ${NGINX_FILES_PATH}/certs:/etc/nginx/certs:ro
- ${NGINX_FILES_PATH}/htpasswd:/etc/nginx/htpasswd:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
logging:
driver: ${NGINX_GEN_LOG_DRIVER:-json-file}
options:
max-size: ${NGINX_GEN_LOG_MAX_SIZE:-2m}
max-file: ${NGINX_GEN_LOG_MAX_FILE:-10}
nginx-letsencrypt:
image: nginxproxy/acme-companion
container_name: ${LETSENCRYPT_CONTAINER:-nginx-letsencrypt}
restart: always
volumes:
- ${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d
- ${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d
- ${NGINX_FILES_PATH}/html:/usr/share/nginx/html
- ${NGINX_FILES_PATH}/certs:/etc/nginx/certs:rw
- /var/opt/acme.sh:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
NGINX_DOCKER_GEN_CONTAINER: ${DOCKER_GEN:-nginx-gen}
NGINX_PROXY_CONTAINER: ${NGINX_WEB:-nginx}
logging:
driver: ${NGINX_LETSENCRYPT_LOG_DRIVER:-json-file}
options:
max-size: ${NGINX_LETSENCRYPT_LOG_MAX_SIZE:-2m}
max-file: ${NGINX_LETSENCRYPT_LOG_MAX_FILE:-10}
Other services to proxy defined VIRTUAL_HOST
and LETSENCRYPT_HOST
environment variables.
For example this was my Nextcloud service configuration (I will only show the relevant parts):
services:
nextcloud:
image: nextcloud:latest
container_name: nextcloud
environment:
VIRTUAL_HOST: ${NEXTCLOUD_HOST}
LETSENCRYPT_HOST: ${NEXTCLOUD_HOST}
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}
Besides the Docker compose services configuration, I also required a Nginx configuration template (that's the nginx.tmpl that you can see in the configuration), and other per-service tweaks (like increasing the maximum request body size for my Nextcloud instance).
I had already replaced the Nginx webserver that hosts this blog to Caddy, so I wanted to do the same with the home server. I decided to use caddy-docker-proxy because it was similar to nginx-proxy, so it would simplify the migration. What I didn't expect is how much simpler it would become.
The above configuration was replaced by:
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
restart: unless-stopped
ports:
- "${DOCKER_HTTP:-80}:80"
- "${DOCKER_HTTPS:-443}:443"
networks:
- default
- webproxy
labels: # Global options
caddy.email: ${CADDY_EMAIL}
UFW_MANAGED: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# this volume is needed to keep the certificates
# otherwise, new ones will be re-issued upon restart
- ${CADDY_FILES_PATH}:/data
Proxying a service is now a matter of just adding some labels:
services:
nextcloud:
image: nextcloud:latest
container_name: nextcloud
labels:
caddy: ${NEXTCLOUD_HOST}
caddy.reverse_proxy: "{{upstreams 80}}"
caddy.request_body.max_size: ${NEXTCLOUD_MAX_UPLOAD}
And that was it!
Caddy automatically manages the TLS certificates, so no need for any extra services. It doesn't get any easier than that.
I was also able to remove the Nginx config files, instead replacing them with a couple labels in the containers that required some special configuration.
Before I close off this article I want to highlight how well nginx-proxy and its acme-companion had been working for years with minimal maintenance. Their stability and backwards compatibility had been rock solid, kudos and a big thanks to their creator!