Over the last 5+ years I have done all my development on local virtual machines managed by Vagrant and provisioned by Puppet. I even created a fairly well-received FOSS called PuPHPet.
At the end of 2017 I started really looking into containers, and as of January started working on what will become PuPHPet’s successor, Dashtainer.
While this is not a post on containers in general, or Dashtainer, the thing I will talk about fits in perfectly in the container world and is used heavily in Dashtainer.
Problems with Docker
While Docker is an amazingly useful tool, it does not come without its own set of problems.
One of the biggest is what I call Docker port dancing. In Docker, you can bind a port on your host to forward to a container.
For example, you can bind port 80
on host to port 80
on a container, so going to http://localhost
will automatically forward the request to the container. In this way you can bind a webserver (80
), PHP-FPM (9000
) and MySQL (3306
) and very quickly have a complete working environment on your local machine without having to actually install any of those tools, existing only within their containers.
If you ever have only a single project, this may be fine, but once you start spinning up more projects you quickly realize the biggest limitation: you cannot bind a port on host multiple times.
If port 80
is mapped to web-server-A
you must choose another port to bind for web-server-B
and web-server-C
. This can quickly get old because you must remember that http://localhost
goes to A
, http://localhost:81
goes toB
and http://localhost:82
goes to C
. Of course the actual port you bind is completely up to you so you can do 8080
or 8000
or any unused port on your local machine.
On virtual machines this problem does not really occur because you can assign a static IP address to your servers, and bind it to your system’s hosts file (/etc/hosts
). Containers are ephemeral by nature and do not normally get created on your host’s network but rather private networks with their own random IP addresses within special ranges. However, you must edit /etc/hosts
for every VM you spin up and the list grows with the number of projects you handle.
Træfik solves both of these problems, first by removing the need to use ports in URLs and second by not needing you to edit /etc/hosts
at all.
What is Træfik?
Træfik (pronounced traffic, spelled Traefik from now on) is a reverse proxy / load balancer similar to HAProxy or Nginx in reverse proxy mode.
Simply put, as a reverse proxy it monitors traffic to specified ports (80
,443
) and routes traffic to the proper endpoint.
Traefik includes baked-in support for Docker and you can configure it almost fully through flags, with no need for config files. It supports hot-loading and automatically detects changes to environment. Best of all, it supports Let’s Encrypt right out of the box.
Traefik runs as a separate container and this single container can work across any number of separate projects you want. It works by listening to the Docker daemon and reacting to labels
you define for each container.
Creating a Traefik Container
First create the network Traefik will use:
1
docker network create --driver bridge traefik_webgateway
Then create the actual Traefik container:
1
2
3
4
5
6
7
8
docker container run -d --name traefik_proxy \
--network traefik_webgateway \
-p 80:80 \
-p 8080:8080 \
--restart always \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /dev/null:/traefik.toml \
traefik --api --docker
With the above we,
- tell Traefik to watch to a specific network (
--network traefik_webgateway
) for new containers. Any containers attached to this network can be monitored by Traefik. - tell Traefik to attach itself to the Docker daemon (
--volume /var/run/docker.sock:/var/run/docker.sock
). This is what allows Traefik to listen to the above network and read other containers’ labels. - decline the use of a config file (
--volume /dev/null:/traefik.toml
) to highlight Traefik’s ability to be completely configured via arguments. - bind host’s ports
80
and8080
(-p 80:80
and-p 8080:8080
) to Traefik. - enable the Traefik GUI dashboard (
--api
)
How Traefik Solves the Port Dance
Traefik can automatically pick up any containers that use the traefik_webgateway
network and reads labels applied.
For example, to spin up a new Nginx container you could do something like:
1
2
3
4
5
6
7
docker run -d --name some-nginx \
-v ${PWD}:/usr/share/nginx/html:ro \
--network traefik_webgateway \
--label traefik.docker.network=traefik_webgateway \
--label traefik.frontend.rule=Host:some-nginx.localhost \
--label traefik.port=80 \
nginx:alpine
Here we tell Traefik that this container’s hostname is some-nginx.localhost
and it receives traffic on port 80
.
If you open some-nginx.localhost in Chrome1 you should see the Nginx container responding.
Using hostnames directly without having to append port numbers to them makes working with Docker containers much easier than having to remember which port goes with which project and which container.
Traefik GUI
Above I mentioned the Traefik GUI dashboard. It listens on port 8080
so simply open localhost:8080 and you will see all the containers Traefik is currently monitoring for changes.
Non-port 80 Example
Some containers do not listen on port 80 by default. This is fine because you can forward any traffic on port 80
on the container’s hostname to the specified port.
For example, MailHog’s GUI sits on port 8025
. You can run it with:
1
2
3
4
5
6
docker run -d --name some-mailhog \
--network traefik_webgateway \
--label traefik.docker.network=traefik_webgateway \
--label traefik.frontend.rule=Host:mailhog.localhost \
--label traefik.port=8025 \
mailhog/mailhog
Even though the container listens to 8025
, opening mailhog.localhost will automatically forward traffic to the proper port.
If you open the Traefik dashboard you will see this new container listed.
When you run docker container rm -f some-mailhog
it will automatically be removed.
Built-in Let’s Encrypt Support
Once you are ready to go live with your website configuring Traefik to automatically request and maintain a valid Let’s Encrypt SSL certificate is fairly easy!
There are some extra arguments you must define but nothing too foreign:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
docker container run -d --name traefik_proxy \
--network traefik_webgateway \
-p 80:80 \
-p 443:443 \
-p 8080:8080 \
--restart always \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /dev/null:/traefik.toml \
--volume /root/acme.json:/root/acme.json \
traefik --docker --logLevel=INFO \
--acme \
--acme.acmelogging \
--acme.dnschallenge=false \
--acme.entrypoint="https" \
--acme.httpchallenge \
--acme.httpChallenge.entryPoint="http" \
--acme.onhostrule=true \
--acme.storage="/root/acme.json" \
--acme.email="your-email-here@example.com" \
--entrypoints="Name:http Address::80 Redirect.EntryPoint:https" \
--entrypoints="Name:https Address::443 TLS" \
--defaultentrypoints="http,https"
Since we will handle SSL traffic we add -p 443:443
to the ports list, and since this is for a live server we remove the dashboard (--api
).
Let’s Encrypt stores its data in files and it requires special permissions before it will work. Unfortunately this is the only thing you cannot do via only CLI arguments. You must create this file and set permissions properly before running the above command.
Simply do touch /root/acme.json && chmod 600 /root/acme.json
and you are all set.
We redirect all 80
traffic to 443
to encrypt all traffic (as you should already be doing!). Let’s Encrypt cert is generated after it pings http://your-website.com/.well-known/acme-challenge
to verify ownership.
Traefik will take care of keeping all certs up to date.
Explicitly Disable Traefik for Non-HTTP Services
It seems by default Traefik will attempt to generate a Let’s Encrypt cert for all containers, even if the containers are not on the Traefik network.
To prevent Let’s Encrypt errors from breaking your build, you must explicitly disable Traefik on containers that do not need a cert, like a PHP-FPM container.
It is easy enough:
1
2
3
4
docker run -d --name php-fpm \
--network private \
--label traefik.enable=false \
your-php/image
The above is not required on dev since you will not be generating SSL certs.
Limitations
Traefik only handles HTTP/HTTPS traffic. It cannot currently handle TCP/UDP.
This means any database containers cannot be aliased to a hostname, and you will need to do the port dance on these services.
This is an unfortunate limitation, and the GitHub issue has been open for a while without much movement.
This does not mean your services cannot communicate with each other via normal Docker hostnames. For example, a service named php-fpm
on network foobar
can still communicate with a service named mysql
on the same foobar
network, using the hostname mysql
.
You, however, cannot access mysql
from your host via a GUI like Sequel Pro or MySQL Administrator. You can either bind a port from host to container, or use something like Adminer.
Via docker-compose.yml
If you prefer to try this out using Docker-Compose, create the following 3 files:
1
2
; traefik/.env
COMPOSE_PROJECT_NAME=traefik
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# traefik/docker-compose.yml
version: '3.2'
services:
proxy:
image: traefik
command: --api --docker --docker.domain=docker.localhost --logLevel=DEBUG
networks:
- webgateway
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /dev/null:/traefik.toml
networks:
webgateway:
driver: bridge
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
# app/docker-compose.yml
version: '3.2'
networks:
private:
public:
external:
name: traefik_webgateway
services:
nginx:
image: nginx:alpine
labels:
- traefik.backend=nginx
- traefik.docker.network=traefik_webgateway
- traefik.frontend.rule=Host:some-nginx.localhost
- traefik.port=80
networks:
- private
- public
mailhog:
image: mailhog/mailhog
labels:
- traefik.backend=mailhog
- traefik.docker.network=traefik_webgateway
- traefik.frontend.rule=Host:mailhog.localhost
- traefik.port=8025
networks:
- private
- public
Run docker-compose up -d
in the traefik
directory, then the app
directory.
Shameless Plug
If you are new to the world of containers, I have created a FOSS called Dashtainer to help you quickly generate and run containers tailored to your app’s requirements.
If you give it a go, I would love to hear from you!
Wrapping it up
Docker has brought containers to the mainstream, but little gotchas like port dancing can be frustrating to new users. Hopefully with this small tutorial you are able to get up and running and get back to developing your Make the World a Better Place app.
Until next time, this is Señor PHP Developer Juan Treminio wishing you adios!
-
I specify Chrome because as of this writing I believe it is the only browser that will always resolve any *.localhost to the loopback interface. This means you do not need to touch the
/etc/hosts
file, the hostname will work automatically. If you do not want to use Chrome then you must install dnsmasq on MacOS or Acrylic on Windows. ↩
- Previous →
Developing at Full Speed with Xdebug - ← Next
Running Docker Containers as Current Host User