Tutorial: Implement Canary releases with Traefik in Docker Compose using headers and weighted routing for gradual PHP deployments

Deploy & DevOps · By · Published on · 7 min read

Paper map with pins and threads connecting points, showing connections between locations
Represents networks and traffic routing between services Photo: TheAndrasBarta / Pixabay

Tutorial showing how to implement Canary releases with Traefik in Docker Compose using headers and weighted routing for gradual PHP deployments. You’ll learn how to route by header/cookie to force a canary version, balance the rest by weights, and safely adjust the rollout — all without Kubernetes.

What we'll build and when to use Canary

You’ll run two versions of the PHP app (stable and canary) behind Traefik. Requests with a header or cookie will go 100% to the canary; the rest will follow a weighted balance. This lets you validate changes in production with low risk.

Use Canary when you need to test real changes with a subset of traffic. For instant swaps between identical infrastructure versions, see Blue‑green deploy.

If you’re still choosing a reverse proxy for Docker, compare options in Traefik vs Nginx.

  • Route traffic to canary by header or cookie
  • Balance the remaining traffic with weights (e.g., 95/5)
  • A single Host route for consumers

Prerequisites

You need a Linux host with Docker and Docker Compose installed, root access to edit /etc/hosts, and port 80 open. Familiarity with Docker networks and Traefik labels helps.

We won’t use TLS here to keep the example simple. In production, add HTTPS and hardening. Compare options in Traefik vs Caddy.

  • Docker and Docker Compose installed
  • Port 80 available on the host
  • Text editor and terminal
  • sudo access to edit /etc/hosts

Create the example PHP app (stable and canary)

We’ll build a simple PHP app served by PHP’s built-in server for demonstration. Do not use the built-in server in production. The code exposes / and /health and distinguishes versions with an environment variable. The command below adds a line to /etc/hosts.

mkdir -p app

cat > app/index.php <<'PHP'
<?php
header('Content-Type: application/json');
echo json_encode([
  'version' => getenv('VERSION') ?: 'unknown',
  'hostname' => gethostname(),
  'time' => date('c'),
  'path' => $_SERVER['REQUEST_URI'] ?? '/'
]);
PHP

cat > app/health.php <<'PHP'
<?php
http_response_code(200);
echo "ok";
PHP

# Mapear domínio local para testes (requer sudo)
echo '127.0.0.1 app.local' | sudo tee -a /etc/hosts

Define docker-compose.yml with Traefik, headers and weights

We’ll create Traefik as the reverse proxy, two app services (stable and canary), and the rules: a high-priority router for header/cookie and a default router using weighted routing.

This setup implements the Canary release flow with Traefik in Docker Compose using headers and weighted routing for gradual PHP app rollouts.

version: '3.9'

services:
  traefik:
    image: traefik:v2.10
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --accesslog=true
      - --log.level=INFO
    ports:
      - '80:80'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - web
    labels:
      - traefik.enable=true

      # Router canary por header/cookie (prioridade alta)
      - traefik.http.routers.app-canary.rule=Host(`app.local`) && (Headers(`X-Canary`,`1`) || Cookie(`canary`,`1`))
      - traefik.http.routers.app-canary.entrypoints=web
      - traefik.http.routers.app-canary.priority=1000
      - traefik.http.routers.app-canary.service=app-canary@docker

      # Router padrão (Host) usando weighted routing
      - traefik.http.routers.app.rule=Host(`app.local`)
      - traefik.http.routers.app.entrypoints=web
      - traefik.http.routers.app.service=app-weighted

      # Services ponderados: comece 100% stable, 0% canary
      - traefik.http.services.app-weighted.weighted.services.stable.name=app-stable@docker
      - traefik.http.services.app-weighted.weighted.services.stable.weight=100
      - traefik.http.services.app-weighted.weighted.services.canary.name=app-canary@docker
      - traefik.http.services.app-weighted.weighted.services.canary.weight=0

  app-stable:
    image: php:8.2-cli-alpine
    working_dir: /app
    command: sh -c 'php -S 0.0.0.0:8000 -t /app'
    environment:
      - VERSION=stable
    volumes:
      - ./app:/app:ro
    networks:
      - web
    labels:
      - traefik.enable=true
      - traefik.http.services.app-stable.loadbalancer.server.port=8000
      - traefik.http.services.app-stable.loadbalancer.healthcheck.path=/health
      - traefik.http.services.app-stable.loadbalancer.healthcheck.interval=5s

  app-canary:
    image: php:8.2-cli-alpine
    working_dir: /app
    command: sh -c 'php -S 0.0.0.0:8000 -t /app'
    environment:
      - VERSION=canary
    volumes:
      - ./app:/app:ro
    networks:
      - web
    labels:
      - traefik.enable=true
      - traefik.http.services.app-canary.loadbalancer.server.port=8000
      - traefik.http.services.app-canary.loadbalancer.healthcheck.path=/health
      - traefik.http.services.app-canary.loadbalancer.healthcheck.interval=5s

