Docker config with traefik as reverse proxy
In the past, I used to self-hosted a Nextcloud instance for things like calendar and fileshare. After that I also hosted my own Bitwarden server. Then even a Wireguard server. In the past I used docker and docker-compose. For ssl stuff I also used Caddy. But recently I was rethinking my server pricing, especially since I saw the recent pricing changes from Hetzner. This made me think, why shouldn’t I update the configs as well?
So first I thought about what applications I wanted: Nextcloud, Bitwarden, VPN server and a personal blog. Once I had that figured out, I thought about how to use them on the server. I could either just install them using the package manager and configure each one. This wasn’t a good option for me as I like my systems clean and want to be able to replicate easily if I change providers. Another method would be to use Flatpak/Snap applications. This doesn’t have the dirty system problem, but if I want to switch providers, I have to reinstall everything on the other system.
So I decided to use containers, especially Docker, since I already had some knowledge of it and it has a lot of resources to read and find bugs. With docker I also like to use docker-compose files to easily start/stop multiple containers and have all the configuration in one file.
So to get started I looked at dockerhub for the applications I wanted:
services:
############################## blog ##############################
blog:
image: nginx:latest
container_name: blog
restart: unless-stopped
volumes:
- ./data/blog:/usr/share/nginx/html
############################## bitwarden ##############################
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
volumes:
- ./data/vaultwarden:/data
environment:
- ADMIN_TOKEN=${VAULTWARDEN_ADMIN_PASSWORD}
############################## nextcloud ##############################
mariadb:
image: mariadb:10.6
container_name: mariadb
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
restart: unless-stopped
volumes:
- ./data/nextcloud/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
nextcloud:
image: nextcloud:latest
container_name: nextcloud
restart: unless-stopped
volumes:
- ./data/nextcloud/cloud:/var/www/html
environment:
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=mariadb
- REDIS_HOST=redis
############################## wireguard ##############################
wg-easy:
image: weejewel/wg-easy
container_name: wg-easy
restart: unless-stopped
environment:
- WG_HOST=wg.${DOMAIN}
- PASSWORD=${WG_PASSWORD}
volumes:
- ./data/wireguard:/etc/wireguard
ports:
- 51820:51820/udp
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
This compose file includes Nextcloud, Bitwarden (Vaultwarden), Wireguard, and a simple nginx server where I can later install my static files for my blog. Most of the configuration is just copied from the dockerhub pages of each image. I also added some environment variables that I will add later via an .env file.
After all that I would still like to have a reverse proxy so that I can browse the pages with ssl/tls encryption. There are many options for this, like Caddy, nginx, apache, … . I looked around and tried to do some things with nginx. While there are nginx proxy images, like nginx-proxy, I would rather use plain nginx and then configure it. But there was the problem that it couldn’t really be done by just dropping some env vars into the nginx container and letting it do it all by itself. So that was a no-go. I also looked at Caddy, but I had already done that once and wanted to try something different.
So I decided to go with Traefik, which is also a very popular reverse proxy that works very well with containers. Traefik does a lot of things on its own if you give it access to the Docker socket. After reading through some examples and documentation, I managed to get a container up and running:
traefik:
image: traefik:latest
container_name: traefik
command:
# - "--log.level=DEBUG"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
# - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.myresolver.acme.email=${EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- 80:80
- 443:443
volumes:
- ./data/traefik/letsencrypt:/letsencrypt
- ./data/traefik/log:/var/log/traefik
- /var/run/docker.sock:/var/run/docker.sock:ro
I hope most of this is self-explanatory. I chose http challange since my domain provider doesn’t support DNS challange (THANKS NAMECHEAP!). The commented out certificateresolver, which should be used if you are still building your site and requesting a lot of certificates from letsencrypt. Letsencrypt has a limit of 50 certificate requests per domain.
Having the container I still need labels so that traefik knows which container it should proxy the request to, so I added some labels to each container depending on the subdomain. I even added similar to each container depending on the subdomain, and also made some special rules for the bitwarden/WG server so that some pages can’t be accessed from the outside:
blog:
...
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
vaultwarden:
...
labels:
- "traefik.enable=true"
- "traefik.http.routers.vaultwarden.rule=Host(`bitwarden.${DOMAIN}`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=myresolver"
- "traefik.http.routers.vaultwarden-admin.rule=Host(`bitwarden.${DOMAIN}`) && PathPrefix(`/admin`)"
- "traefik.http.routers.vaultwarden-admin.entrypoints=websecure"
- "traefik.http.routers.vaultwarden-admin.tls.certresolver=myresolver"
- "traefik.http.routers.vaultwarden-admin.middlewares=local@docker"
nextcloud:
...
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`cloud.${DOMAIN}`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=myresolver"
wg-easy:
...
labels:
- "traefik.enable=true"
- "traefik.http.routers.wg.rule=Host(`wg.${DOMAIN}`)"
- "traefik.http.routers.wg.entrypoints=websecure"
- "traefik.http.routers.wg.tls.certresolver=myresolver"
- "traefik.http.services.wg.loadbalancer.server.port=51821"
- "traefik.http.middlewares.local.ipallowlist.sourcerange=${IPALLOWLIST}"
- "traefik.http.routers.wg.middlewares=local@docker"
With that you could just do it. This is a good configuration in itself. But since I’m also a bit into security, I wanted to protect myself a bit more. In particular, I wanted to protect myself against the various vulnscanners that are out there with free and open source frameworks. I knew about Fail2Ban as an intrusion detection software. After looking around, I found that while it would be sufficient, there were other, newer detection software out there. So I found out about crowdsec.
Crowdsec is kind of the same as Fail2Ban, but with a community aspect. It allows you to interact with lists created by the community and even share malicious IPs with other users. This seemed like a good piece of software that I wanted to try out. The first thing I had to do was register and get an API key. After that and reading through the documentation, I made some changes to my docker-compose config. I added the plugin to traefik, added a new middleware for crowdsec, inserted my api key and enabled logging in traefik. These and some other changes leave me with the final configuration.
docker-compose.yml
services:
############################## reverse proxy ##############################
traefik:
image: traefik:latest
container_name: traefik
depends_on:
- crowdsec
command:
# - "--log.level=DEBUG"
- "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.httpchallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
# - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.myresolver.acme.email=${EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--experimental.plugins.crowdsec-bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.crowdsec-bouncer.version=v1.3.1"
ports:
- 80:80
- 443:443
volumes:
- ./data/traefik/letsencrypt:/letsencrypt
- ./data/traefik/log:/var/log/traefik
- /var/run/docker.sock:/var/run/docker.sock:ro
############################## blog ##############################
blog:
image: nginx:latest
container_name: blog
restart: unless-stopped
volumes:
- ./data/blog:/usr/share/nginx/html
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=myresolver"
- "traefik.http.routers.blog.middlewares=crowdsec@docker"
############################## bitwarden ##############################
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
volumes:
- ./data/vaultwarden:/data
environment:
- ADMIN_TOKEN=${VAULTWARDEN_ADMIN_PASSWORD}
labels:
- "traefik.enable=true"
- "traefik.http.routers.vaultwarden.rule=Host(`bitwarden.${DOMAIN}`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=myresolver"
- "traefik.http.routers.vaultwarden.middlewares=crowdsec@docker"
- "traefik.http.routers.vaultwarden-admin.rule=Host(`bitwarden.${DOMAIN}`) && PathPrefix(`/admin`)"
- "traefik.http.routers.vaultwarden-admin.entrypoints=websecure"
- "traefik.http.routers.vaultwarden-admin.tls.certresolver=myresolver"
- "traefik.http.routers.vaultwarden-admin.middlewares=local@docker"
############################## nextcloud ##############################
mariadb:
image: mariadb:10.6
container_name: mariadb
command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
restart: unless-stopped
volumes:
- ./data/nextcloud/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
nextcloud:
image: nextcloud:latest
container_name: nextcloud
restart: unless-stopped
volumes:
- ./data/nextcloud/cloud:/var/www/html
environment:
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=mariadb
- REDIS_HOST=redis
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`cloud.${DOMAIN}`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=myresolver"
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.crowdsec-bouncer.crowdseclapikey=${CROWDSEC_LAPI_KEY}"
- "traefik.http.routers.nextcloud.middlewares=crowdsec@docker"
############################## wireguard ##############################
wg-easy:
image: weejewel/wg-easy
container_name: wg-easy
restart: unless-stopped
environment:
- WG_HOST=wg.${DOMAIN}
- PASSWORD=${WG_PASSWORD}
volumes:
- ./data/wireguard:/etc/wireguard
ports:
- 51820:51820/udp
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
labels:
- "traefik.enable=true"
- "traefik.http.routers.wg.rule=Host(`wg.${DOMAIN}`)"
- "traefik.http.routers.wg.entrypoints=websecure"
- "traefik.http.routers.wg.tls.certresolver=myresolver"
- "traefik.http.services.wg.loadbalancer.server.port=51821"
- "traefik.http.middlewares.local.ipallowlist.sourcerange=${IPALLOWLIST}"
- "traefik.http.routers.wg.middlewares=local@docker"
############################## crowdsec ##############################
crowdsec:
image: crowdsecurity/crowdsec
container_name: crowdsec
restart: unless-stopped
environment:
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve
- ENROLL_KEY=${CROWDSEC_ENROLL_KEY}
- BOUNCER_KEY_traefikBouncer=${CROWDSEC_LAPI_KEY}
volumes:
- ./data/traefik/log:/var/log/traefik:ro
- ./data/crowdsec/db:/var/lib/crowdsec/data
- ./data/crowdsec/config:/etc/crowdsec
- ./data/crowdsec/acquis.d:/etc/crowdsec/acquis.d
traefik.yaml
filenames:
- /var/log/traefik/*
labels:
type: traefik
.env
EMAIL=
DOMAIN=
VAULTWARDEN_ADMIN_PASSWORD=
MYSQL_ROOT_PASSWORD=
MYSQL_PASSWORD=
WG_PASSWORD=
IPALLOWLIST=172.16.0.0/12
CROWDSEC_LAPI_KEY=
CROWDSEC_ENROLL_KEY=
Now just fill in the .env file and the server runs without problems. I also uploaded all the code to github: https://github.com/crasoke/get-traefiked. If you have any questions or more ideas to the project, just contact me or do a PR or something.
Greetings