Skip to content

Architecture — Déploiement

Le service tourne en conteneur. Trois stacks Docker Compose servent à trois usages distincts.

Stacks Compose

FichierUsagePersistanceParticularités
docker-compose.ymlDev localNonBuild local, montage .:/app, WITH_DOCS=true
docker-compose-test.ymlDB de test (port 5433)NonQue Postgres, pour pytest
docker-compose-deploy.ymlProduction / stagingOuiImage pré-build, secrets Docker, réseau externe

Dev (docker-compose.yml)

  • Lance Postgres 17 + pgvector et build l'image localement.
  • Variables d'env hardcodées dans le fichier (sauf INFOMANIAK_PID / INFOMANIAK_TOKEN injectées depuis .env).
  • WITH_DOCS=true → expose /docs, /redoc, /openapi.json (Swagger).
  • DEFAULT_RRF_VECTOR_WEIGHT=0.4, DEFAULT_SEMANTIC_FLOOR=0.5, DEFAULT_TSVECTOR_FLOOR=0.0 — plus permissif que le défaut prod.
  • Le volumes: - .:/app monte le repo dans le conteneur pour le live-reload, mais l'image est build sur la copie du Dockerfile à docker compose build. Pour un reload Python à chaud, redémarrer le conteneur.

Tests (docker-compose-test.yml)

  • Que Postgres + pgvector, exposé sur le port 5433 (≠ 5432 du dev) pour cohabiter.
  • DB s'appelle better_search_test.
  • Pas de conteneur applicatif — pytest tourne en local et se connecte à localhost:5433.

Prod / staging (docker-compose-deploy.yml)

  • Image tirée de registry.internal.spektrum-suisse.ch/buchsearch:<tag> (où <tag>X.Y.Z, X.Y.Z-staging.N, latest, latest-staging).
  • Tous les secrets sont montés en secrets: Docker (fichiers dans /run/secrets/).
  • DATABASE_URL_FILE=/run/secrets/database_urlapp.py lit ce fichier et écrit la valeur dans os.environ['DATABASE_URL'] au démarrage.
  • Volume Postgres persistant (postgres-data).
  • Réseau Docker externe app-network partagé avec le reverse proxy / Traefik / etc. (pas dans ce repo).
  • WITH_DOCS omis → Swagger fermé en prod.

Secrets attendus dans ./ au moment du docker compose up :

FichierContenu
infomaniak_pid.txtProduct ID de l'API embedding (cf. coffre-fort équipe)
infomaniak_token.txtToken Bearer associé
postgres_password.txtMot de passe better_search de la DB
database_url.txtURL complète, ex postgresql://better_search:…@postgres:5432/…

Construction d'image (Dockerfile)

dockerfile
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
ENV UV_PROJECT_ENVIRONMENT="/opt/venv"
RUN uv sync --frozen --no-dev
COPY . .
RUN chmod +x entrypoint.sh
ENV PATH="/opt/venv/bin:$PATH"
ARG VERSION_TAG="dev"
ENV VERSION_TAG=${VERSION_TAG}
ARG BUILD_DATE="unknown"
ENV BUILD_DATE=${BUILD_DATE}
CMD ["./entrypoint.sh"]
  • Base : python:3.13-slim.
  • Installeur : uv (rapide, déterministe via uv.lock).
  • --no-dev : exclut les deps de test (pytest, etc.).
  • VERSION_TAG / BUILD_DATE : injectés au docker build, exposés via / (sanity check). Si vous voyez la mauvaise version après déploiement, vérifier que CI a bien passé --build-arg VERSION_TAG=....

entrypoint.sh

sh
#!/bin/sh
set -e
echo "Running database migrations..."
yoyo apply --database "$DATABASE_URL" ./migrations --batch
echo "Starting application..."
exec fastapi run app.py --port 80
  • Migrations à chaque démarrage : yoyo apply est idempotent (yoyo tient un registre _yoyo_migration côté DB). Pas de souci à redémarrer.
  • --batch : ne demande pas de confirmation interactive.
  • exec fastapi run : remplace le shell, fastapi devient PID 1, reçoit SIGTERM directement (graceful shutdown).
  • Port 80 : interne au conteneur. Le mapping vers 8088 (dev) ou 80/443 (prod via reverse proxy) est défini dans le Compose.

Variables d'environnement

