Installing Docker with Portainer & Traefik
Most the services that I’ll be running on my home server will be Docker containers. Therefore, I decided to make a dedicated virtual machine in Proxmox to be my Docker host machine. I’ll install Portainer to manage my Docker containers and Traefik to securely connect to my containers.
The Traefik configuration described in this post is only for internal access. None of my services are exposed to the public. I’m planning on exposing some of my services but I’ll use a different setup for that.
Creating a new VM
The first step is to create a new VM and choosing the host OS. After searching around and trying some Linux distributions (like Alpine), I settled on Ubuntu server because I was already familiar with Ubuntu and I wanted something that would just work. I really like the idea behind Alpine Linux, but it was too much hassle for me to get it up and running correctly. Maybe I’ll move my Docker containers to a new VM running Alpine in the future.
I gave the VM a virtual disk of 64 GB, 4 GB of memory and 2 CPU cores. After deploying some containers I discovered that my memory usage average was around 80%, so I increased the memory for the VM to 6 GB. I enabled the firewall and made rules for ping and SSH connections.
Now it was time to start the VM for the first time. I followed mostly the default settings during installation except for the IP, I disabled DHCP and set a static IP outside my routers DHCP range. What’s handy on Ubuntu is that at the end of the installation procedure you can already select some packages to install. Here I selected Docker so Ubuntu would install it directly for me.
When the VM was up and running, I logged in to the VM over SSH and installed updates.
1
2
sudo apt update
sudo apt upgrade
Secondly I installed the QEMU Guest Agent.
1
sudo apt-get install qemu-guest-agent
When the QEMU Guest Agent was installed, I shut down the VM, enabled the QEMU Guest Agent in Proxmox in the tab Options and started the VM again.
The last thing I did was setting a firewall rule to block all outgoing connections to my local network. This made sure that the containers could not access any service outside the VM by default. Later I can add exceptions like allowing access to my TrueNAS sharing service.
After checking that the Docker service was working, I made a backup in Proxmox.
Finishing the Docker installation
I checked if Docker was installed by running the hello-world container.
1
sudo docker run hello-world
This printed a success message in the terminal. The only thing that bothered me was that I needed sudo to access Docker. I followed the official Docker documentation to change this behaviour, so I could manage Docker as non-root user.
Portainer
Quick start
Getting started with Portainer was really easy when following the official documentation. I added a firewall rule in Proxmox to allow incoming traffic on port 9443. Now I could browse to the web UI and created an user account.
Docker compose
Starting the Portainer container from the CLI is great to get started, but it becomes a real hassle when you have to add a lot of labels to it. Luckily there is a great solution for this: Docker compose. In my home folder, I created a directory for Portainer and a subdirectory data.
1
2
3
mkdir portainer
cd portainer
mkdir data
In the portainer directory, I created a docker-compose.yml file containing all the relevant settings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: "3"
services:
portainer:
container_name: portainer
image: portainer/portainer-ce:latest
ports:
- 9443:9443
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/myuser/portainer/data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.entrypoints=web"
- "traefik.http.routers.portainer.rule=Host(`portainer.local.mywebsite.be`)"
- "traefik.http.middlewares.portainer-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.portainer.middlewares=portainer-https-redirect"
- "traefik.http.routers.portainer-secure.entrypoints=websecure"
- "traefik.http.routers.portainer-secure.rule=Host(`portainer.local.mywebsite.be`)"
- "traefik.http.routers.portainer-secure.tls=true"
- "traefik.http.routers.portainer-secure.service=portainer"
- "traefik.http.services.portainer.loadbalancer.server.port=9443"
- "traefik.http.services.portainer.loadbalancer.server.scheme=https"
networks:
proxy:
external: true
In this Docker compose file I already included all the needed labels for Traefik.
Configuring Traefik
The objective is to have a series of DNS records (like portainer.local.mywebsite.be, pihole.local.mywebsite.be…) all pointing to the internal IP address of my Docker host container. In this container I’ll have Traefik listening on ports 80 and 443 for HTTP and HTTPS request and route those request to the corresponding service/container. I have my domain managed by Cloudflare, so I can configure wildcard SSL certificates with Cloudflare so all of my internal domains have a valid SSL certificate.
Getting all of this working was a real hassle with a lot of trial-and-error. I’ll not walk you through everything I did while trying to get it working. Instead, I’ll walk you through all the steps I would have to do if I had to set it all up again from scratch.
There isn’t one guide or video that I followed, but there are some videos and documentations that were especially helpful.
File structure
Before we can start the Traefik container we need to make the required file structure. This can be done with the mkdir command to make directories and the touch command to make files. The final structure is listed below. I made the traefik directory in my users home folder but it can be anywhere.
1
2
3
4
5
6
7
traefik/
├── data
│ ├── certs
│ │ └── acme.json
│ ├── config.yml
│ └── traefik.yml
└── docker-compose.yml
A short description of the use case for every file I made:
traefik/data/certs/acme.json- Here can Traefik store the SSL certs it gets from Cloudflare.
traefik/data/config.yml- For manual static configuration. Here I can add routes to services outside the VM.
traefik/data/traefik.yml- Contains all the required configuration for Traefik.
traefik/docker-compose.yml- The Docker compose file to start the Traefik container.
Next we need to change the permissions of the traefik/data/certs/acme.json file.
1
chmod 600 /home/myuser/traefik/data/certs/acme.json
The Cloudflare API token
We need an API token for Traefik to be able to get SSL certificates from Cloudflare. So I created an API token in the Cloudflare dashboard with the following permissions:
- Zone / Zone / Read
- Zone / DNS / Edit
Creating the Traefik network
In order for the dynamic configuration of Traefik to work, we need to make a Docker network to which both the Traefik container as well as all the containers Traefik needs to route to, have to connect. I called my Traefik network proxy but the name can be anything.
1
docker network create proxy
The Traefik configuration file
Next we need to populate the traefik/data/traefik.yml file. This file contains all the Traefik configuration. I based mine on the lovely boilerplate from Christian Lempa (see resources).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
global:
checkNewVersion: false
sendAnonymousUsage: false
# -- Enable API and Dashboard
api:
dashboard: true
insecure: true
# -- Change EntryPoints here...
entryPoints:
web:
address: :80
# -- (Optional) Redirect all HTTP to HTTPS
# http:
# redirections:
# entryPoint:
# to: websecure
# scheme: https
websecure:
address: :443
# -- Configure CertificateResolver here...
certificatesResolvers:
cloudflare:
acme:
email: MY-EMAIL
storage: /etc/traefik/certs/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
# -- Disable TLS Cert verification check
serversTransport:
insecureSkipVerify: true
# -- List the providers here...
providers:
docker:
exposedByDefault: false
file:
directory: /etc/traefik
watch: true
I decided to not redirect all HTTP to HTTPS by default but instead configure that for each container individually, so I’ve more control over when the redirection happens and when not.
I replaced MY-EMAIL with the email-address with which my Cloudflare account is registered.
The insecureSkipVerify: true is necessary to be able to connect to services from Traefik over HTTPS that have self-signed certificates. An example of such a service is Portainer when you connect to it over the port 9443. So this label tells Traefik that it can accept self-signed certificates from the services.
As last thing we need to list the providers that provide the routing information for Traefik. The first provider is the Docker host itself. This enables me to dynamically configure new routes when adding new containers through labels on these containers (see adding containers). I set exposedByDefault: false so that Traefik only configures routes for containers containing the traefik.enable=true label. The second provider is a static provider. Traefik will watch the contents of the /etc/traefik directory (inside the container) and apply any routers and middlewares I configure in configuration files inside this directory. This is where I can add routes to services outside my Docker VM.
Adding authentication
One of the nice things about Traefik is that I can easily add authentication for a service by enabling a Basic-Auth middleware. I added this config to my static configuration file to have a my-auth middleware that I can use anywhere when a service does not have any authentication build in.
1
2
3
4
5
6
7
8
http:
middlewares:
my-auth:
basicAuth:
users:
- myuser:HASHED-PASSWD
headerField: "X-WebAuth-User"
removeHeader: true
I replaced myuser with the username I wanted to use and generated the hashed password.
1
htpasswd -n myuser
The Docker compose file
The last thing to do before I can deploy the Traefik container is writing the Docker compose file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3'
services:
reverse-proxy:
image: traefik:latest
container_name: traefik
networks:
- proxy
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /home/myuser/traefik/data:/etc/traefik
- /etc/localtime:/etc/localtime:ro
environment:
- CF_DNS_API_TOKEN=CLOUDFLARE-API-TOKEN
- CF_ZONE_API_TOKEN=CLOUDFLARE-API-TOKEN
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=web"
- "traefik.http.routers.traefik.rule=Host(`traefik-dashboard.local.mywebsite.be`)"
- "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
- "traefik.http.routers.traefik-secure.entrypoints=websecure"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik-dashboard.local.mywebsite.be`)"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=local.mywebsite.be"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.local.mywebsite.be"
- "traefik.http.routers.traefik-secure.service=api@internal"
- "traefik.http.routers.traefik-secure.middlewares=my-auth@file"
restart: unless-stopped
networks:
proxy:
external: true
I connect the Traefik container to the proxy network I prepared earlier and I bound ports 80 and 443 for HTTP and HTTPS traffic. I also bound port 8080 to access the Traefik dashboard in case the traefik-dashboard.local.mywebsite.be domain I set up does not work.
The first bound volume is so that Traefik can scan for new containers and configure routes accordingly. The second is the directory with the static routes configuration files and the Traefik configuration file, and the last volume is simply to set the correct timezone in the Traefik container.
There are only two environment variables necessary, those to pass the Cloudflare API token to the Traefik container.
Lastly we come to the list of labels. Most of them are described in a next section because they are the same for each container for which I want to enable Traefik. The following three labels is where the real magic for the wildcard SSL certificates happens:
1
2
3
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=local.mywebsite.be"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.local.mywebsite.be"
Deploying the container
Now everything is ready to deploy the container. One last thing to do is adding firewall rules in Proxmox to allow HTTP and HTTPS connections, and adding DNS records in Cloudflare for each domain I want to use pointing at the internal IP address of my Docker host VM.
Now it is just as simple as
1
docker compose up -d
to launch the Traefik container.
After launching some more containers with the correctly configured labels, I was able to see all the configured routes in the Traefik dashboard. And the SSL certificates where working wonderfully so after a lot of struggling I found the correct configurations 🥳.
Resources
- Is this the BEST Reverse Proxy for Docker? // Traefik Tutorial by Christian Lempa
- Put Wildcard Certificates and SSL on EVERYTHING - Traefik Tutorial by Techno Tim
- Official Traefik documentation
- Lego docs about Cloudflare API
- Traefik boilerplates by Christian Lempa
- Cloudflare API docs
- And a lot of forum posts
Adding containers
This post is getting long enough, so I’ll write separate posts for each container and service I deploy containing the Traefik labels. To have some general overview I’m ending this post with an overview of the default labels I need to add to a container to make it work with Traefik.
Default labels
1
2
3
4
5
6
7
8
9
labels:
- "traefik.enable=true"
- "traefik.http.routers.MY-SERVICE.entrypoints=web"
- "traefik.http.routers.MY-SERVICE.rule=Host(`MY-SERVICE.local.mywebsite.be`)"
- "traefik.http.middlewares.MY-SERVICE-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.MY-SERVICE.middlewares=MY-SERVICE-https-redirect"
- "traefik.http.routers.MY-SERVICE-secure.entrypoints=websecure"
- "traefik.http.routers.MY-SERVICE-secure.rule=Host(`MY-SERVICE.local.mywebsite.be`)"
- "traefik.http.routers.MY-SERVICE-secure.tls=true"
I can replace MY-SERVICE with anything I want, but I’ll change it mostly to the name of the container.
traefik.enable=true- Enable Traefik for this container.
traefik.http.routers.MY-SERVICE.entrypoints=web- Accept HTTP requests for this service.
traefik.http.routers.MY-SERVICE.rule=Host(`MY-SERVICE.local.mywebsite.be`)- Configure HTTP domain for this service.
traefik.http.middlewares.MY-SERVICE-https-redirect.redirectscheme.scheme=https- Create a middleware that redirects any HTTP requests to HTTPS requests.
traefik.http.routers.MY-SERVICE.middlewares=MY-SERVICE-https-redirect- Use the freshly configured redirect middleware for HTTP connections.
traefik.http.routers.MY-SERVICE-secure.entrypoints=websecure- Accept HTTPS request for this service.
traefik.http.routers.MY-SERVICE-secure.rule=Host(`MY-SERVICE.local.mywebsite.be`)- Configure HTTPS domain for this service.
traefik.http.routers.MY-SERVICE-secure.tls=true- Use SSL certificates for the HTTPS connections.
Optional labels
These labels are for the websecure (HTTPS) router. I can off course change them for the web (HTTP) router if needed.
traefik.http.routers.MY-SERVICE-secure.middlewares=my-auth@file- Use my authentication middleware to authenticate users for this service.
traefik.http.routers.MY-SERVICE-secure.service=MY-SERVICEtraefik.http.services.MY-SERVICE.loadbalancer.server.port=PORT- Traefik forwards by default to the first port the container is listening to. This way I can tell Traefik to which port it should forward all the requests for this service.
traefik.http.routers.MY-SERVICE-secure.service=MY-SERVICEtraefik.http.services.MY-SERVICE.loadbalancer.server.scheme=https- By default Traefik connects to services over HTTP, this is to tell Traefik to use HTTPS instead of HTTP for this service.
traefik.docker.network=proxy- Explicitly tell Traefik which Docker network to use if there are multiple Docker networks connected to the container.
Network
Lastly, do not forget to put the container in the proxy network!
1
2
3
4
5
6
7
8
9
services:
container:
...
networks:
- proxy
networks:
proxy:
external: true