Docker-Compose - Traefik + More

I run a small VPS (Virtual Private Server) for several services that I use e.g. a Git server, web servers etc.. As I began to add more and more services, I felt the need to separate these into self-contained blocks. Doing this meant that I could develop the applications on whichever platform I like and not worry about dependency mismatches. I made the decision to move my services to Docker. Since moving, my workflow is far more streamlined as I no longer need to worry about said dependencies. In this post I will describe my setup, with emphasis on aspects that I found tricky such as the reverse proxy.

Currently, I use docker-compose to create all my containers, specifically version 2.2, so that I can specify resource limitations. Version 3 and above removed these options and are only available through the docker swarm API.


Here are the services that run on the VPS:

  • Traefik: As the reverse proxy
  • Gitea: As the Git server
  • Nextcloud: As the cloud data store
  • WordPress: As my website
  • Django: As my personal API

In this instance Traefik is a reverse proxy and is the only service that has its ports exposed to the outside network. When a request comes into the server on either of these ports, Traefik forwards the request to the relevant service on a virtual network.


You can download the docker-compose file: traefik-compose.

What is it?

Traefik is a reverse proxy that will allows us to run multiple services on the same host using the same ports. Essentially, it maps different domains to different containers. Traefik listens on port 80 and 443 of the host and all requests to these ports will be forwarded to the Traefik daemon. Depending on the destination of the request, it is forwarded onto the relevant container on the system.

SSL is used to secure our web requests and Traefik can handle all of the configuration and auto renewal of the Let’s Encrypt certificates. Once setup, that means that we will never had to worry about certificates expiring and our sites losing HTTPS.

The quick and dirty of how it works

When a request for a service comes in, it is initially encrypted with HTTPS. The reverse proxy then forwards this request, as an unencrypted HTTP request, to the target container. This means that all the SSL certificates are stored in the Traefik container only, aiding in management of certificates. Using unencrypted services behind our proxy comes the benefits of not having to configure every container for HTTPS.

Traefik autoconfig with Docker

Docker provides an API for creating/managing containers and can send events to a UNIX socket when they occur. This socket is located at /var/run/docker.sock. By allowing the Traefik container access to this socket we can let it take care of all configuration of containers that are started. We can control the configurations that Traefik applies we can use labels inside of our docker compose file.

Setting up with docker-compose

The options in the docker-compose file linked above should be self-explanatory but I shall elaborate on some of the labels here. If you want to read up on all the configuration options you can find them over on Traefiks website: here

traefik.frontend.ruleWhich domain to map to the controller
traefik.frontend.auth.basicSecures the dashboard with a username and password (admin, admin)
traefik.enableWhether or not to add the container to the Traefik dashboard
traefik.portThe port to run the dashboard on

Before you can run the service you will need the two files that were specified under the volumes section. These are Traefiks configuration file and the ACME file that will store SSL certificates. The acme.json file needs to be an empty file with its permissions set to 600 (otherwise Traefik will throw an error). Here is my config file: traefik


I use Gitea as a personal Git server. This allows me an unlimited number of private repositories; something GitHub doesn’t provide unless your a PRO user. This example is a simple one as there are no dependencies required to run the container. It is completely self-contained. Here is the compose file that I use:

  image: gitea/gitea:latest
  restart: unless-stopped
  container_name: gitea
    - ./data/gitea:/data:rw
    - "3000"
    - "22"
    - traefik
    - "traefik.backend=gitea"
    - ""
    - ""
    - "traefik.enable=true"
    - "traefik.port=3000"

Most of this configuration should be fairly self explanatory; if you have you looked at the Traefik compose file from earlier. Couple of things to note is that Gitea runs on port 3000 so that port needs to be exposed (along with 22 for SSH connections). By telling Traefik that Gitea runs on this port using the label traefik.port, we don’t need to do any port mapping as Traefik will auto configure everything for us.


The WordPress service is a slightly more involved setup as it requires a MySQL server. When creating the compose file for this service we will rely heavily on the use on environment variables to configure the containers.

First off the MySQL container:

  image: mysql:8.0.3
  restart: unless-stopped
  container_name: mysql
    MYSQL_ROOT_PASSWORD: 'wordpress'
    MYSQL_DATABASE: 'wordpress'
    MYSQL_USER: 'wordpress'
    MYSQL_PASSWORD: 'wordpress'
    - ./data/wordpress-mysql/database:/var/lib/mysql
    - wordpress-core
    - "traefik.enable=false"

We attach the MySQL container to a separate network to the rest of the system. This means that the only containers that can access the MySQL server are ones connected to this network (wordpress-core). The environment variables here should be pretty self-explanatory given their names; they define how we will be connecting to the MySQL server. We will also be using persistent storage for our database so that if ever we need to recreate the container, all of our data will still be intact. Lastly, we disable this container in the Traefik dashboard as we don’t need information on it because it isn’t front facing.

Next we configure the container that will actually run the wordpress site:

    - mysql
  image: wordpress:5.0.3-php7.1-apache
  restart: unless-stopped
  container_name: wordpress
    WORDPRESS_DB_HOST: mysql:3306
    WORDPRESS_DB_NAME: 'wordpress'
    WORDPRESS_DB_USER: 'wordpress'
    WORDPRESS_DB_PASSWORD: 'wordpress'
    - ./data/wordpress/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
    - ./data/wordpress/wp-content:/var/www/html/wp-content
    - traefik
    - wordpress-core
    - "traefik.backend=wordpress"
    - ""
    - ","
    - "traefik.enable=true"
    - "traefik.port=80"

Most of these options should already look familiar. However, the new one here is depends_on. This means that the wordpress container will not run if the MySQL container is not running. Something to note in the environment section is that for the database host, we reference the MySQL container using its container name and the port at which the server runs on. We need to do this because the IP address of container is not static and can change between restarts. In the volume section we map a configuration file called uploads.ini. This config file contains information about uploading to the wordpress container, specifically increasing the size of the uploads for media.

file_uploads = On
memory_limit = 256M
post_max_size = 64M
upload_max_filesize = 64M
max_execution_time = 600

Hopefully at this point all the services should start up correctly when you run

docker-compose up -d

Assuming everything did start, you should be able to access the following URLs (after adding them to your hosts file):