Implement Traefik Into API Platform Dockerized

An open-source reverse proxy and load balancer for HTTP and TCP-based applications that is easy, dynamic, automatic, fast, full-featured, production proven, provides metrics and integrates with every major cluster technology.

https://traefik.io

Basic Implementation

This tutorial will help you to define your own routes for your client, api and more generally for your containers.

Use this custom API Platform docker-compose.yml file which implements ready-to-use Traefik container configuration. Override ports and add labels to tell Traefik to listen on the routes mentioned and redirect routes to a specified container.

A few points to note:

  • --api Tells Traefik to generate a browser view to watch containers and IP/DNS associated easier

  • --docker Tells Traefik to listen on Docker Api

  • labels: Key for Traefik configuration into Docker integration

    services:
    #  ...
    api:
      labels: 
        - traefik.frontend.rule=Host:api.localhost

    The API DNS will be specified with traefik.frontend.rule=Host:your.host (here api.localhost)

  • --traefik.port=3000 The port specified to Traefik will be exposed by the container (here the React app exposes the 3000 port)

# docker-compose.yml
version: '3.4'

x-cache:
  &cache
  cache_from:
    - ${CONTAINER_REGISTRY_BASE}/php
    - ${CONTAINER_REGISTRY_BASE}/nginx
    - ${CONTAINER_REGISTRY_BASE}/varnish

services:
  traefik:
    image: traefik
    command: --api --docker
    ports:
      - "80:80" #All HTTP access will be caught by Traefik
      - "443:443" #All HTTPS access will be caught by Traefik
      - "8080:8080" #Access Traefik webview
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  php:
    image: ${CONTAINER_REGISTRY_BASE}/php
    build:
      context: ./api
      target: api_platform_php
      <<: *cache
    depends_on:
      - db
    # Comment out these volumes in production
    volumes:
      - ./api:/srv/api:rw,cached
      # If you develop on Linux, uncomment the following line to use a bind-mounted host directory instead
      # - ./api/var:/srv/api/var:rw

  api:
    image: ${CONTAINER_REGISTRY_BASE}/nginx
    labels:
      - traefik.frontend.rule=Host:api.localhost
    build:
      context: ./api
      target: api_platform_nginx
      <<: *cache
    depends_on:
      - php
    # Comment out this volume in production
    volumes:
      - ./api/public:/srv/api/public:ro

  cache-proxy:
    image: ${CONTAINER_REGISTRY_BASE}/varnish
    build:
      context: ./api
      target: api_platform_varnish
      <<: *cache
    depends_on:
      - api
    volumes:
      - ./api/docker/varnish/conf:/usr/local/etc/varnish:ro
    tmpfs:
      - /usr/local/var/varnish:exec
    labels:
      - traefik.frontend.rule=Host:cache.localhost

  db:
    # In production, you may want to use a managed database service
    image: postgres:10-alpine
    labels:
      - traefik.frontend.rule=Host:db.localhost
    environment:
      - POSTGRES_DB=api
      - POSTGRES_USER=api-platform
      # You should definitely change the password in production
      - POSTGRES_PASSWORD=!ChangeMe!
    volumes:
      - db-data:/var/lib/postgresql/data:rw
      # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
      # - ./docker/db/data:/var/lib/postgresql/data:rw
    ports:
      - "5432:5432"

  mercure:
    # In production, you may want to use the managed version of Mercure, https://mercure.rocks
    image: dunglas/mercure
    environment:
      # You should definitely change all these values in production
      - JWT_KEY=!UnsecureChangeMe!
      - ALLOW_ANONYMOUS=1
      - CORS_ALLOWED_ORIGINS=*
      - PUBLISH_ALLOWED_ORIGINS=http://mercure.localhost
      - DEMO=1
    labels:
      - traefik.frontend.rule=Host:localhost

  client:
    # Use a static website hosting service in production
    # See https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.mddeployment
    image: ${CONTAINER_REGISTRY_BASE}/client
    build:
      context: ./client
      cache_from:
        - ${CONTAINER_REGISTRY_BASE}/client
    env_file:
      - ./client/.env
    volumes:
      - ./client:/usr/src/client:rw,cached
      - /usr/src/client/node_modules
    expose:
      - 3000
    labels:
      - traefik.port=3000
      - traefik.frontend.rule=Host:localhost

  admin:
    # Use a static website hosting service in production
    # See https://facebook.github.io/create-react-app/docs/deployment
    image: ${CONTAINER_REGISTRY_BASE}/admin
    build:
      context: ./admin
      cache_from:
        - ${CONTAINER_REGISTRY_BASE}/admin
    volumes:
      - ./admin:/usr/src/admin:rw,cached
      - /usr/src/admin/node_modules
    expose:
      - 3000
    labels:
      - traefik.port=3000
      - traefik.frontend.rule=Host:admin.localhost

volumes:
  db-data: {}

Don't forget the db-data, or the database won't work in this dockerized solution.

localhost is a reserved domain referred to in your /etc/hosts. If you want to implement custom DNS such as production DNS in local, just add them at the end of your /etc/host file like that:

# /etc/hosts
# ...

127.0.0.1       your.domain.com

If you do that, you'll have to update the CORS_ALLOW_ORIGIN environment variable api/.env to accept the specified URL.

Known Issues

If your network is of type B, it may conflict with the Traefik sub-network.

Going Further

As this Traefik configuration listens on 80 and 443 ports, you can run only 1 Traefik instance per server. However, you may want to run multiple API Platform projects on the same server. To deal with it, you'll have to externalize the Traefik configuration to another docker-compose.yml file, anywhere on your server.

