439 lines
14 KiB
Markdown
439 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 !**
|