Skip to main content

Traefik with multiple Docker networks

This blog post might be outdated!
This blog post was published more than one year ago and might be outdated!
· 4 min read
Stephan Hochdörfer
Head of IT Business Operations

Sometimes, mostly for smaller customers, we deploy staging and production environments on the same machine. Not an ideal setup, but doable. Thanks to Docker the different instances can live in isolation, except of course when one instance takes the server down, the other instance is also affected.

Deploying an application twice with the same docker-compose.yaml file does not work, because the service names need to be unique. And how would e.g. nginx know to with php-fpm instance to talk to? Thankfully, Docker has us covered.

Docker networks to the rescue! We can create separate networks in Docker which will isolate the different applications and their services from each other. Following along I will cover all the steps needed to configure Docker, Traefik, and an example application to make use of Docker networks.

First, let's define some global services in a global docker-compose.yaml file which is hosted somewhere on the server. Global services like Traefik or a MariaDB database should be deployed and upgraded independently of the application itself.

The docker-compose.yaml file for the global services looks like this:

version: "3.7"

services:
traefik:
image: traefik:2.9
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/opt/traefik/traefik.toml:/traefik.toml
- /etc/opt/traefik/acme.json:/acme.json
networks:
- staging_default
- production_default
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`myhost.loc`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls.certresolver=cert_resolver"

database:
image: mariadb:10.4
restart: always
volumes:
- /var/lib/mysql:/var/lib/mysql
networks:
- staging_default
- production_default

networks:
staging_default:
production_default:

In the docker-compose.yaml file we have 2 services defined: traefik and database - as well as 2 networks staging_default and production_default. The traefik and database service are part of both networks.

Since Docker Compose uses the current directory name as a prefix for service names or network names, we need to use a common prefix when launching the global services as well as when launching the applications. Otherwise, the services end up in different networks and can't "see" each other. Instead of using the current directory name, we can define the environment variable COMPOSE_PROJECT_NAME with some custom name:

export COMPOSE_PROJECT_NAME="customer"

Now, when running docker compose up Docker Compose will use customer as a prefix and e.g. generate the networks as "customer_staging_default" and "customer_production_default".

What does the docker-compose.deploy.yaml file from our application look like?

version: '3.7'

services:
web:
image: nexus3.loc/customer/sulu/nginx:${TAG:-latest}
build:
context: .
dockerfile: docker/nginx/Dockerfile
environment:
- PHPFPM_HOST=phpfpm
- PHPFPM_PORT=9000
- NGINX_PORT=80
links:
- phpfpm
volumes: &appvolumes
- "/var/www/${TRAEFIK_ENV}/public/upload/:/var/www/public/upload"
- "/var/www/${TRAEFIK_ENV}/var/log/:/var/www/var/log/"
- "/var/www/${TRAEFIK_ENV}/var/uploads/:/var/www/var/uploads/"
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.sulu_${TRAEFIK_ENV}_https.redirectscheme.scheme=https"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}.entrypoints=web"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}.middlewares=sulu_${TRAEFIK_ENV}_https@docker"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}_https.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}_https.tls.certresolver=cert_resolver"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}_https.tls=true"
- "traefik.http.routers.sulu_${TRAEFIK_ENV}_https.entrypoints=websecure"

phpfpm:
image: nexus3.loc/customer/sulu/phpfpm:${TAG:-latest}
build:
context: .
dockerfile: docker/phpfpm/Dockerfile
target: php-prod
environment:
- "APP_ENV=${APP_ENV}"
- "APP_SECRET=${APP_SECRET}"
- "DATABASE_URL=${APP_DATABASE_URL}"
volumes: *appvolumes

As you can see, we are using quite a few environment variables which are defined in our GitLab project as CI/CD variables. Those variables depend on the staging or production environment. That means the variable names are identical for the different environments and GitLab CI will take care to pick the "right" value.

The most important variables are TRAEFIK_ENV and TRAEFIK_HOST. TRAEFIK_ENV contains either "staging" or "production" as a value, depending on the active environment. TRAEFIK_HOST is the hostname that Traefik will use to route the requests to that container.

In our Gitlab CI pipeline configuration, we use the following logic for deploying the application:

deploy:stage:
stage: deploy
only:
- tags
environment:
name: staging
url: https://staging.loc
script:
- export TAG="${CI_COMMIT_TAG}"
- export COMPOSE_PROJECT_NAME="customer_staging"
- docker compose -f docker-compose.deploy.yml pull
- docker compose -f docker-compose.deploy.yml stop || true
- docker compose -f docker-compose.deploy.yml up -d
allow_failure: false

Ihe COMPOSE_PROJECT_NAME environment variable is configured slightly differently here. We have to set it customer_staging to make it work as the default network name in Docker Compose is default.

The deployment configuration for production looks identical, except for the environment configuration and the COMPOSE_PROJECT_NAME. Both obviously point to the production setup.