Skip to content

Conteneurs et réseaux ​

Tous les services tournent dans des conteneurs Docker, décrits par des fichiers Docker Compose. Cette page montre ce qui tourne sur chaque machine, comment les conteneurs se parlent, et comment leurs images sont construites.

Sur tm-prod ​

ConteneurImageJoignable par
timemanager-traefiktraefik:v3.3internet (80, 443)
timemanager-frontghcr.io/<owner>/timemanager-web:latestTraefik seulement
timemanager-docsghcr.io/<owner>/timemanager-docs:latestTraefik seulement
timemanager-backghcr.io/<owner>/timemanager-api:latestTraefik seulement
timemanager-dbpostgres:17-alpinel'API seulement
  • La base est branchée uniquement sur le réseau internal, déclaré avec internal: true : elle n'a aucun accès à internet et Traefik ne peut pas la joindre.
  • L'API est branchée sur les deux réseaux : Traefik la joint d'un côté, elle joint la base de l'autre.
  • L'API ne démarre qu'une fois PostgreSQL prêt, grâce à un healthcheck sur la base et à depends_on: condition: service_healthy.
  • Tous les conteneurs ont restart: unless-stopped : ils repartent tout seuls après un redémarrage de la VM.

Les données ​

Les données PostgreSQL sont dans le volume nommé app_pgdata, sur le disque de tm-prod, dans /var/lib/docker/volumes/app_pgdata/_data.

Il survit à la recréation des conteneurs, aux redéploiements et aux redémarrages de la VM. Il ne protège pas contre la perte du disque ou de la VM : voir Limites et améliorations.

Sur tm-tooling ​

ConteneurImageJoignable par
jenkinsimage maison basée sur jenkins/jenkins:ltsl'IP interne de tm-tooling, port 8080

L'image ajoute le client Docker et les plugins de jenkins/Plugins/plugins.txt. Jenkins construit les images à travers le socket Docker de la machine. Voir Jenkins.

Les images du projet ​

ImageConstruite depuisParContient
ghcr.io/saliehsab/timemanager-webtimemanager-web/DockerfileJenkinsLe site compilé, servi par nginx
ghcr.io/saliehsab/timemanager-apitimemanager-api/DockerfileJenkinsLa release Elixir de l'API
postgres:17-alpineDocker HubLa base de données
traefik:v3.3Docker HubLe reverse proxy
Image Jenkins localetimemanager-devops/jenkins/DockerfileAnsible, sur tm-toolingJenkins + Docker + plugins

Chaque image du projet est publiée avec deux étiquettes : le hash court du commit (a1b2c3d) et latest.

L'image du frontend ​

timemanager-web/Dockerfile construit l'image en trois étapes (« multi-stage build ») :

dockerfile
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:22-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG VITE_API_URL
RUN test -n "$VITE_API_URL" || (echo "VITE_API_URL build argument is required" && exit 1)
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build

FROM nginxinc/nginx-unprivileged:stable-alpine AS runner
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 8080
ÉtapeRôleGardée dans l'image finale ?
depsInstaller les dépendances. Docker la garde en cache tant que package.json ne change pas.Non
builderCompiler le site avec Vite. Échoue si VITE_API_URL n'est pas fourni.Non
runnerServir les fichiers compilés avec nginxOui

L'image finale ne contient ni Node, ni le code source, ni les dépendances : seulement nginx et les fichiers HTML, CSS et JS. Elle est petite et a peu de failles possibles.

nginx-unprivileged tourne sans être root, d'où le port 8080 (les ports sous 1024 demandent les droits root).

L'adresse de l'API est figée au build

Vite remplace import.meta.env.VITE_API_URL par sa valeur pendant la compilation. Changer l'adresse de l'API demande donc de reconstruire l'image. C'est pour ça que Jenkins passe --build-arg VITE_API_URL=....

La configuration nginx ​