networks:
  web: {}

Bring the environment up and validate header/cookie routing

Start the containers and run test requests. First validate the default Host route. Then force traffic to canary with a header and with a cookie. If you use a local firewall, make sure port 80 is allowed.

docker compose up -d

# Rota padrão (deve ir para stable)
curl -s http://app.local/

# Forçar canary pelo header X-Canary=1
curl -s -H 'X-Canary: 1' http://app.local/

# Forçar canary por cookie canary=1
curl -s --cookie 'canary=1' http://app.local/

# Conferir health dos serviços
curl -s -I http://app.local/health

Gradual rollout: adjust weights and apply

Edit docker-compose.yml and change the weights. A common sequence is: 99/1, 95/5, 90/10, 75/25 and 50/50, monitoring metrics and errors between steps. Always ensure database and schema compatibility.

  • Increase the canary weight gradually
  • Monitor errors, latency and saturation
  • Reduce to zero in case of failures (rollback)
# Exemplo: mudar para 90% stable, 10% canary
# No serviço traefik, ajuste as labels:
# - traefik.http.services.app-weighted.weighted.services.stable.weight=90
# - traefik.http.services.app-weighted.weighted.services.canary.weight=10

# Aplique sem recriar os apps
docker compose up -d --no-deps traefik

# Gere amostras e observe distribuição
for i in $(seq 1 20); do curl -s http://app.local/ | grep -o '"version":"[a-z]*"'; done | sort | uniq -c

Sessions, healthchecks and observability

User sessions: weighted balancing can switch users between stable and canary. If your app requires affinity, use cookie routing for specific users or store sessions externally (e.g., Redis).

HTTP healthchecks are configured on /health. Only healthy containers receive traffic. To follow activity, check Traefik access logs and your app metrics.

Centralize structured logs to speed up debugging. See Structured JSON logs & request correlation and for dashboards check Prometheus & Grafana with Docker Compose.

  • Cookie affinity for pilot user groups
  • Healthchecks keep unhealthy instances out
  • Logs and metrics for fast feedback
# Acompanhar acessos direcionados pelo Traefik
docker logs -f traefik | grep app.local

Final checks, next steps and common issues

Confirm that: 1) requests with the header/cookie always hit the canary; 2) the default route respects the weights; 3) rollback sets the canary weight to zero. Automate weight changes in your CI pipeline, validating metrics between steps.

For risky features, combine Canary with feature flags in the code. See ideas in feature flags in PHP monoliths.

Host not resolving? Verify app.local is in /etc/hosts. Port 80 in use? Stop the service taking it. Weights not taking effect? Ensure the labels are on the Traefik service and run docker compose up again. Header/cookie ignored? Check the canary router priority.

  • /etc/hosts and ports correct
  • Labels on the Traefik service, not on the apps
  • High priority on the canary router (priority)
  • Quick rollback if 5xx errors appear

Frequently asked questions

Can I do Canary without Kubernetes?

Yes. Traefik with Docker Compose supports header/cookie routing and weighted routing. It’s sufficient for many scenarios: you control traffic between stable and canary using labels and Docker networking, without Kubernetes complexity.

How do I route by cookie or header in Traefik?

Create a router with high priority and a rule combining Host with Headers or Cookie. Point that router to the canary service. The rest of the traffic hits the default router, which uses weighted routing to distribute between stable and canary.

What about user sessions during a canary?

Because traffic can switch between versions, in-memory sessions may be lost. Use a cookie to force users to the canary during tests or store sessions in a shared backend like Redis so users can switch without losing state.

How do I automate the gradual rollout?

In your CI pipeline, update the Traefik weight values in the labels and redeploy only the Traefik service. After each increment, validate metrics and errors. If something goes wrong, set the canary weight back to zero.

How to handle database changes?

Keep backward compatibility between versions. First make compatible (expand) schema changes, then deploy code changes, and only later remove old fields (contract). For risky behavioral changes, combine with feature flags in the app.

Want to adapt Canary to your environment?

If your scenario involves multiple services, TLS, sessions and automated validations, we can design a canary rollout with observability and safe rollback.

Discuss Canary with Traefik

See also