└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Traefik v2 2 | 3 | ###### guide by examples 4 | 5 | ![logo](https://i.imgur.com/yHvP7zM.png) 6 | 7 | *edit* - discovered caddy, seems simpler, 8 | [here](https://github.com/DoTheEvo/selfhosted-apps-docker/tree/master/caddy_v2) is its guide. 9 | 10 | requirements 11 | 12 | - have docker running somewhere 13 | - have a domain `example.com` 14 | - use cloudflare to manage DNS of the domain 15 | - have 80/443 ports open 16 | 17 | chapters 18 | 19 | 1. [traefik routing to docker containers](#1-traefik-routing-to-various-docker-containers) 20 | 2. [traefik routing to a local IP addresses](#2-traefik-routing-to-a-local-IP-addresses) 21 | 3. [middlewares](#3-middlewares) 22 | 4. [let's encrypt certificate HTTP challenge](#4-lets-encrypt-certificate-HTTP-challenge) 23 | 5. [let's encrypt certificate DNS challenge](#5-lets-encrypt-certificate-DNS-challenge-on-cloudflare) 24 | 6. [redirect HTTP traffic to HTTPS](#6-redirect-HTTP-traffic-to-HTTPS) 25 | 26 | # #1 traefik routing to various docker containers 27 | 28 | ![traefik-dashboard-pic](https://i.imgur.com/5jKHJmm.png) 29 | 30 | - **create a new docker network** `docker network create traefik_net`.
31 | Traefik and the containers need to be on the same network. 32 | Compose creates one automatically, but that fact is hidden and there is potential for a fuck up later on. 33 | Better to just create own network and set it as default in every compose file. 34 | 35 | *extra info:* use `docker network inspect traefik_net` to see containers connected to that network 36 | 37 | - **create traefik.yml**
38 | This file contains so called static traefik configuration.
39 | In this basic example there are just few self-explanatory settings.
40 | Since exposedbydefault is set to false, a label `"traefik.enable=true"` will be needed 41 | for containers that should be routed by traefik.
42 | This file will be passed to a docker container using bind mount, 43 | this will be done when we get to docker-compose.yml for traefik. 44 | 45 | `traefik.yml` 46 | ``` 47 | ## STATIC CONFIGURATION 48 | log: 49 | level: INFO 50 | 51 | api: 52 | insecure: true 53 | dashboard: true 54 | 55 | entryPoints: 56 | web: 57 | address: ":80" 58 | 59 | providers: 60 | docker: 61 | endpoint: "unix:///var/run/docker.sock" 62 | exposedByDefault: false 63 | ``` 64 | 65 | later on when traefik container is running, use command `docker logs traefik` 66 | and check if there is a notice stating: `"Configuration loaded from file: /traefik.yml"`. 67 | You don't want to be the moron who makes changes to traefik.yml 68 | and it does nothing because the file is not actually being used. 69 | 70 | - **create `.env`** file that will contain environmental variables.
71 | Domain names, api keys, ip addresses, passwords,... 72 | whatever is specific for one case and different for another, all of that ideally goes here. 73 | These variables will be available for docker-compose when running 74 | the `docker-compose up` command.
75 | This allows compose files to be moved from system to system more freely and changes are done to the .env file, 76 | so there's a smaller possibility for a fuckup of forgetting to change domain name 77 | in some host rule in a big ass compose file or some such. 78 | 79 | `.env` 80 | ``` 81 | MY_DOMAIN=example.com 82 | DEFAULT_NETWORK=traefik_net 83 | ``` 84 | 85 | *extra info:*
86 | Command `docker-compose config` shows how the compose will look 87 | with the variables filled in. 88 | 89 | These variables are only filled in during the compose initial building of container. 90 | If an env variable should be available also inside the running container, 91 | it needs to be declared in the `environment` section of the compose file. 92 | 93 | - **create traefik-docker-compose.yml file**.
94 | It's a simple typical compose file.
95 | Port 80 is mapped since we want traefik to be in charge of what comes on it - using it as an entrypoint. 96 | Port 8080 is for dashboard where traefik shows info. Mount of docker.sock is needed, 97 | so it can actually do its job interacting with docker. 98 | Mount of `traefik.yml` is what gives the static traefik configuration. 99 | The default network is set to the one created in the first step, as it will be set in all other compose files. 100 | 101 | `traefik-docker-compose.yml` 102 | ``` 103 | version: "3.7" 104 | 105 | services: 106 | traefik: 107 | image: "traefik:v2.1" 108 | container_name: "traefik" 109 | hostname: "traefik" 110 | ports: 111 | - "80:80" 112 | - "8080:8080" 113 | volumes: 114 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 115 | - "./traefik.yml:/traefik.yml:ro" 116 | 117 | networks: 118 | default: 119 | external: 120 | name: $DEFAULT_NETWORK 121 | ``` 122 | 123 | - **run traefik-docker-compose.yml**
124 | `docker-compose -f traefik-docker-compose.yml up -d` will start the traefik container. 125 | 126 | traefik is running, you can check it at the ip:8080 where you get the dashboard.
127 | Can also check out logs with `docker logs traefik`. 128 | 129 | *extra info:*
130 | Typically you see guides having just a single compose file called `docker-compose.yml` 131 | with several services/containers in it. Then it's just `docker-compose up -d` to start it all. 132 | You don't even need to bother defining networks when it is all one compose. 133 | But this time I prefer small and separate steps when learning new shit. 134 | So that's why going with custom named docker-compose files as it allows easier separation. 135 | 136 | *extra info2:*
137 | What you can also see in tutorials is no mention of traefik.yml 138 | and stuff is just passed from docker-compose using traefik's commands or labels.
139 | like [this](https://docs.traefik.io/getting-started/quick-start/): `command: --api.insecure=true --providers.docker`
140 | But that way compose files look bit more messy and you still can't do everything from there, 141 | you still sometimes need that damn traefik.yml.
142 | So... for now, going with nicely structured readable traefik.yml 143 | 144 | - **add labels to containers that traefik should route**.
145 | Here are examples of whoami, nginx, apache, portainer.
146 | 147 | > \- "traefik.enable=true" 148 | 149 | enables traefik 150 | 151 | > \- "traefik.http.routers.whoami.entrypoints=web" 152 | 153 | defines router named `whoami` that listens on entrypoint web(port 80) 154 | 155 | > \- "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 156 | 157 | defines a rule for this `whoami` router, specifically that when url 158 | equals `whoami.example.com` (the domain name comes from the `.env` file), 159 | that it means for router to do its job and route it to a service. 160 | 161 | Nothing else is needed, traefik knows the rest from the fact that these labels 162 | are coming from context of a docker container. 163 | 164 | `whoami-docker-compose.yml` 165 | ``` 166 | version: "3.7" 167 | 168 | services: 169 | whoami: 170 | image: "containous/whoami" 171 | container_name: "whoami" 172 | hostname: "whoami" 173 | labels: 174 | - "traefik.enable=true" 175 | - "traefik.http.routers.whoami.entrypoints=web" 176 | - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 177 | 178 | networks: 179 | default: 180 | external: 181 | name: $DEFAULT_NETWORK 182 | ``` 183 | 184 | `nginx-docker-compose.yml` 185 | ``` 186 | version: "3.7" 187 | 188 | services: 189 | nginx: 190 | image: nginx:latest 191 | container_name: nginx 192 | hostname: nginx 193 | labels: 194 | - "traefik.enable=true" 195 | - "traefik.http.routers.nginx.entrypoints=web" 196 | - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" 197 | 198 | networks: 199 | default: 200 | external: 201 | name: $DEFAULT_NETWORK 202 | ``` 203 | 204 | `apache-docker-compose.yml` 205 | ``` 206 | version: "3.7" 207 | 208 | services: 209 | apache: 210 | image: httpd:latest 211 | container_name: apache 212 | hostname: apache 213 | labels: 214 | - "traefik.enable=true" 215 | - "traefik.http.routers.apache.entrypoints=web" 216 | - "traefik.http.routers.apache.rule=Host(`apache.$MY_DOMAIN`)" 217 | 218 | networks: 219 | default: 220 | external: 221 | name: $DEFAULT_NETWORK 222 | ``` 223 | 224 | `portainer-docker-compose.yml` 225 | ``` 226 | version: "3.7" 227 | 228 | services: 229 | portainer: 230 | image: portainer/portainer 231 | container_name: portainer 232 | hostname: portainer 233 | volumes: 234 | - /var/run/docker.sock:/var/run/docker.sock:ro 235 | - portainer_data:/data 236 | labels: 237 | - "traefik.enable=true" 238 | - "traefik.http.routers.portainer.entrypoints=web" 239 | - "traefik.http.routers.portainer.rule=Host(`portainer.$MY_DOMAIN`)" 240 | 241 | networks: 242 | default: 243 | external: 244 | name: $DEFAULT_NETWORK 245 | 246 | volumes: 247 | portainer_data: 248 | 249 | ``` 250 | 251 | - **run the damn containers**
252 | ignore some orphans talk, it's cuz these compose files are in the same directory 253 | and compose uses parent directory name for name of compose project 254 | 255 | `docker-compose -f whoami-docker-compose.yml up -d`
256 | `docker-compose -f nginx-docker-compose.yml up -d`
257 | `docker-compose -f apache-docker-compose.yml up -d`
258 | `docker-compose -f portainer-docker-compose.yml up -d` 259 | 260 | *extra info:*
261 | to stop all containers running: `docker stop $(docker ps -q)` 262 | 263 | # #2 traefik routing to a local IP addresses 264 | 265 | When url should aim at something other than a docker container. 266 | 267 | ![simple-network-diagram-pic](https://i.imgur.com/lTpUvWJ.png) 268 | 269 | - **define a file provider, add required routing and service** 270 | 271 | What is needed is a router that catches some url and route it to some IP.
272 | Previous examples shown how to catch whatever url, on port 80, 273 | but no one told it what to do when something fits the rule. 274 | Traefik just knew since it was all done using labels in the context of a container and 275 | thanks to docker being set as a provider in `traefik.yml`.
276 | For this "sending traffic at some IP" a traefik service is needed, 277 | and to define traefik service a new provider is required, a file provider - just a fucking stupid 278 | file that tells traefik what to do.
279 | Somewhat common is to set `traefik.yml` itself as a file provider so thats what will be done.
280 | Under providers theres a new `file` section and `traefik.yml` itself is set.
281 | Then the dynamic configuration stuff is added.
282 | A router named `route-to-local-ip` with a simple subdomain hostname rule. 283 | What fits that rule, in this case exact url `test.example.com`, 284 | is send to the loadbalancer service which just routes it to a specific IP and specific port. 285 | 286 | `traefik.yml` 287 | ``` 288 | ## STATIC CONFIGURATION 289 | log: 290 | level: INFO 291 | 292 | api: 293 | insecure: true 294 | dashboard: true 295 | 296 | entryPoints: 297 | web: 298 | address: ":80" 299 | 300 | providers: 301 | docker: 302 | endpoint: "unix:///var/run/docker.sock" 303 | exposedByDefault: false 304 | file: 305 | filename: "traefik.yml" 306 | 307 | ## DYNAMIC CONFIGURATION 308 | http: 309 | routers: 310 | route-to-local-ip: 311 | rule: "Host(`test.example.com`)" 312 | service: route-to-local-ip-service 313 | priority: 1000 314 | entryPoints: 315 | - web 316 | 317 | services: 318 | route-to-local-ip-service: 319 | loadBalancer: 320 | servers: 321 | - url: "http://10.0.19.5:80" 322 | ``` 323 | 324 | Priority of the router is set to 1000, a very high value, 325 | beating any possible other routers. 326 | 327 | *extra info:*
328 | Unfortunately the `.env` variables are not working here, 329 | otherwise domain name in host rule and that IP would come from variables. 330 | So heads up that you will definitely forget to change these. 331 | 332 | - **run traefik-docker-compose** and test if it works 333 | 334 | `docker-compose -f traefik-docker-compose.yml up -d`
335 | 336 | # #3 middlewares 337 | 338 | Example of an authentication middleware for any container. 339 | 340 | ![logic-pic](https://i.imgur.com/QkfPYel.png) 341 | 342 | - **create a new file - `users_credentials`** containing username:passwords pairs, 343 | [htpasswd](https://www.htaccesstools.com/htpasswd-generator/) style
344 | Bellow example has password `krakatoa` set to all 3 accounts 345 | 346 | `users_credentials` 347 | ``` 348 | me:$apr1$L0RIz/oA$Fr7c.2.6R1JXIhCiUI1JF0 349 | admin:$apr1$ELgBQZx3$BFx7a9RIxh1Z0kiJG0juE/ 350 | bastard:$apr1$gvhkVK.x$5rxoW.wkw1inm9ZIfB0zs1 351 | ``` 352 | 353 | - **mount users_credentials in traefik-docker-compose.yml** 354 | 355 | `traefik-docker-compose.yml` 356 | ``` 357 | version: "3.7" 358 | 359 | services: 360 | traefik: 361 | image: "traefik:v2.1" 362 | container_name: "traefik" 363 | hostname: "traefik" 364 | ports: 365 | - "80:80" 366 | - "8080:8080" 367 | volumes: 368 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 369 | - "./traefik.yml:/traefik.yml:ro" 370 | - "./users_credentials:/users_credentials:ro" 371 | 372 | networks: 373 | default: 374 | external: 375 | name: $DEFAULT_NETWORK 376 | ``` 377 | 378 | - **add two labels to any container** that should have authentication
379 | - The first label attaches new middleware called `auth-middleware` 380 | to an already existing `whoami` router. 381 | - The second label gives this middleware type basicauth, 382 | and tells it where is the file it should use to authenticate users. 383 | 384 | No need to mount the `users_credentials` here, it's traefik that needs that file 385 | and these labels are a way to pass info to traefik, what it should do 386 | in context of containers. 387 | 388 | `whoami-docker-compose.yml` 389 | ``` 390 | version: "3.7" 391 | 392 | services: 393 | whoami: 394 | image: "containous/whoami" 395 | container_name: "whoami" 396 | hostname: "whoami" 397 | labels: 398 | - "traefik.enable=true" 399 | - "traefik.http.routers.whoami.entrypoints=web" 400 | - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 401 | - "traefik.http.routers.whoami.middlewares=auth-middleware" 402 | - "traefik.http.middlewares.auth-middleware.basicauth.usersfile=/users_credentials" 403 | 404 | networks: 405 | default: 406 | external: 407 | name: $DEFAULT_NETWORK 408 | ``` 409 | 410 | `nginx-docker-compose.yml` 411 | ``` 412 | version: "3.7" 413 | 414 | services: 415 | nginx: 416 | image: nginx:latest 417 | container_name: nginx 418 | hostname: nginx 419 | labels: 420 | - "traefik.enable=true" 421 | - "traefik.http.routers.nginx.entrypoints=web" 422 | - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" 423 | - "traefik.http.routers.nginx.middlewares=auth-middleware" 424 | - "traefik.http.middlewares.auth-middleware.basicauth.usersfile=/users_credentials" 425 | 426 | networks: 427 | default: 428 | external: 429 | name: $DEFAULT_NETWORK 430 | ``` 431 | 432 | - **run the damn containers** and now there is login and password needed 433 | 434 | `docker-compose -f traefik-docker-compose.yml up -d`
435 | `docker-compose -f whoami-docker-compose.yml up -d`
436 | `docker-compose -f nginx-docker-compose.yml up -d`
437 | 438 | # #4 let's encrypt certificate, HTTP challenge 439 | 440 | ![letsencrypt-http-challenge-pic](https://i.imgur.com/yTshxC9.png) 441 | 442 | My understanding of the process, simplified. 443 | 444 | `LE` - Let's Encrypt. A service that gives out free certificates
445 | `Certificate` - a cryptographic key stored in a file on the server, 446 | allows encrypted communication and confirms the identity
447 | `ACME` - a protocol(precisely agreed way of communication) to negotiate certificates 448 | from LE. It is part of traefik.
449 | `DNS` - servers on the internet, translate domain names in to ip address
450 | 451 | Traefik uses ACME to ask LE for a certificate for a specific domain, like `example.com`. 452 | LE answers with some random generated text that traefik puts at a specific place on the server. 453 | LE then asks DNS internet servers for `example.com` and that points to some IP address. 454 | LE looks at that IP address through ports 80/443 for the file containing that random text. 455 | 456 | If it's there then this proves that whoever asked for the certificate controls both 457 | the server and the domain, since it showed control over DNS records. 458 | Certificate is given and is valid for 3 months, traefik will automatically try to renew 459 | when less than 30 days is remaining. 460 | 461 | Now how to actually get it done. 462 | 463 | 464 | - **create an empty acme.json file with 600 permissions** 465 | 466 | This file will store the certificates and all the info about them. 467 | 468 | `touch acme.json && chmod 600 acme.json` 469 | 470 | - **add 443 entrypoint and certificate resolver to traefik.yml**
471 | 472 | In entrypoint section new entrypoint is added called websecure, port 443 473 | 474 | certificatesResolvers is a configuration section that tells traefik 475 | how to use acme resolver to get certificates. 476 | 477 | ``` 478 | certificatesResolvers: 479 | lets-encr: 480 | acme: 481 | #caServer: https://acme-staging-v02.api.letsencrypt.org/directory 482 | storage: acme.json 483 | email: whatever@gmail.com 484 | httpChallenge: 485 | entryPoint: web 486 | ``` 487 | 488 | - the name of the resolver is `lets-encr` and uses acme 489 | - commented out staging caServer makes LE issue a staging certificate, 490 | it is an invalid certificate and wont give green lock but has no limitations, 491 | so it's good for testing. If it's working it will say issued by let's encrypt. 492 | - Storage tells where to store given certificates - `acme.json` 493 | - The email is where LE sends notification about certificates expiring 494 | - httpChallenge is given an entrypoint, so acme does http challenge over port 80 495 | 496 | That is all that is needed for acme 497 | 498 | `traefik.yml` 499 | ``` 500 | ## STATIC CONFIGURATION 501 | log: 502 | level: INFO 503 | 504 | api: 505 | insecure: true 506 | dashboard: true 507 | 508 | entryPoints: 509 | web: 510 | address: ":80" 511 | websecure: 512 | address: ":443" 513 | 514 | providers: 515 | docker: 516 | endpoint: "unix:///var/run/docker.sock" 517 | exposedByDefault: false 518 | 519 | certificatesResolvers: 520 | lets-encr: 521 | acme: 522 | #caServer: https://acme-staging-v02.api.letsencrypt.org/directory 523 | storage: acme.json 524 | email: whatever@gmail.com 525 | httpChallenge: 526 | entryPoint: web 527 | ``` 528 | 529 | - **expose/map port 443 and mount acme.json in traefik-docker-compose.yml** 530 | 531 | Notice that acme.json is **not** :ro - read only 532 | 533 | `traefik-docker-compose.yml` 534 | ``` 535 | version: "3.7" 536 | 537 | services: 538 | traefik: 539 | image: "traefik:v2.1" 540 | container_name: "traefik" 541 | hostname: "traefik" 542 | env_file: 543 | - .env 544 | ports: 545 | - "80:80" 546 | - "443:443" 547 | - "8080:8080" 548 | volumes: 549 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 550 | - "./traefik.yml:/traefik.yml:ro" 551 | - "./acme.json:/acme.json" 552 | 553 | networks: 554 | default: 555 | external: 556 | name: $DEFAULT_NETWORK 557 | ``` 558 | 559 | - **add required labels to containers**
560 | compared to just plain http from first chapter, 561 | it is just changing router's entryPoint from `web` to `websecure` 562 | and assigning certificate resolver named `lets-encr` to the existing router 563 | 564 | `whoami-docker-compose.yml` 565 | ``` 566 | version: "3.7" 567 | 568 | services: 569 | whoami: 570 | image: "containous/whoami" 571 | container_name: "whoami" 572 | hostname: "whoami" 573 | labels: 574 | - "traefik.enable=true" 575 | - "traefik.http.routers.whoami.entrypoints=websecure" 576 | - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 577 | - "traefik.http.routers.whoami.tls.certresolver=lets-encr" 578 | 579 | networks: 580 | default: 581 | external: 582 | name: $DEFAULT_NETWORK 583 | ``` 584 | 585 | `nginx-docker-compose.yml` 586 | ``` 587 | version: "3.7" 588 | 589 | services: 590 | nginx: 591 | image: nginx:latest 592 | container_name: nginx 593 | hostname: nginx 594 | labels: 595 | - "traefik.enable=true" 596 | - "traefik.http.routers.nginx.entrypoints=websecure" 597 | - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" 598 | - "traefik.http.routers.nginx.tls.certresolver=lets-encr" 599 | 600 | networks: 601 | default: 602 | external: 603 | name: $DEFAULT_NETWORK 604 | ``` 605 | - **run the damn containers**
606 | give it a minute
607 | containers will now work only over https and have the greenlock
608 | 609 | *extra info:*
610 | check content of acme.json
611 | delete acme.json if you want fresh start 612 | 613 | # #5 let's encrypt certificate DNS challenge on cloudflare 614 | 615 | ![letsencrypt-dns-challenge-pic](https://i.imgur.com/dkgxFTR.png) 616 | 617 | My understanding of the process, simplified. 618 | 619 | `LE` - Let's Encrypt. A service that gives out free certificates
620 | `Certificate` - a cryptographic key stored in a file on the server, 621 | allows encrypted communication and confirms the identity
622 | `ACME` - a protocol(precisely agreed way of communication) to negotiate certificates 623 | from LE. It is part of traefik.
624 | `DNS` - servers on the internet, translate domain names in to ip address
625 | 626 | Traefik uses ACME to ask LE for a certificate for a specific domain, like `example.com`. 627 | LE answers with some random generated text that traefik puts as a new DNS TXT record. 628 | LE then checks `example.com` DNS records to see if the text is there. 629 | 630 | If it's there then this proves that whoever asked for the certificate controls the domain. 631 | Certificate is given and is valid for 3 months. Traefik will automatically try to renew 632 | when less than 30 days is remaining. 633 | 634 | Benefit over httpChallenge is ability to have wild card certificates. 635 | These are certificates that validate all subdomains `*.example.com`
636 | Also no ports are needed to be open. 637 | 638 | But traefik needs to be able to make these automated changes to DNS records, 639 | so there needs to be support for this from whoever manages sites DNS. 640 | Thats why going with cloudflare. 641 | 642 | Now how to actually get it done. 643 | 644 | - **add type A DNS records for all planned subdomains** 645 | 646 | [whoami, nginx, \*] are used example subdomains, each one should have A-record pointing at traefik IP 647 | 648 | - **create an empty acme.json file with 600 permissions** 649 | 650 | `touch acme.json && chmod 600 acme.json` 651 | 652 | - **add 443 entrypoint and certificate resolver to traefik.yml**
653 | 654 | In entrypoint section new entrypoint is added called websecure, port 443 655 | 656 | certificatesResolvers is a configuration section that tells traefik 657 | how to use acme resolver to get certificates.
658 | 659 | ``` 660 | certificatesResolvers: 661 | lets-encr: 662 | acme: 663 | #caServer: https://acme-staging-v02.api.letsencrypt.org/directory 664 | email: whatever@gmail.com 665 | storage: acme.json 666 | dnsChallenge: 667 | provider: cloudflare 668 | resolvers: 669 | - "1.1.1.1:53" 670 | - "8.8.8.8:53" 671 | ``` 672 | 673 | - the name of the resolver is `lets-encr` and uses acme 674 | - commented out staging caServer makes LE issue a staging certificate, 675 | it is an invalid certificate and wont give green lock but has no limitations, 676 | if it's working it will say issued by let's encrypt. 677 | - Storage tells where to store given certificates - `acme.json` 678 | - The email is where LE sends notification about certificates expiring 679 | - dnsChallenge is specified with a [provider](https://docs.traefik.io/https/acme/#providers), 680 | in this case cloudflare. Each provider needs differently named environment variable 681 | in the .env file, but thats later, here it just needs the name of the provider 682 | - resolvers are IP of well known DNS servers to use during challenge 683 | 684 | `traefik.yml` 685 | ``` 686 | ## STATIC CONFIGURATION 687 | log: 688 | level: INFO 689 | 690 | api: 691 | insecure: true 692 | dashboard: true 693 | 694 | entryPoints: 695 | web: 696 | address: ":80" 697 | websecure: 698 | address: ":443" 699 | 700 | providers: 701 | docker: 702 | endpoint: "unix:///var/run/docker.sock" 703 | exposedByDefault: false 704 | 705 | certificatesResolvers: 706 | lets-encr: 707 | acme: 708 | #caServer: https://acme-staging-v02.api.letsencrypt.org/directory 709 | email: whatever@gmail.com 710 | storage: acme.json 711 | dnsChallenge: 712 | provider: cloudflare 713 | resolvers: 714 | - "1.1.1.1:53" 715 | - "8.8.8.8:53" 716 | ``` 717 | 718 | - **to the `.env` file add required variables**
719 | We know what variables to add based on the [list of supported providers](https://docs.traefik.io/https/acme/#providers).
720 | For cloudflare variables are 721 | - `CF_API_EMAIL` - cloudflare login 722 | - `CF_API_KEY` - global api key 723 | 724 | `.env` 725 | ``` 726 | MY_DOMAIN=example.com 727 | DEFAULT_NETWORK=traefik_net 728 | CF_API_EMAIL=whateverbastard@gmail.com 729 | CF_API_KEY=8d08c87dadb0f8f0e63efe84fb115b62e1abc 730 | ``` 731 | 732 | - **expose/map port 443 and mount acme.json** in traefik-docker-compose.yml 733 | 734 | Notice that acme.json is **not** :ro - read only 735 | 736 | `traefik-docker-compose.yml` 737 | ``` 738 | version: "3.7" 739 | 740 | services: 741 | traefik: 742 | image: "traefik:v2.1" 743 | container_name: "traefik" 744 | hostname: "traefik" 745 | env_file: 746 | - .env 747 | ports: 748 | - "80:80" 749 | - "443:443" 750 | - "8080:8080" 751 | volumes: 752 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 753 | - "./traefik.yml:/traefik.yml:ro" 754 | - "./acme.json:/acme.json" 755 | 756 | networks: 757 | default: 758 | external: 759 | name: $DEFAULT_NETWORK 760 | ``` 761 | 762 | - **add required labels to containers**
763 | compared to just plain http from the first chapter 764 | - router's entryPoint is switched from `web` to `websecure` 765 | - certificate resolver named `lets-encr` assigned to the router 766 | - a label defining main domain that will get the certificate, 767 | in this it is whoami.example.com, domain name pulled from `.env` file 768 | 769 | `whoami-docker-compose.yml` 770 | ``` 771 | version: "3.7" 772 | 773 | services: 774 | whoami: 775 | image: "containous/whoami" 776 | container_name: "whoami" 777 | hostname: "whoami" 778 | labels: 779 | - "traefik.enable=true" 780 | - "traefik.http.routers.whoami.entrypoints=websecure" 781 | - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 782 | - "traefik.http.routers.whoami.tls.certresolver=lets-encr" 783 | - "traefik.http.routers.whoami.tls.domains[0].main=whoami.$MY_DOMAIN" 784 | 785 | 786 | networks: 787 | default: 788 | external: 789 | name: $DEFAULT_NETWORK 790 | ``` 791 | 792 | `nginx-docker-compose.yml` 793 | ``` 794 | version: "3.7" 795 | 796 | services: 797 | nginx: 798 | image: nginx:latest 799 | container_name: nginx 800 | hostname: nginx 801 | labels: 802 | - "traefik.enable=true" 803 | - "traefik.http.routers.nginx.entrypoints=websecure" 804 | - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" 805 | - "traefik.http.routers.nginx.tls.certresolver=lets-encr" 806 | - "traefik.http.routers.nginx.tls.domains[0].main=nginx.$MY_DOMAIN" 807 | 808 | networks: 809 | default: 810 | external: 811 | name: $DEFAULT_NETWORK 812 | ``` 813 | - **run the damn containers**
814 | `docker-compose -f traefik-docker-compose.yml up -d`
815 | `docker-compose -f whoami-docker-compose.yml up -d`
816 | `docker-compose -f nginx-docker-compose.yml up -d`
817 | 818 | - **Fuck that, the whole point of DNS challenge is to get wildcards!**
819 | fair enough
820 | so for wildcard these labels go in to traefik compose. 821 | - same `lets-encr` certificateresolver is used as before, the one defined in traefik.yml 822 | - the wildcard for subdomains(*.example.com) is set as the main domain to get certificate for 823 | - the naked domain(just plain example.com) is set as sans(Subject Alternative Name) 824 | 825 | again, you do need `*.example.com` and `example.com` 826 | set in your DNS control panel as A-record pointing to IP of traefik 827 | 828 | `traefik-docker-compose.yml` 829 | ``` 830 | version: "3.7" 831 | 832 | services: 833 | traefik: 834 | image: "traefik:v2.1" 835 | container_name: "traefik" 836 | hostname: "traefik" 837 | env_file: 838 | - .env 839 | ports: 840 | - "80:80" 841 | - "443:443" 842 | - "8080:8080" 843 | volumes: 844 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 845 | - "./traefik.yml:/traefik.yml:ro" 846 | - "./acme.json:/acme.json" 847 | labels: 848 | - "traefik.enable=true" 849 | - "traefik.http.routers.traefik.tls.certresolver=lets-encr" 850 | - "traefik.http.routers.traefik.tls.domains[0].main=*.$MY_DOMAIN" 851 | - "traefik.http.routers.traefik.tls.domains[0].sans=$MY_DOMAIN" 852 | 853 | networks: 854 | default: 855 | external: 856 | name: $DEFAULT_NETWORK 857 | ``` 858 | 859 | Now if a container wants to be accessible as a subdomain, 860 | it just needs a regular router that has rule for the url, 861 | be on 443 port entrypoint, and use the same `lets-encr` certificate resolver 862 | 863 | `whoami-docker-compose.yml` 864 | ``` 865 | version: "3.7" 866 | 867 | services: 868 | whoami: 869 | image: "containous/whoami" 870 | container_name: "whoami" 871 | hostname: "whoami" 872 | labels: 873 | - "traefik.enable=true" 874 | - "traefik.http.routers.whoami.entrypoints=websecure" 875 | - "traefik.http.routers.whoami.rule=Host(`whoami.$MY_DOMAIN`)" 876 | - "traefik.http.routers.whoami.tls.certresolver=lets-encr" 877 | 878 | networks: 879 | default: 880 | external: 881 | name: $DEFAULT_NETWORK 882 | ``` 883 | 884 | `nginx-docker-compose.yml` 885 | ``` 886 | version: "3.7" 887 | 888 | services: 889 | nginx: 890 | image: nginx:latest 891 | container_name: nginx 892 | hostname: nginx 893 | labels: 894 | - "traefik.enable=true" 895 | - "traefik.http.routers.nginx.entrypoints=websecure" 896 | - "traefik.http.routers.nginx.rule=Host(`nginx.$MY_DOMAIN`)" 897 | - "traefik.http.routers.nginx.tls.certresolver=lets-encr" 898 | 899 | networks: 900 | default: 901 | external: 902 | name: $DEFAULT_NETWORK 903 | ``` 904 | 905 | Here is apache but this time run on the naked domain `example.com`
906 | 907 | `apache-docker-compose.yml` 908 | ``` 909 | version: "3.7" 910 | 911 | services: 912 | apache: 913 | image: httpd:latest 914 | container_name: apache 915 | hostname: apache 916 | labels: 917 | - "traefik.enable=true" 918 | - "traefik.http.routers.apache.entrypoints=websecure" 919 | - "traefik.http.routers.apache.rule=Host(`$MY_DOMAIN`)" 920 | - "traefik.http.routers.apache.tls.certresolver=lets-encr" 921 | 922 | networks: 923 | default: 924 | external: 925 | name: $DEFAULT_NETWORK 926 | ``` 927 | 928 | # #6 redirect http traffic to https 929 | 930 | ![padlocks-pic](https://i.imgur.com/twTDSym.png) 931 | 932 | http stops working with https setup, better to redirect http(80) to https(443).
933 | Traefik has special type of middleware for this purpose - redirectscheme. 934 | 935 | There are several places where this redirect can be declared, 936 | in `traefik.yml`, in the dynamic section when `traefik.yml` itself is set as a file provider.
937 | Or using labels in any running container, this example does it in traefik compose. 938 | 939 | - **add new router and a redirect scheme using labels in traefik compose** 940 | 941 | >\- "traefik.enable=true" 942 | 943 | enables traefik on this traefik container, 944 | not that there is need of the typical routing to a service here, 945 | but other labels would not work without this 946 | 947 | >\- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" 948 | 949 | creates new middleware called `redirect-to-https`, type "redirectscheme" 950 | and assigns it scheme `https`.
951 | 952 | >\- "traefik.http.routers.redirect-https.rule=hostregexp(\`{host:.+}\`)" 953 | 954 | creates new router called `redirect-https`, with a regex rule that 955 | catches any and every incoming request 956 | 957 | >\- "traefik.http.routers.redirect-https.entrypoints=web" 958 | 959 | declares on which entrypoint this router listens - web(port 80)
960 | 961 | >\- "traefik.http.routers.redirect-https.middlewares=redirect-to-https" 962 | 963 | assigns the freshly created redirectscheme middleware to this freshly created router. 964 | 965 | So to sum it up, when a request comes at port 80, router that listens at that entrypoint looks at it. 966 | If it fits the rule, and it does because everything fits that rule, it goes to the next step. 967 | Ultimately it should get to a service, but if there is middleware declared, that middleware goes first, 968 | and since middleware is there, and it is some redirect scheme, it never reaches any service, 969 | it gets redirected using https scheme, which I guess is stating - go for port 443. 970 | 971 | Here is the full traefik compose, with dns challenge labels from previous chapter included: 972 | 973 | `traefik-docker-compose.yml` 974 | ``` 975 | version: "3.7" 976 | 977 | services: 978 | traefik: 979 | image: "traefik:v2.1" 980 | container_name: "traefik" 981 | hostname: "traefik" 982 | env_file: 983 | - .env 984 | ports: 985 | - "80:80" 986 | - "443:443" 987 | - "8080:8080" 988 | volumes: 989 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 990 | - "./traefik.yml:/traefik.yml:ro" 991 | - "./acme.json:/acme.json" 992 | labels: 993 | - "traefik.enable=true" 994 | ## DNS CHALLENGE 995 | - "traefik.http.routers.traefik.tls.certresolver=lets-encr" 996 | - "traefik.http.routers.traefik.tls.domains[0].main=*.$MY_DOMAIN" 997 | - "traefik.http.routers.traefik.tls.domains[0].sans=$MY_DOMAIN" 998 | ## HTTP REDIRECT 999 | - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" 1000 | - "traefik.http.routers.redirect-https.rule=hostregexp(`{host:.+}`)" 1001 | - "traefik.http.routers.redirect-https.entrypoints=web" 1002 | - "traefik.http.routers.redirect-https.middlewares=redirect-to-https" 1003 | 1004 | networks: 1005 | default: 1006 | external: 1007 | name: $DEFAULT_NETWORK 1008 | ``` 1009 | 1010 | - **run the damn containers** and now `http://whoami.example.com` is immediately changed to `https://whoami.example.com` 1011 | 1012 | # stuff to checkout 1013 | - [when everything is done in file provider](https://github.com/pamendoz/personalDockerCompose) 1014 | - [ansible, docker and traefik](https://thoughtfuldragon.com/a-summary-of-how-i-automated-my-server-with-ansible-docker-and-traefik/) 1015 | - [traefik official site blog with good examples reads](https://containo.us/blog/) 1016 | - [traefik v2 forums](https://community.containo.us/c/traefik/traefik-v2) 1017 | --------------------------------------------------------------------------------