blog.lazkani.io/content/posts/let-s-play-with-traefik.md

438 lines
14 KiB
Markdown

+++
title = "Let's play with Traefik"
author = ["Elia el Lazkani"]
date = 2021-06-24T21:00:00+02:00
lastmod = 2021-06-28T00:00:42+02:00
tags = ["docker", "linux", "traefik", "nginx", "ssl", "letsencrypt"]
categories = ["container"]
draft = false
+++
I've been playing around with containers for a few years now. I find them very useful.
If you host your own, like I do, you probably write a lot of _nginx_ configurations, maybe _apache_.
If that's the case, then you have your own solution to get certificates.
I'm also assuming that you are using _let's encrypt_ with _certbot_ or something.
Well, I didn't want to anymore. It was time to consolidate. Here comes Traefik.
<!--more-->
## Traefik {#traefik}
So [Traefik](https://doc.traefik.io/traefik/) is
> an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them.
Which made me realize, I still need _nginx_ somewhere. We'll see when we get to it. Let's focus on _Traefik_.
### Configuration {#configuration}
If you run a lot of containers and manage them, then you probably use _docker-compose_.
I'm still using `version 2.3`, I know I am due to an upgrade but I'm working on it slowly.
It's a bigger project... One step at a time.
Let's start from the top, literally.
<a id="code-snippet--docker-compose-header"></a>
```yaml
---
version: '2.3'
services:
```
<div class="admonition note">
<p class="admonition-title">Note</p>
Upgrading to `version 3.x` of _docker-compose_ requires the creation of _network_ to _link_ containers together. It's worth investing into, this is not a _docker-compose_ tutorial.
</div>
Then comes the service.
<a id="code-snippet--docker-compose-service-traefik"></a>
```yaml
traefik:
container_name: traefik
image: "traefik:latest"
restart: unless-stopped
mem_limit: 40m
mem_reservation: 25m
```
and of course, who can forget the volume mounting.
<a id="code-snippet--docker-compose-traefik-volumes"></a>
```yaml
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
```
### Design {#design}
Now let's talk design to see how we're going to configuse this bad boy.
I want to _Traefik_ to listen on ports `80` and `443` at a minimum to serve traffic.
Let's do that.
<a id="code-snippet--docker-compose-traefik-config-listeners"></a>
```yaml
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
```
and let's not forget to map them.
<a id="code-snippet--docker-compose-traefik-port-mapping"></a>
```yaml
ports:
- "80:80"
- "443:443"
```
Next, we would like to redirect `http` to `https` always.
<a id="code-snippet--docker-compose-traefik-config-https-redirect"></a>
```yaml
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
```
We are using docker, so let's configure that as the provider.
<a id="code-snippet--docker-compose-traefik-config-provider"></a>
```yaml
- --providers.docker
```
We can set the log level.
<a id="code-snippet--docker-compose-traefik-config-log-level"></a>
```yaml
- --log.level=INFO
```
If you want a _dashboard_, you have to enable it.
<a id="code-snippet--docker-compose-traefik-config-dashboard"></a>
```yaml
- --api.dashboard=true
```
And finally, if you're using Prometheus to scrape metrics... You have to enable that too.
<a id="code-snippet--docker-compose-traefik-config-prometheus"></a>
```yaml
- --metrics.prometheus=true
```
### Let's Encrypt {#let-s-encrypt}
Let's talk **TLS**. You want to serve encrypted traffic to users. You will need an _SSL Certificate_.
Your best bet is _open source_. Who are we kidding, you'd want to go with _let's encrypt_.
Let's configure _acme_ to do just that. Get us certificates. In this example, we are going to be using _Cloudflare_.
<a id="code-snippet--docker-compose-traefik-config-acme"></a>
```yaml
- --certificatesresolvers.cloudflareresolver.acme.email=<your@email.here>
- --certificatesresolvers.cloudflareresolver.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cloudflareresolver.acme.storage=./acme.json
```
<div class="admonition warning">
<p class="admonition-title">warning</p>
_Let's Encrypt_ have set limits on **how many** certificates you can request per certain amount of time. To test your certificate request and renewal processes, use their staging infrastructure. It is made for such purpose.
</div>
Then we mount it, for persistence.
<a id="code-snippet--docker-compose-traefik-volumes-acme"></a>
```yaml
- "./traefik/acme.json:/acme.json"
```
Let's not forget to add our _Cloudflare_ **API** credentials as environment variables for _Traefik_ to use.
<a id="code-snippet--docker-compose-traefik-environment"></a>
```yaml
environment:
- CLOUDFLARE_EMAIL=<your-cloudflare@email.here>
- CLOUDFLARE_API_KEY=<your-api-key-goes-here>
```
### Dashboard {#dashboard}
Now let's configure _Traefik_ a bit more with a bit of labeling.
First, we specify the _host_ _Traefik_ should listen for to service the _dashboard_.
<a id="code-snippet--docker-compose-traefik-labels"></a>
```yaml
labels:
- "traefik.http.routers.dashboard-api.rule=Host(`dashboard.your-host.here`)"
- "traefik.http.routers.dashboard-api.service=api@internal"
```
With a little bit of _Traefik_ documentation searching and a lot of help from `htpasswd`, we can create a `basicauth` login to protect the dashboard from public use.
<a id="code-snippet--docker-compose-traefik-labels-basicauth"></a>
```yaml
- "traefik.http.routers.dashboard-api.middlewares=dashboard-auth-user"
- "traefik.http.middlewares.dashboard-auth-user.basicauth.users=<user>:$$pws5$$rWsEfeUw9$$uV45uwsGeaPbu8RSexB9/"
- "traefik.http.routers.dashboard-api.tls.certresolver=cloudflareresolver"
```
### Middleware {#middleware}
I'm not going to go into details about the _middleware_ flags configured here but you're welcome to check the _Traefik_ middleware [docs](https://doc.traefik.io/traefik/middlewares/overview/).
<a id="code-snippet--docker-compose-traefik-config-middleware"></a>
```yaml
- "traefik.http.middlewares.frame-deny.headers.framedeny=true"
- "traefik.http.middlewares.browser-xss-filter.headers.browserxssfilter=true"
- "traefik.http.middlewares.ssl-redirect.headers.sslredirect=true"
```
### Full Configuration {#full-configuration}
Let's put everything together now.
<a id="code-snippet--docker-compose-traefik"></a>
```yaml
traefik:
container_name: traefik
image: "traefik:latest"
restart: unless-stopped
mem_limit: 40m
mem_reservation: 25m
ports:
- "80:80"
- "443:443"
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --providers.docker
- --log.level=INFO
- --api.dashboard=true
- --metrics.prometheus=true
- --certificatesresolvers.cloudflareresolver.acme.email=<your@email.here>
- --certificatesresolvers.cloudflareresolver.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cloudflareresolver.acme.storage=./acme.json
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/acme.json:/acme.json"
environment:
- CLOUDFLARE_EMAIL=<your-cloudflare@email.here>
- CLOUDFLARE_API_KEY=<your-api-key-goes-here>
labels:
- "traefik.http.routers.dashboard-api.rule=Host(`dashboard.your-host.here`)"
- "traefik.http.routers.dashboard-api.service=api@internal"
- "traefik.http.routers.dashboard-api.middlewares=dashboard-auth-user"
- "traefik.http.middlewares.dashboard-auth-user.basicauth.users=<user>:$$pws5$$rWsEfeUw9$$uV45uwsGeaPbu8RSexB9/"
- "traefik.http.routers.dashboard-api.tls.certresolver=cloudflareresolver"
- "traefik.http.middlewares.frame-deny.headers.framedeny=true"
- "traefik.http.middlewares.browser-xss-filter.headers.browserxssfilter=true"
- "traefik.http.middlewares.ssl-redirect.headers.sslredirect=true"
```
## nginx {#nginx}
[nginx](https://nginx.org/en/) pronounced
> [engine x] is an HTTP and reverse proxy server, a mail proxy server, and a generic TCP/UDP proxy server, originally written by Igor Sysoev.
In this example, we're going to assume you have a _static blog_ generated by a _static blog generator_ of your choice and you would like to serve it for people to read it.
So let's do this quickly as there isn't much to tell except when it comes to labels.
<a id="code-snippet--docker-compose-service-nginx"></a>
```yaml
nginx:
container_name: nginx
image: nginxinc/nginx-unprivileged:alpine
restart: unless-stopped
mem_limit: 8m
command: ["nginx", "-enable-prometheus-metrics", "-g", "daemon off;"]
volumes:
- "./blog/:/usr/share/nginx/html/blog:ro"
- "./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro"
environment:
- NGINX_BLOG_PORT=80
- NGINX_BLOG_HOST=<blog.your-host.here>
```
We are mounting the blog directory from our _host_ to `/usr/share/nginx/html/blog` as **read-only** into the _nginx_ container. We are also providing _nginx_ with a template configuration and passing the variables as _environment_ variables as you noticed. It is also mounted as **read-only**. The configuration template looks like the following, if you're wondering.
```nginx
server {
listen ${NGINX_BLOG_PORT};
server_name localhost;
root /usr/share/nginx/html/${NGINX_BLOG_HOST};
location / {
index index.html;
try_files $uri $uri/ =404;
}
}
```
### Traefik configuration {#traefik-configuration}
So, _Traefik_ configuration at this point is a little bit tricky for the first time.
First, we configure the _host_ like we did before.
<a id="code-snippet--docker-compose-nginx-labels"></a>
```yaml
labels:
- "traefik.http.routers.blog-http.rule=Host(`blog.your-host.here`)"
```
We tell _Traefik_ about our service and the _port_ to loadbalance on.
<a id="code-snippet--docker-compose-nginx-labels-service"></a>
```yaml
- "traefik.http.routers.blog-http.service=blog-http"
- "traefik.http.services.blog-http.loadbalancer.server.port=80"
```
We configure the _middleware_ to use configuration defined in the _Traefik_ middleware configuration section.
<a id="code-snippet--docker-compose-nginx-labels-middleware"></a>
```yaml
- "traefik.http.routers.blog-http.middlewares=blog-main"
- "traefik.http.middlewares.blog-main.chain.middlewares=frame-deny,browser-xss-filter,ssl-redirect"
```
Finally, we tell it about our resolver to generate an _SSL Certificate_.
<a id="code-snippet--docker-compose-nginx-labels-tls"></a>
```yaml
- "traefik.http.routers.blog-http.tls.certresolver=cloudflareresolver"
```
### Full Configuration {#full-configuration}
Let's put the _nginx_ service together.
<a id="code-snippet--docker-compose-nginx"></a>
```yaml
nginx:
container_name: nginx
image: nginxinc/nginx-unprivileged:alpine
restart: unless-stopped
mem_limit: 8m
command: ["nginx", "-enable-prometheus-metrics", "-g", "daemon off;"]
volumes:
- "./blog/:/usr/share/nginx/html/blog:ro"
- "./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro"
environment:
- NGINX_BLOG_PORT=80
- NGINX_BLOG_HOST=<blog.your-host.here>
labels:
- "traefik.http.routers.blog-http.rule=Host(`blog.your-host.here`)"
- "traefik.http.routers.blog-http.service=blog-http"
- "traefik.http.services.blog-http.loadbalancer.server.port=80"
- "traefik.http.routers.blog-http.middlewares=blog-main"
- "traefik.http.middlewares.blog-main.chain.middlewares=frame-deny,browser-xss-filter,ssl-redirect"
- "traefik.http.routers.blog-http.tls.certresolver=cloudflareresolver"
```
## Finale {#finale}
It's finally time to put everything together !
```yaml
---
version: '2.3'
services:
traefik:
container_name: traefik
image: "traefik:latest"
restart: unless-stopped
mem_limit: 40m
mem_reservation: 25m
ports:
- "80:80"
- "443:443"
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --providers.docker
- --log.level=INFO
- --api.dashboard=true
- --metrics.prometheus=true
- --certificatesresolvers.cloudflareresolver.acme.email=<your@email.here>
- --certificatesresolvers.cloudflareresolver.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cloudflareresolver.acme.storage=./acme.json
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/acme.json:/acme.json"
environment:
- CLOUDFLARE_EMAIL=<your-cloudflare@email.here>
- CLOUDFLARE_API_KEY=<your-api-key-goes-here>
labels:
- "traefik.http.routers.dashboard-api.rule=Host(`dashboard.your-host.here`)"
- "traefik.http.routers.dashboard-api.service=api@internal"
- "traefik.http.routers.dashboard-api.middlewares=dashboard-auth-user"
- "traefik.http.middlewares.dashboard-auth-user.basicauth.users=<user>:$$pws5$$rWsEfeUw9$$uV45uwsGeaPbu8RSexB9/"
- "traefik.http.routers.dashboard-api.tls.certresolver=cloudflareresolver"
- "traefik.http.middlewares.frame-deny.headers.framedeny=true"
- "traefik.http.middlewares.browser-xss-filter.headers.browserxssfilter=true"
- "traefik.http.middlewares.ssl-redirect.headers.sslredirect=true"
nginx:
container_name: nginx
image: nginxinc/nginx-unprivileged:alpine
restart: unless-stopped
mem_limit: 8m
command: ["nginx", "-enable-prometheus-metrics", "-g", "daemon off;"]
volumes:
- "./blog/:/usr/share/nginx/html/blog:ro"
- "./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro"
environment:
- NGINX_BLOG_PORT=80
- NGINX_BLOG_HOST=<blog.your-host.here>
labels:
- "traefik.http.routers.blog-http.rule=Host(`blog.your-host.here`)"
- "traefik.http.routers.blog-http.service=blog-http"
- "traefik.http.services.blog-http.loadbalancer.server.port=80"
- "traefik.http.routers.blog-http.middlewares=blog-main"
- "traefik.http.middlewares.blog-main.chain.middlewares=frame-deny,browser-xss-filter,ssl-redirect"
- "traefik.http.routers.blog-http.tls.certresolver=cloudflareresolver"
```
Now we're all set to save it in a `docker-compose.yaml` file and
```bash
docker-compose up -d
```
If everything is configured correctly, your blog should pop-up momentarily.
**Enjoy !**