I recently finished setting up a completely Dockerized setup of Syncthing and the Syncthing Discovery server (among some other services) behind an Nginx reverse-proxy. The information here is mostly the same as in the docker-letsencrypt-nginx-proxy-companion README, but there are a few gotchas.
What do we get out of this?
- Easy SSL-secured subdomain name access to our Syncthing GUI and Syncthing discovery server.
syncthing.domain.com
anddiscovery.domain.com
- Easy to move the whole setup to a different host
- Easy to add additional reverse-proxied containers without having to restart or reconfigure the nginx stack.
- Client containers can be easily stopped/restarted without affecting the rest of the clients.
- Automatic SSL certificate generation and renewal through the Let’s Encrypt service.
Requirements
-
A host with Docker installed and domain name/subdomains setup.
-
Docker images (these can be pulled at runtime or beforehand, except Syncthing needs some setup first). Note that I don’t provide images for syncthing or discosrv through the Docker Hub…you will need to build your own.
-
Syncthing. See the README for setup.
-
jwilder/docker-gen (From https://github.com/jwilder/docker-gen)
-
jrcs/letsencrypt-nginx-proxy-companion (From https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion)
-
Official Docker nginx image
- Nginx template for docker-gen. We will modify it later.
Setup
- The nginx template needs to be modified to add the following:
- The X-SSL-Cert headers
proxy_set_header X-SSL-Cert $ssl_client_cert;
- ssl_verify_client directive.
ssl_verify_client optional_no_ca;
Notice theif
statement bracketing this directive. Make sure to update the (sub)domain name that you will be using for the discovery server. The discovery server container is the only one that should need this.
Partial nginx.tmpl:
....
proxy_set_header Connection $proxy_connection;
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 $proxy_x_forwarded_proto;
proxy_set_header X-SSL-Cert $ssl_client_cert;
{{ end }}
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
listen 80;
.......
{{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }}
ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }};
{{ end }}
add_header Strict-Transport-Security "max-age=31536000";
{{ if (eq $host "discovery.domain.com") }}
ssl_verify_client optional_no_ca;
{{ end }}
{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
......
- Create directories on your filesystem (you could also just use volumes) to
store the downloaded certificates and the nginx.tmpl file. For our purposes
here, I’m storing certificates in
/usr/local/etc/nginex/certs
and the nginx.tmpl file in/usr/local/etc/nginx/nginx.tmpl
Running
We will end up with five running containers: nginx_run, nginx_gen_run,
nginx_letsencrypt_run, syncthing_run and syncthing_discovery_run. How you manage
your running containers is dependent on your platform. I’m currently using
Ansible to start the containers for this particular set, but I use systemd
service files on another platform. To make it easy for me (and it’s fairly
readable as well), I’ll show the Ansible playbook snippets to run the various
containers. These should be easily translated into regular docker run
statements or Docker Compose stanzas.
- Nginx
- name: run Nginx container
docker:
image: nginx
name: nginx_run
state: running
ports: 80:80,443:443
stdin_open: True
tty: True
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/nginx/conf.d
- /etc/nginx/vhost.d
- /usr/share/nginx/html
- /usr/local/etc/nginx/certs:/etc/nginx/certs:ro
Equivalent docker run syntax (I’ll just put this here for the first example):
docker run -d --name nginx_run \
-p 80:80 -p 443:443
-v /etc/localtime:/etc/localtime:ro \
-v /etc/nginx/conf.d \
-v /etc/nginx/vhost.d \
-v /usr/share/nginx/html \
-v /usr/local/etc/nginx/certs:/etc/nginx/certs:ro \
nginx
- The docker-gen container. Automatically generates reverse-proxy nginx configuration for attached container.
- name: run Nginx-gen container
docker:
image: jwilder/docker-gen
command: -notify-sighup nginx_run -watch -only-exposed -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
name: nginx_gen_run
state: running
stdin_open: True
tty: True
volumes_from: nginx_run
volumes:
- /etc/localtime:/etc/localtime:ro
- /usr/local/etc/nginx/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro"
- /var/run/docker.sock:/tmp/docker.sock:ro
- The Let’s Encrypt nginx client container. Automatically requests and downloads SSL certificates for client containers.
- name: run Nginx-Letsencrypt container
docker:
image: jrcs/letsencrypt-nginx-proxy-companion
name: nginx_letsencrypt_run
state: running
stdin_open: True
tty: True
volumes_from: nginx_run
volumes:
- /etc/localtime:/etc/localtime:ro
- /usr/local/etc/nginx/certs:/etc/nginx/certs:rw
- /var/run/docker.sock:/var/run/docker.sock:ro
env:
NGINX_DOCKER_GEN_CONTAINER: nginx_gen_run
-
Syncthing container (if desired).
-
Note that although the reverse proxy forwards the GUI port 8384, it does not forward the working ports 22000 or 21027/udp. Therefore if you add this client address to another client for direct access, it would be
tcp://<domain.name>:22000
instead oftcp://<sub.domain.name>:22000
. -
Another caveat to running Syncthing in a container is that Local discovery won’t work unless you use host-only networking. This typically won’t be done on the same host as a Discovery server anyways.
-
Also note that I’m keeping my Syncthing configuration in a data-only volume
syncthing_config
. -
The VIRTUAL_HOST, VIRTUAL_PORT and VIRUAL_PROTO are the environment variables that get passed to the docker-gen container to generate the reverse-proxy config. Make sure you set the subdomain you will be using.
-
The LETSENCRYPT_EMAIL and LETSENCRYPT_HOST are for the nginx_letsencrypt container. The LETSENCRYPT_HOST should match the VIRTUAL_HOST.
-
- name: run Syncthing
docker:
image: syncthing
expose: 22000,21027/udp,8384
name: syncthing_run
ports: 22000:22000,21027:21027/udp
state: running
stdin_open: True
tty: True
volumes:
- /home/username:/home/username
- /etc/localtime:/etc/localtime:ro
volumes_from: syncthing_config
env:
VIRTUAL_HOST: syncthing.domain.com
VIRTUAL_PORT: 8483
VIRTUAL_PROTO: https
LETSENCRYPT_HOST: syncthing.domain.com
LETSENCRYPT_EMAIL: user@email.com
- Syncthing Discovery server. Same comments apply for the environment variables here. Notice the -http flag to run the discovery server in proxy mode. This feature is only available (as of today) in the git version, not the v0.12.2 release.
- name: run Syncthing Discovery Server
docker:
image: syncthing_discovery
name: syncthing_discovery_run
command: -http
expose: 8443/tcp
state: running
stdin_open: True
tty: True
volumes:
- /etc/localtime:/etc/localtime:ro
env:
VIRTUAL_HOST: discovery.domain.com
VIRTUAL_PORT: 8443
LETSENCRYPT_HOST: discovery.domain.com
LETSENCRYPT_EMAIL: user@email.com
You can add other containers as well using the same pattern, and they will get certificates (if requested) and a reverse-proxy configuration automatically assigned without having to restart the whole stack.
TODO
-
Try using Docker Networking features from v1.10+ instead of the NGINX_DOCKER_GEN_CONTAINER env variable and legacy style links.
-
Systemd service files to start/restart the containers in the correct order
-
Docker Compose file to start/restart the containers in the correct order
Feedback and corrections are welcome!
Scott