My server setup: reverse-proxy and free SSL on Docker thanks to Traefik

I show you my VPS setup with Docker, Traefik, the Portainer UI, and watchtower to keep images up-to-date. This helped me and my friends in keeping the VPS tidy and functional.

TL;DR

  • Do not install anything but Docker on the server
  • Use Docker to run applications
  • Use docker-compose to keep application separated in stacks
  • Use Traefik 1.7 as a reverse https proxy
  • Use Portainer as web UI for Docker
  • Keep images in running containers up-to-date with watchtower
  • Use some bash aliases to speed up your common operations

How to do that? Just read titles and copy code.

Doesn’t work? Read carefully all the provided instructions, if you have some problem don’t be shy and write to me, you can find my contacts on ald.ooo.

How we structured our server and why

I have a shared VPS with a couple of friends. We host on it some web services and we use it to test and deploy small projects, both private and public.

Here is what we wanted and how we achieved it.

How to keep a server clean and tidy

Since the server is shared between three people and that we also have applications on the server that need some continuity, for example our sites or some Telegram bots, we impose ourselves to keep it clean and tidy.

One of the biggest risks is to start installing applications, libraries and spread configuration files until different versions of the same program start to conflict, updates start to take more time and more attention, the configurations of one of us corrupt the installation of another and everything becomes unmanageable.

So the first rule we set between us is to limit ourselves in the installation of programs and libraries and to keep all the configurations and environment variables in our home user.

Docker

Then we decide to use Docker to ship everything, even for testing.

For those unfamiliar with Docker, it is a tool that allows you to encapsulate a program, its data and its configurations in a container.

For example, you can start a Node.js container, copy the files to be executed into it, mount any external files such as volumes and pass the environment variables to it.

All without the need to install Node.js on the server.

To date, the things installed on the VPS still be Docker, docker-compose and some python library needed for the docker-compose.

You can learn more about Docker taking a look at their quick start. Going on on this article I’ll assume you know how Docker works.

Docker compose

docker-compose is a tool for running multi-container Docker applications from a YAML file.

For example, if we want to run MongoDB, Some APIs in Node.js and a front-end with Nginx, we can just write a docker-file.yml with the parameters to be passed to Docker.

docker-compose takes care to build the images, prepare a virtual network, run containers, set environment variables, mount volumes, expose ports and so on.

Moreover, this setup facilitates the use of networks, that allow containers on the same stack to communicate safely without exposing ports on the Internet.

We will see a docker-compose example later on, but if want to know more about the docker-compose you can explore their documentation. It will be useful to understand better our configuration.

TLS, please

Chrome and Firefox and most of the browsers have deprecated unsecured http websites.

It is now old news, as the first steps began in 2016 as we can see from the blog posts of the two most popular browsers. However, recently we clash against this fact, as the sites are marked as unsafe and many permissions are denied. Now not only is it not safe to run a site on http, but it is practically impossible to make it accessible to others.

So we needed some certificates. Let’s Encrypt releases free certificates for all, so it is a good start. But we are lazy people, so we want also to automate this step with a tool that generates and manages the certificates automatically.

It would also be nice if this tool could also act as a reverse proxy, by relaying the incoming traffic to target containers.

Fortunately, this perfect tool exists, and it’s called Traefik.

Traefik is a reverse proxy with built-in TLS certificates management.
We use Traefik v1.7 since it is easier to use with respect to v2, which is thought for Kubernetes.

This allows us to assign a domain or subdomain to a container by adding a label to it. When visiting this domain, Traefik will redirect the traffic to the container, adding the TLS layer for the https connection. Moreover, we can stop exposing ports, since Trafik communicates with the container through the internal network.

We will see how to set up Traefik on our Docker installation, but if you are curious you can have a sneak peek at their getting started docs.

Yes, but I need a UI too!

Of course. Portainer is a great UI for Docker. We use it primarily to inspect the status, logs, and configs of the running containers, but you can use it to run containers too.

Anyway, Portainer support only docker-compose v2, but we love the new features of v3. Moreover, some things are easier and faster to be done from a CLI after some practice, so we usually prefer the raw terminal to the web UI.

If you need something more advanced you can take a look at Rancher. I found very useful Rancher v1 since it has a plugin with an integrated reverse proxy with TLS and its UI was very advanced and complete staying handy. Unfortunately, v2 has been taught on Kubernetes and it has become a lot more complicated to be installed, managed and used.

Keep containers online and up to date, without losing data

Every time there is a new version of the image used in one of your containers, watchtower pulls it and restarts gracefully a new container with the newer image.

I’ll provide instructions, but here there is its repository on GitHub.

Another important thing to consider is Docker’s restart: always policy, which restarts a new container with the same configurations as the previous one in case the application crashes.

This is useful also for updating Docker. It’s important to perform a sudo apt update && sudo apt upgrade sometimes to keep the server updated. If there is an update for Docker do it without any worries. The daemon will be restarted after the update and all the containers with restart: always will be restarted together with the daemon.

Note that the data inside a container are ephemeral. If you have a database, mount a local path on the server as a volume inside the container. The containerized application will store the data in such that folder, and when the container is restarted after a crash will find again its data in that path.

Configuration

Install docker

