Setting up Portainer with Traefik using Cloudflare
This is a guide on setting up a new server for self hosting in my preferred configuration. We’ll use Portainer to manage Docker Containers and Stack. And we’ll use Traefik as a reverse proxy configured to pull certificates from Cloudflare directly instead of LetsEncrypt.
It also assumes a couple things
- You have a domain purchased through and/or connected to Cloudflare
- You have a computer or VM setup with Ubuntu Server, or any other Linux OS
- You know how to configure your router
Docker Setup
Install docker using the official guides according to your operating system. I always use Ubuntu Server. Once it’s installed, let’s setup Docker Swarm.
sudo docker swarm init
If you wanted to setup a distributed file system like GlusterFS, now is it the time to do it. But going forward, we’ll just assume that it is setup, and you have a folder called docker_volumes
at the root of your drive. If you don’t want to do that, make sure to edit docker volumes into the configuration files instead.
Portainer Setup
Sometimes people like to setup Traefik first. Personally, I like to setup Portainer first that way Traefik can be managed by Portainer for easier configuration changes.
Either way, we still need to make a traefik network before setting up Portainer. You can do that like this:
docker network create --driver=overlay traefik-public
Configuration File
version: "3.3"
services:
agent:
image: portainer/agent:sts
environment:
AGENT_CLUSTER_ADDR: tasks.agent
DOMAIN: portainer.<YOUR DOMAIN HERE>
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- agent-network
deploy:
mode: global
placement:
constraints:
- node.platform.os == linux
portainer:
image: portainer/portainer-ce:sts
command: -H tcp://tasks.agent:9001 --tlsskipverify
volumes:
- /docker_volumes/portainer_portainer-data:/data
ports:
- target: 9443
published: 9443
mode: host
networks:
- agent-network
- traefik-public
environment:
DOMAIN: portainer.<YOUR DOMAIN HERE>
deploy:
placement:
constraints:
- node.role == manager
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.routers.portainer-http.rule=Host(`portainer.<YOUR DOMAIN HERE>`)
- traefik.http.routers.portainer-http.entrypoints=http
- traefik.http.routers.portainer-http.middlewares=https-redirect
- traefik.http.routers.portainer-https.rule=Host(`portainer.<YOUR DOMAIN HERE>`)
- traefik.http.routers.portainer-https.entrypoints=https
- traefik.http.routers.portainer-https.tls=true
- traefik.http.routers.portainer-https.tls.certresolver=cloudflare
- traefik.http.services.portainer.loadbalancer.server.port=9000
networks:
agent-network:
driver: overlay
attachable: true
traefik-public:
external: true
Explanation
This starts two services. An agent and the main service. The agent is deployed on all nodes in the swarm, while the main service sits on the manager node. Which is going to be the node you setup docker swarm from, or the node you’re probably on now. Note the docker deploy constraints.
Another thing to look out for is our port configuration. This explicitly tells docker to open up Portainer to port 9000 and 9443. Don't open up these ports on your router. I believe this will also punch a hole through UFW, if you use that. Look up docker iptables to disable it.
There are a few labels related to Treafik. Those tell Traefik how to open up Portainer to the internet, but right now they don't do anything, at least until Traefik is setup.
Running Portainer
Copy the above contents into a yaml file called portainer_stack.yml
on your server. Then run the following command to deploy it:
sudo docker stack deploy -c portainer_stack.yml portainer
You can check the status with sudo docker ps
. At this point, you should be able to navigate to https://<SERVER IP>:9443
and setup Portainer.
Traefik Setup
Before getting to this step, make sure you've done the following:
- Purchased a domain through Cloudflare you can use for all your self hosting services (I use
dalenw.net
). - Pointed a wildcard at your home IP address like
*.dalenw.net
- Said DNS entry is proxied
- Port forwarding setup on your router for HTTP/HTTPS (80/443) to your manager node
Now let's setup Traefik. First thing we need is a hashed password to access the Traefik admin page that is stored as an environment variable.
$ export HASHED_PASSWORD=$(openssl passwd -apr1)
Password: $ enter your password here
Verifying - Password: $ re enter your password here
echo $HASHED_PASSWORD
Copy the results to a text file or something to store temporarily.
The other information we need is your Cloudflare email and API key. Here's how to get that:
- Go to your Cloudflare Profile
- Click on API Tokens on the left hand side
- Click on View under your Global API Key
- Write it down in temporary place
Before we can add the Traefik stack, there's one more thing we need to do. We need to add a node label to our manager node so that Traefik will always deploy to it.
- In Portainer, click on Swarm -> Details
- Click on the manager node
- Add a new node label. Name:
traefik-public.traefik-public-certificates
Value:true
In your Portainer instance, navigate to stacks and click on Add Stack. Call it traefik
. Copy the following configuration file, then we'll add the environment variables.
version: "3.3"
services:
traefik:
# Use the latest Traefik image
image: traefik:v2.11
restart: always
ports:
# Listen on port 80, default for HTTP, necessary to redirect to HTTPS
- target: 80
published: 80
mode: host
# Listen on port 443, default for HTTPS
- target: 443
published: 443
mode: host
environment:
USERNAME: admin
DOMAIN: traefik.<YOUR DOMAIN HERE>
EMAIL: ${CLOUDFLARE_EMAIL}
CLOUDFLARE_EMAIL: ${CLOUDFLARE_EMAIL}
CLOUDFLARE_API_KEY: ${CLOUDFLARE_API_KEY}
deploy:
placement:
constraints:
# Make the traefik service run only on the node with this label
# as the node with it has the volume for the certificates
- node.labels.traefik-public.traefik-public-certificates == true
labels:
- io.portainer.accesscontrol.public
# Enable Traefik for this service, to make it available in the public network
- traefik.enable=true
# Use the traefik-public network (declared below)
- traefik.docker.network=traefik-public
# Use the custom label "traefik.constraint-label=traefik-public"
# This public Traefik will only use services with this label
# That way you can add other internal Traefik instances per stack if needed
- traefik.constraint-label=traefik-public
# admin-auth middleware with HTTP Basic auth
# Using the environment variables USERNAME and HASHED_PASSWORD
- traefik.http.middlewares.admin-auth.basicauth.users=admin:${HASHED_PASSWORD?Variable not set}
# https-redirect middleware to redirect HTTP to HTTPS
# It can be re-used by other stacks in other Docker Compose files
- traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
# traefik-http set up only to use the middleware to redirect to https
# Uses the environment variable DOMAIN
- traefik.http.routers.traefik-public-http.rule=Host(`traefik.<YOUR DOMAIN HERE>`)
- traefik.http.routers.traefik-public-http.entrypoints=http
- traefik.http.routers.traefik-public-http.middlewares=https-redirect
# traefik-https the actual router using HTTPS
# Uses the environment variable DOMAIN
- traefik.http.routers.traefik-public-https.rule=Host(`traefik.<YOUR DOMAIN HERE>`)
- traefik.http.routers.traefik-public-https.entrypoints=https
- traefik.http.routers.traefik-public-https.tls=true
# Use the special Traefik service api@internal with the web UI/Dashboard
- traefik.http.routers.traefik-public-https.service=api@internal
# Use the "le" (Let's Encrypt) resolver created below
- traefik.http.routers.traefik-public-https.tls.certresolver=cloudflare
# Enable HTTP Basic auth, using the middleware created above
- traefik.http.routers.traefik-public-https.middlewares=admin-auth
# Define the port inside of the Docker service to use
- traefik.http.services.traefik-public.loadbalancer.server.port=8080
volumes:
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
- /var/run/docker.sock:/var/run/docker.sock:ro
# Mount the volume to store the certificates
- /docker_volumes/traefik_traefik-public-certificates:/certificates
command:
# Enable Docker in Traefik, so that it reads labels from Docker services
- --providers.docker
# Add a constraint to only use services with the label "traefik.constraint-label=traefik-public"
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
# Do not expose all Docker services, only the ones explicitly exposed
- --providers.docker.exposedbydefault=false
# Enable Docker Swarm mode
- --providers.docker.swarmmode
# Create an entrypoint "http" listening on address 80
- --entrypoints.http.address=:80
# Create an entrypoint "https" listening on address 443
- --entrypoints.https.address=:443
# Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
# - [email protected]
# Store the Let's Encrypt certificates in the mounted volume
# - --certificatesresolvers.le.acme.storage=/certificates/acme.json
# Use the TLS Challenge for Let's Encrypt
# - --certificatesresolvers.le.acme.tlschallenge=true
## Cloudflare configuration
- --certificatesresolvers.cloudflare.acme.dnschallenge=true
- --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cloudflare.acme.email=${CLOUDFLARE_EMAIL}
- --certificatesresolvers.cloudflare.acme.storage=/certificates/cloudflare.json
# Enable the access log, with HTTP requests
- --accesslog
# Enable the Traefik log, for configurations and errors
- --log
# Enable the Dashboard and API
- --api
networks:
# Use the public network created to be shared between Traefik and
# any other service that needs to be publicly available with HTTPS
- traefik-public
# - traefik-internal
# volumes:
# Create a volume to store the certificates, there is a constraint to make sure
# Traefik is always deployed to the same Docker node with the same volume containing
# the HTTPS certificates
# traefik-public-certificates:
networks:
# Use the previously created public network "traefik-public", shared with other
# services that need to be publicly available via this Traefik
traefik-public:
external: true
traefik-internal:
driver: overlay
And don't forget about the environment variables for our new stack. Values should be self explanatory.
HASHED_PASSWORD=
CLOUDFLARE_EMAIL=
CLOUDFLARE_API_KEY=
It should be noted that I figured most of this out from this website: https://dockerswarm.rocks/traefik/#preparation, I've just tweaked it here and there to fit my use case.
Now traefik is running. And if everything works, you should now be able to access traefik.yourdomain.net and portainer.yourdomain.net proxied behind Cloudflare.
Traefik Labels Explained
Traefik is a reverse proxy. It does a lot for you, and at the end of the day, connecting a container to be accessible from the internet from anywhere only has a couple requirements.
- The container or service is on the
traefik-public
network. If you have a service that runs multiple containers, then traffic will be distributed evenly across them. - The container has the appropriate traefik configuration labels to tell traefik what to do.
These are the labels for Portainer:
deploy:
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
- traefik.http.routers.portainer-http.rule=Host(`portainer.<YOUR DOMAIN HERE>`)
- traefik.http.routers.portainer-http.entrypoints=http
- traefik.http.routers.portainer-http.middlewares=https-redirect
- traefik.http.routers.portainer-https.rule=Host(`portainer.<YOUR DOMAIN HERE>`)
- traefik.http.routers.portainer-https.entrypoints=https
- traefik.http.routers.portainer-https.tls=true
- traefik.http.routers.portainer-https.tls.certresolver=cloudflare
- traefik.http.services.portainer.loadbalancer.server.port=9000
There are two routers and a service. The router uses an entry point as declared in our traefik configuration. We have two, http
& https
. We also declare a load balancer to point it to the http port that portainer runs from. The last thing to point out is the certresolver. It's cloudflare
, again as declared earlier in our treafik configuration.
At the end of the day, I simply copy and paste the label group and change the names. In this case, it would be replacing all instances of portainer
with ghost
if I was hosting a ghost blog or something.