nginx
server {
    listen 8080;
    root /usr/share/nginx/html;
    index index.html;
    server_tokens off;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
  • try_files ... /index.html : le site utilise vue-router en mode « history ». Une adresse comme /clock/12 n'est pas un vrai fichier : nginx renvoie index.html et c'est Vue qui affiche la bonne page.
  • /assets/ : les fichiers compilés ont un hash dans leur nom (index-a1b2c3.js). Leur contenu ne change jamais, donc le navigateur peut les garder un an.
  • server_tokens off : nginx ne dit pas sa version.

L'image du backend ​

timemanager-api/Dockerfile a été généré avec mix phx.gen.release --docker, puis adapté. Il utilise deux étapes :

ÉtapeImage de baseRôle
builderhexpm/elixir:1.17.3-erlang-26.2.5.16-debian-trixie-...Télécharger les dépendances, compiler, produire une release (mix release)
finaldebian:trixie-...-slimSeulement la release et les bibliothèques système nécessaires

Une release Elixir est un dossier autonome qui contient le code compilé et la machine virtuelle Erlang. L'image finale n'a pas besoin d'Elixir.

L'étape finale lance apt-get upgrade pour appliquer les correctifs de sécurité, et l'image tourne avec l'utilisateur nobody, sans droits.

Le démarrage : entrypoint.sh ​

sh
#!/bin/sh
set -e

/app/bin/migrate
exec /app/bin/server

À chaque démarrage du conteneur, les migrations de base de données sont appliquées, puis le serveur démarre. Un déploiement qui ajoute une table n'a donc rien de spécial à faire. set -e arrête tout si la migration échoue : le serveur ne démarre pas sur une base incohérente.

L'étape builder sert aussi aux tests ​

Le pipeline backend construit seulement l'étape builder (--target builder) pour lancer mix test dedans. Voir Pipelines.

Le fichier de production : app/docker-compose.yml ​

yaml
services:
  front:
    image: ghcr.io/${GHCR_OWNER}/timemanager-web:latest
    container_name: timemanager-front
    restart: unless-stopped
    networks: [timemanager]

  back:
    image: ghcr.io/${GHCR_OWNER}/timemanager-api:latest
    container_name: timemanager-back
    restart: unless-stopped
    environment:
      DATABASE_URL: ecto://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB}
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
      PHX_HOST: ${PHX_HOST}
      CORS_ORIGINS: ${CORS_ORIGINS}
      BREVO_API_KEY: ${BREVO_API_KEY}
      MAIL_FROM: ${MAIL_FROM}
    depends_on:
      db:
        condition: service_healthy
    networks: [timemanager, internal]

  db:
    image: postgres:17-alpine
    container_name: timemanager-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks: [internal]

volumes:
  pgdata:

networks:
  timemanager:
    external: true
  internal:
    internal: true
ÉlémentExplication
${GHCR_OWNER}, ${POSTGRES_...}Lues dans le fichier .env à côté, écrit par Ansible
container_nameUn nom fixe, utilisé par Traefik pour joindre le conteneur
restart: unless-stoppedRedémarre tout seul après un plantage ou un redémarrage de la machine
depends_on + service_healthyL'API attend que PostgreSQL réponde vraiment avant de démarrer (sinon la migration échouerait)
healthcheckToutes les 5 s, pg_isready vérifie que la base accepte des connexions
@db dans DATABASE_URLLe nom du service : Docker le traduit en adresse
volumes: pgdataLes données de la base survivent à la suppression du conteneur. Compose préfixe le nom par celui du dossier (app) : sur la machine, le volume s'appelle app_pgdata.
external: trueLe réseau timemanager existe déjà (créé par le rôle traefik)
internal: trueLe réseau internal n'a pas d'accès vers l'extérieur

Les variables de l'API ​

timemanager-api/config/runtime.exs lit ces variables au démarrage :

VariableObligatoireFournie en prodRôle
DATABASE_URLOuiOuiConnexion à PostgreSQL
SECRET_KEY_BASEOuiOuiClé de signature de Phoenix
PHX_HOSTNonOuiLe nom de domaine de l'API
CORS_ORIGINSOui (ou WEB_URL)OuiLes sites autorisés à appeler l'API avec des cookies
JWT_SECRETNonNonClé des jetons. Sans elle : SECRET_KEY_BASE
WEB_URLNonNonL'adresse du site dans les emails. Sans elle : le premier CORS_ORIGINS
BREVO_API_KEYNonOuiClé du service d'envoi d'emails (Brevo). Sans elle, les emails sont seulement écrits dans les journaux.
MAIL_FROMNonOuiL'expéditeur. Sans elle : no-reply@<PHX_HOST>
PORTNonNonPar défaut 4000
POOL_SIZENonNonNombre de connexions à la base