Docker

Since several years, I’m hosting a lot of things: blog, wiki, emails etc… I’ve played with Vserver, OpenVZ, KVM and finaly LXC. For years, I’ve learned how to use all of them but the most known solution during the last years is Docker.

I even can remeber the first Meetup I’ve attended in Paris talking about Docker 5 years ago. Now containers are eveverywhere. I recently changed the server I’m running for my own usage and this was the good timing to switch from LXC to Docker.

I’ve wrote several blog posts regarding Kubernetes and my first thoughts were about using Kubernetes. But it looks like really overkill on a single machine. I then tought about using Docker Compose and I think this is the most appropriate solution for this use case.

Install and configure Docker Compose

I’m running on a Debian server and have installed Docker from the official repository.

Then to make it simple, I’m using systemd to automatically run my containers when the server boots up /etc/systemd/system/docker-compose@.service :

[Unit]
Description=%i service with docker compose
Requires=docker.service
After=docker.service

[Service]
Restart=always
# The directory where your compose config files are stored
WorkingDirectory=/etc/docker/compose/%i

# Remove old containers, images and volumes
ExecStartPre=/usr/bin/docker-compose down -v
ExecStartPre=/usr/bin/docker-compose rm -fv
ExecStartPre=-/bin/bash -c 'docker volume ls -qf "name=%i_" | xargs docker volume rm'
ExecStartPre=-/bin/bash -c 'docker network ls -qf "name=%i_" | xargs docker network rm'
ExecStartPre=-/bin/bash -c 'docker ps -aqf "name=%i_*" | xargs docker rm'

# Compose up
ExecStart=/usr/bin/docker-compose up

# Compose down, remove containers and volumes
ExecStop=/usr/bin/docker-compose down -v

[Install]
WantedBy=multi-user.target

You can add a Docker compose configuration file (docker-compose.yml) and start it:

systemctl enable docker-compose@myservice
systemctl start docker-compose@myservice

This makes container configuration very simple and easily manageable.

To finish, let’s create a dedicated network for containers (here called frontend):

docker network create frontend

Using Traefik as reverse proxy

To make containers accessible from the outside, with Let’s Encrypt TLS and connected to Docker. For this I’ve setup Traefik with Docker compose.

I’m using this Docker compose configuration file /etc/docker/compose/traefik/docker-compose.yml:

version: '3'
services:
  traefik:
    image: traefik:1.7.6-alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
      # WebUI admin port
      - "8082:8080"
    # Need this network to make Traefik able to forward on all containers running on this one too
    networks:
      - frontend
    labels:
      traefik.enable: true
      traefik.domain: 'fqdn.com'
      traefik.tags: web,lb,traefik
      traefik.frontend.rule: 'Host:traefik.docker.local'
      traefik.docker.network: 'frontend'
    volumes:
      - ./conf/traefik:/etc/traefik
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      TZ: "Europe/Paris"
      # Cloudflare API credentials
      CF_API_EMAIL: my@email
      CF_API_KEY: mykey

networks:
  frontend:
    external: true

And the Traefik configuration /etc/docker/compose/traefik/conf/traefik/traefik.toml:

# Set default entrypoints
defaultEntryPoints = ["http", "https"]

checkNewVersion = true
MaxIdleConnsPerHost = 500
logLevel = "INFO"
sendAnonymousUsage = true

# Launch API
[api]

# Connect to Docker host to get the running docker instances
[docker]
  domain = "docker.local"
  watch = true
  exposedByDefault = false

# Additional static configuration file
[file]
  filename = "/etc/traefik/rules.toml"
  watch = true

# TLS Let's Encrypt configuration with Cloudflare support
[acme]
email = "my@email"
storage = "/etc/traefik/acme/acme.json"
entryPoint = "https"
OnHostRule = true
acmeLogging = true
  [acme.DNSChallenge]
  provider = "cloudflare"

# Request wildcard domain TLS
[[acme.domains]]
  main = "*.fqdn.com"

# Run on those ports
[entryPoints]
  [entryPoints.http]
  address = ":80"
  compress = true
  [entryPoints.https]
  address = ":443"
  compress = true
    [entryPoints.https.tls]

I’m now able to test it with:

cd /etc/docker/compose/traefik/
docker-compose up

Port 80 and 443 should be open of course and they should now answer. The admin WebUI port is not open from the outside so you need a VPN or an SSH tunnel to access it for example.

Once your configuration works as expected, you enable this docker compose as a service:

systemctl enable docker-compose@traefik
systemctl start docker-compose@traefik

Wordpress configuration

For Wordpress, the documentation recommended to mount only some folders (like plugins and uploads). However, I do not especially recommand it because you’ll need to restart the container with a newer version anytime you want to upgrade it. That’s why I’m mounting all the Wordpress folder. This is not the “Container way” theory with immutable images, but from a pragmatic point of view, I prefer having a Wordpress always up to date (with dedicated plugin managing the upgrade of all components) instead of having a process managing the upgrade of my containers like Watchtower. That’s my choice and point of view, I know that other persons would choose the other solution, so select the most appropriate one.

Here is my config:

version: '3'
services:
  # Required MariaDB instance with mounted volumes
  mysql:
    image: mariadb:10.3.11
    restart: always
    volumes:
      - ./conf/mariadb:/etc/mysql/mariadb.conf.d
      - /mnt/container/wordpress/mariadb:/var/lib/mysql
    networks:
      - frontend
    environment:
      MYSQL_ROOT_PASSWORD: password
  # Wordpress:expose it using labels with defined rule name
  wordpress:
    image: wordpress:5.0.0-php7.1
    restart: always
    labels:
      traefik.enable: true
      traefik.tags: web
      traefik.frontend.rule: 'Host:test.fqdn.com'
      traefik.frontend.entryPoints: https
    # Need this network to allow traefik redirect to it
    networks:
      - frontend
    ports:
      - "8080:80"
    volumes:
      - ./conf/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
      - /mnt/container/wordpress/wordpress:/var/www/html
    environment:
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_NAME: wordpress
    links:
      - mysql
    depends_on:
      - mysql
networks:
  frontend:
    external: true

Note: if you’re using it this way like me, the very first time, you’ll need to copy a wordpress content from the container to your volume. Otherwise, your Wordpress volume will be empty.

Same as Traefik, I’m now able to test it with:

cd /etc/docker/compose/wordpress/
docker-compose up

It should be now accessible from https://test.fqdn.com. You can double check on the Traefik WebUI if your service is correctly exposed to the outside.

Once your configuration works as expected, you enable this docker compose as a service:

systemctl enable docker-compose@wordpress
systemctl start docker-compose@wordpress

I’ve finished with it. It’s a simple and smart way to manage containers on a single server, it’s repetable. Do not forget to backup your data, configuration files and you’re good to go.

Hope this helped