Just follow the official documentation.

Install docker-compose

I’ve built this simple bash script to install and update docker-compose. Unfortunately, the update must be done manually, but just running this script is enough to replace the old version with the new one.

Tip: if you clone my repository vps-conf from your server, you’ll get all the scripts and files used in this article.

#!/bin/bash# get latest docker compose released tag
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)
# Install docker-compose
sh -c "curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose"
chmod +x /usr/local/bin/docker-compose
sh -c "curl -L https://raw.githubusercontent.com/docker/compose/${COMPOSE_VERSION}/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose"
# Output compose version
docker-compose -v

Run Traefik, Portainer, and watchtower

First, create a directory for the Portainer data. This folder will be mounted as a volume to the container so that the configurations will remain.

mkdir portainer-data

Then create a file called traefik.toml with the Traefik configurations.
We have already set it up to redirect http connections to https.
Remember to change <your-email>. This will notify you in case of errors with some certificates.

defaultEntryPoints = ["https","http"][entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[retry][docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
network = "traefik"
# prevent exposing each container by default
# you'll need to add the `traefik.enable=true` label to containers
#exposedByDefault = false
# will expose all your containers on <container-name>.<your.domain>
# override adding to containers the label
# `trafik.frontend.rule=Host:<your.other.domain>`
#domain=<your.domain>
[acme]
email = "<your-email>"
storage = "/etc/traefik/acme/acme.json"
entryPoint = "https"
onHostRule = true
[acme.tlsChallenge]
[acme.httpChallenge]
entryPoint = "http"

Finally, prepare the deployment file for the traefik-portainer-watchtower stack. Create a docker-compose.yml in the same directory of the Portainer data folder and the Traefik configurations.

version: '3.7'services:
traefik:
image: traefik:v1.7
container_name: traefik
restart: always
ports:
- "80:80"
- "443:443"
networks:
- default
- traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.toml:/etc/traefik/traefik.toml
- ./acme:/etc/traefik/acme
labels:
- traefik.frontend.headers.customResponseHeaders=Access-Control-Allow-Origin:*
stdin_open: true
tty: true
portainer:
image: portainer/portainer:latest
container_name: portainer
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./portainer-data:/data
labels:
- traefik.frontend.rule=Host:portainer.<your-domain>
stdin_open: true
tty: true
watchtower:
image: v2tec/watchtower:latest
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
stdin_open: true
tty: true
networks:
traefik:
name: traefik

This docker-compose will create a stack of three services: traefik, portainer, and watchtower. The only exposed ports will be the one of Traefik (80 for http and 443 for https). Portainer will be accessible through Traefik: remember to change <your-domain>. watchtower doesn’t need to be exposed.

portainer-data and traefik.toml are mounted as volumes.

Finally, we passed the Docker socket to all this container, since they need to query the Docker daemon to manage containers. Be careful when giving container access to the Docker daemon: it will be able to run, stop and delete all your containers! Give it only to trusted services, and only if you understand why this is necessary.

You can now run your containers with the following command.

docker-compose up -d

Ship and run your application under https

To expose your container with a public domain, you only need to apply this label on it.

traefik.frontend.rule=Host:<your-domain>

If you run it with docker-compose, you will also need to attach it to the traefik network.

Here there is a docker-compose.yml that you can use as an example for your application.

version: "3.7"services:
db:
image: mongo # the MongoDB image from Docker Hub
container_name: mongodb # default name: myapp_db_1
restart: always # restart the container if crashes
volumes:
# will mount the db-data dir inside the cwd as volume
- ./db-data:/data/db
server:
# build an image using the files and the Dockerfile in ./server
build: ./server
image: myapp/server # name of the built image (optional)
container_name: server
restart: always
depends_on: # starts after the db
- mongo
networks: # attach the container to the traefik network,
- traefik # so that traefik can route traffic to it
- default # attach it also to the default network to see db
labels:
# define the domain associated to this service
# DNS must point to your server
- traefik.frontend.rule=Host:api.myapp.com
frontend:
image: nginx
container_name: frontend
restart: always
volumes:
# mount the html as read-only (least privilege principle)
- ./frontend:/usr/share/nginx/html:ro
networks:
# we don't need the default network, frontend its stand-alone
- traefik
labels:
# assign two domains to this service, the first is the primary
- traefik.frontend.rule=Host:myapp.com,en.myapp.com
# link the traefik network created in the traefik's docker-compose
networks:
traefik:
external:
name: traefik

Bash aliases with some shortcuts

In the repository, you will find also a script that installs this bash aliases.

alias down="docker-compose down"
alias up="docker-compose up -d"
alias run="docker run -dit --restart=always"
alias stop="docker rm -f"
alias logs="docker logs"

Conclusions

That’s all, thank you for your attention. I hope that was useful. If so, please clap your hands 👏

If you find some errors on the code or the instructions, or you have some suggestions, please reach me through the contacts on my website or open an issue or PR on the GitHub repo.

I’ll be glad to know your setup so that I and other people can take inspiration from that. Share it in the comments block.

Do you want to get in touch? Feel free to contact me 😉

🔗 https://ald.ooo — 👨‍💻 Infrastructure Engineer @ BendingSpoons

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store