Chaîne complète : CI → GHCR privé → Watchtower → Prod
Mettre en place un flux de déploiement continu fiable avec GitHub Actions, GHCR privé et Watchtower.
·
Série
Watchtower + GHCR en production
#3
Written by AI
1. Build & Push d’image avec GitHub Actions
Workflow minimal .github/workflows/build.yml :
name: Build and Push (GHCR)
on:
push:
branches: [ "main" ]
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ghcr.io/<user>/<repo>:main
- L’action pousse
mainà chaque commit sur main. - Ton image reste privée tant que tu ne changes pas sa visibilité.
2. Infra Watchtower (Compose)
infra/compose.yaml :
services:
watchtower:
image: containrrr/watchtower
command: --interval 300 --cleanup --label-enable
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/ubuntu/.docker/config.json:/config.json:ro
restart: unless-stopped
3. Service applicatif (ex. blog Symfony)
blog/compose.prod.yaml :
services:
blog:
image: ghcr.io/<user>/<repo>:main
restart: unless-stopped
networks: [edge]
expose: ["8080"] # aligne avec ton app (ex. FrankenPHP :8080)
env_file:
- .env.prod # secrets côté serveur, hors Git
labels:
com.centurylinklabs.watchtower.enable: "true"
caddy: blog.code-ludus.com
caddy.encode: zstd gzip
caddy.reverse_proxy: "{{upstreams 8080}}"
networks:
edge:
external: true
Contenu typique de .env.prod (sur le serveur, non commité)
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=change-me-please-32+chars
TRUSTED_PROXIES=172.16.0.0/12
TRUSTED_HOSTS=^blog\.code-ludus\.com$
Conseil Symfony : garde un .env non sensible dans l’image (même vide) pour éviter l’exception Unable to read the "/app/.env", et surcharge tout en prod via .env.prod.
4. Reverse proxy (Caddy Docker Proxy)
infra/compose.yaml (extrait) :
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:2.8-alpine
ports: ["80:80","443:443"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- caddy_data:/data
- caddy_config:/config
environment:
CADDY_INGRESS_NETWORKS: edge
ACME_EMAIL: <email@exemple.com>
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
Dès que ton service blog porte des labels Caddy et rejoint le réseau edge, Caddy publie automatiquement https://blog.code-ludus.com (certificat Let’s Encrypt inclus).
5. Déroulé “de bout en bout”
git push main- GitHub Actions build → push
ghcr.io/<user>/<repo>:main - Watchtower (toutes les 5 min) détecte un digest nouveau
- Watchtower pull → stop/remove/create du conteneur blog
- Caddy reverse proxy vers le nouveau conteneur
6. Dépannage rapide
- GHCR privé : monte /config.json dans Watchtower, pas ~/.docker/config.json.
- Port 502 via Caddy : vérifie que le label caddy.reverse_proxy vise le bon port ({{upstreams 8080}} vs 8000).
- Erreur Symfony .env : inclure un .env non sensible dans l’image, secrets via .env.prod.
- Labels Caddy invalides : éviter handle_path mal formé ; préférer un respond avec matcher :
labels:
caddy.@health.path: /healthz
caddy.respond.@health: '"ok" 200'
- Watchtower ne voit pas le service : activer --label-enable + label com.centurylinklabs.watchtower.enable: "true" sur le service cible.