Here is a working example:

# /somewhere/docker-compose.yml
version: '3.4'

services:
  traefik:
    image: traefik
    command: --api --docker
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
# load a TOML configuration file and to generate Let's Encrypt certificated as explained in https://docs.traefik.io/user-guide/docker-and-lets-encrypt/
#      - ./traefik.toml:/traefik.toml
#      - ./acme.json:/acme.json
    networks:
      - api_platform_network
    # Add other networks here

networks:
  api_platform_network:
    external: true
  # Add other networks here

Then update the docker-compose.yml file belonging to your API Platform projects:

# docker-compose.yml
version: '3.4'

x-cache:
  &cache
  cache_from:
    - ${CONTAINER_REGISTRY_BASE}/php
    - ${CONTAINER_REGISTRY_BASE}/nginx
    - ${CONTAINER_REGISTRY_BASE}/varnish

+x-network:
+  &network
+  networks:
+    - api_platform_network

services:
# Uncomment these lines only if you want to run one api-platform instance using traefik
-  traefik:
-    image: traefik:latest
-    command: --api --docker
-    ports:
-      - "80:80"
-      - "443:443"
-    volumes:
-      - /var/run/docker.sock:/var/run/docker.sock
-    <<: *network

  php:
    image: ${CONTAINER_REGISTRY_BASE}/php
    build:
      context: ./api
      target: api_platform_php
      <<: *cache
    depends_on: 
      - db
+    environment:
+      # You should remove these variables from .env into api folder
+      - TRUSTED_HOSTS=^(((${SUBDOMAINS_LIST}\.)?${DOMAIN_NAME})|api)$$
+      - CORS_ALLOW_ORIGIN=^${HTTP_OR_SSL}(${SUBDOMAINS_LIST}.)?${DOMAIN_NAME}$$
+      - DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/${DB_NAME}
+      - MERCURE_SUBSCRIBE_URL=${HTTP_OR_SSL}mercure.${DOMAIN_NAME}$$
+      - MERCURE_PUBLISH_URL=${HTTP_OR_SSL}mercure.${DOMAIN_NAME}$$
+      - MERCURE_JWT_SECRET=${JWT_KEY}
    volumes:
      - ./api:/srv/api:rw,cached
+    <<: *network

  api:
    image: ${CONTAINER_REGISTRY_BASE}/nginx
    build:
      context: ./api
      target: api_platform_nginx
      <<: *cache
    depends_on:
      - php
    volumes:
      - ./api/public:/srv/api/public:ro
    labels:
      - traefik.frontend.rule=Host:api.${DOMAIN_NAME}
+    <<: *network

  cache-proxy:
    image: ${CONTAINER_REGISTRY_BASE}/varnish
    build:
      context: ./api
      target: api_platform_varnish
      <<: *cache
    depends_on:
      - api
    volumes:
      - ./api/docker/varnish/conf:/usr/local/etc/varnish:ro
    tmpfs:
      - /usr/local/var/varnish:exec
    labels:
      - traefik.frontend.rule=Host:cache.${DOMAIN_NAME}
+    <<: *network

  db:
    image: postgres:10-alpine
    environment:
      - POSTGRES_DB=${DB_NAME}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASS}
    volumes:
      - db-data:/var/lib/postgresql/data:rw
+    <<: *network

  mercure:
    image: dunglas/mercure
    environment:
      - JWT_KEY=${JWT_KEY}
      - ALLOW_ANONYMOUS=0
      - CORS_ALLOWED_ORIGINS=^${HTTP_OR_SSL}(${SUBDOMAINS_LIST}.)?${DOMAIN_NAME}$$
      - PUBLISH_ALLOWED_ORIGINS=${HTTP_OR_SSL}
      - DEMO=1
    labels:
      - traefik.frontend.rule=Host:mercure.${DOMAIN_NAME}
+    <<: *network

  client:
    image: ${CONTAINER_REGISTRY_BASE}/client
    build:
      context: ./client
      cache_from:
        - ${CONTAINER_REGISTRY_BASE}/client
    volumes:
      - ./client:/usr/src/client:rw,cached
      - /usr/src/client/node_modules
    expose:
      - 3000
    labels:
      - traefik.frontend.rule=Host:${DOMAIN_NAME},www.${DOMAIN_NAME}
      - traefik.port=3000
    environment:
      # You should remove this variable from .env into client folder
      - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME}
+    <<: *network

  admin:
    image: ${CONTAINER_REGISTRY_BASE}/admin
    build:
      context: ./admin
      cache_from:
        - ${CONTAINER_REGISTRY_BASE}/admin
    environment:
      # You should remove this variable from .env into admin folder
      - REACT_APP_API_ENTRYPOINT=${HTTP_OR_SSL}api.${DOMAIN_NAME}
    volumes:
      - ./admin:/usr/src/admin:rw,cached
      - /usr/src/admin/node_modules
    expose:
      - 3000
    labels:
      - traefik.frontend.rule=Host:admin.${DOMAIN_NAME}
      - traefik.port=3000
+    <<: *network

volumes:
  db-data: {}

+networks:
+  api_platform_network:
+    external: true

Finally, some environment variables must be defined, here is an example of a .env file to set them:

CONTAINER_REGISTRY_BASE=quay.io/api-platform
DOMAIN_NAME=localhost
HTTP_OR_SSL=http://
DB_NAME=api-platform-db-name
DB_PASS=YouMustChangeThisPassword
DB_USER=api-platform
JWT_KEY=!UnsecureChangeMe!
SUBDOMAINS_LIST=(admin|api|cache|mercure|www)

This way, you can configure your main variables into one single file.