VariableObligatoireSource typiqueEffet
DATABASE_URL✅ (ou _FILE)Compose / secretConnexion Postgres
DATABASE_URL_FILEalt. au précédentCompose secretChemin fichier lu par app.py au boot
SLOW_SEARCH_URLComposeURL amont, ex https://horizon.buchard.ch/api/travels
EMBEDDING_URLCompose (legacy, encore assert dans app.py)URL embeddings (préfixe template Infomaniak)
INFOMANIAK_PIDSecretProduct ID Infomaniak
INFOMANIAK_TOKENSecretToken Bearer Infomaniak
INFOMANIAK_PID_FILEalt.Secret Docker (prod)Chemin fichier (non lu par app.py, à vérifier)
INFOMANIAK_TOKEN_FILEalt.Secret Docker (prod)Idem
REINDEX_FREQUENCYnonComposeSecondes entre reindex auto (défaut 900)
DEFAULT_RRF_VECTOR_WEIGHTnonComposePondération RRF par défaut ([0, 1])
DEFAULT_SEMANTIC_FLOORnonComposePlancher cosinus par défaut ([0, 1])
DEFAULT_TSVECTOR_FLOORnonComposePlancher ts_rank_cd par défaut (≥ 0)
WITH_DOCSnonComposetrue expose /docs /redoc /openapi.json
VERSION_TAGnonDocker build-arg (CI)Affiché sur /
BUILD_DATEnonDocker build-arg (CI)Affiché sur /
TEST_DATABASE_URL(tests)LocalOverride de DATABASE_URL pour pytest

Note : INFOMANIAK_PID_FILE / INFOMANIAK_TOKEN_FILE sont déclarés dans docker-compose-deploy.yml mais app.py n'a pas (pour l'instant) la logique pour lire ces fichiers comme il le fait pour DATABASE_URL_FILE. À valider lors d'un déploiement prod — option safe : pré-injecter dans INFOMANIAK_PID / INFOMANIAK_TOKEN directement, ou patcher app.py.

Pipeline CI/CD (GitLab)

Trois stages : test, release (manuel), publish.

trunk MR ──merge──► master ──cut release──► release/X.Y.Z MR ──merge──► master ──► publish-staging|prod

Voir modules/releasing.md pour la procédure complète. Côté infra, ce qu'il faut savoir :

  • Image registry : registry.internal.spektrum-suisse.ch/buchsearch.
  • Tags push à chaque release :
    • Staging : :X.Y.Z-staging.N + :latest-staging.
    • Prod : :X.Y.Z + :latest.
  • Variable CI obligatoire : RELEASE_TOKEN (scopes write_repository + api, masked + protected).
  • Stage test :
    • Tourne sur tous les pushs/MRs, allow_failure: true partout sauf sur master et release/*.
    • Service Postgres 17 + pgvector en sidecar GitLab (alias postgres).
    • yoyo apply puis pytest tests/ -v --cov=src --cov-fail-under=70.

Réseau et reverse proxy

Le service écoute en clair sur le port 80 du conteneur. Le mapping externe et la terminaison TLS sont assurés par le reverse proxy (Traefik / Nginx, hors repo). Le service expose :

  • GET / — sanity check (200 toujours).
  • GET /travels, GET /reindex — endpoints fonctionnels.
  • GET /metrics — Prometheus (à protéger côté reverse proxy si exposé publiquement).
  • GET /.well-known/assetlinks.json — fichier statique pour Android App Links (ch.buchard.app).

CORS est ouvert : allow_origins=["*"] + allow_credentials=True (cf. src/server.py ligne 36-42). À tightener si on bouge le service hors d'un réseau interne.

Health check / monitoring de la liveness

  • Health check applicatif : GET / répond toujours 200 si FastAPI tourne. Ne valide PAS la connexion DB.
  • Health check Postgres : pg_isready -U better_search dans les Compose (interval: 5s, retries 5).
  • Métriques fonctionnelles : last_reindex_time, db_size, n_results_in_search — voir modules/monitoring.md. Une absence d'avancement de last_reindex_time est le meilleur signal d'un problème de fond.

Rollback

  • Image : redéployer le tag immutable précédent (:X.Y.Z). Les tags :latest / :latest-staging sont des pointeurs mobiles, à éviter pour un rollback ciblé.
  • DB : yoyo supporte le rollback (yoyo rollback) si chaque migration a une section __step__ avec un rollback. Aujourd'hui les migrations n'ont pas de rollback explicite → en cas de schéma cassé, c'est restauration depuis backup PG, pas rollback applicatif.
  • Config : changer une env var (RRF weight, floor) ne nécessite pas de re-déploiement d'image — juste un redémarrage du conteneur après mise à jour du Compose.

Contributors

No contributors

Changelog

No recent changes