Architecture — Déploiement
Le service tourne en conteneur. Trois stacks Docker Compose servent à trois usages distincts.
Stacks Compose
| Fichier | Usage | Persistance | Particularités |
|---|---|---|---|
docker-compose.yml | Dev local | Non | Build local, montage .:/app, WITH_DOCS=true |
docker-compose-test.yml | DB de test (port 5433) | Non | Que Postgres, pour pytest |
docker-compose-deploy.yml | Production / staging | Oui | Image 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_TOKENinjecté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: - .:/appmonte le repo dans le conteneur pour le live-reload, mais l'image est build sur la copie duDockerfileà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 —
pytesttourne 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_url—app.pylit ce fichier et écrit la valeur dansos.environ['DATABASE_URL']au démarrage.- Volume Postgres persistant (
postgres-data). - Réseau Docker externe
app-networkpartagé avec le reverse proxy / Traefik / etc. (pas dans ce repo). WITH_DOCSomis → Swagger fermé en prod.
Secrets attendus dans ./ au moment du docker compose up :
| Fichier | Contenu |
|---|---|
infomaniak_pid.txt | Product ID de l'API embedding (cf. coffre-fort équipe) |
infomaniak_token.txt | Token Bearer associé |
postgres_password.txt | Mot de passe better_search de la DB |
database_url.txt | URL complète, ex postgresql://better_search:…@postgres:5432/… |
Construction d'image (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 viauv.lock). --no-dev: exclut les deps de test (pytest, etc.).VERSION_TAG/BUILD_DATE: injectés audocker 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
#!/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 applyest idempotent (yoyo tient un registre_yoyo_migrationcôté DB). Pas de souci à redémarrer. --batch: ne demande pas de confirmation interactive.exec fastapi run: remplace le shell,fastapidevient PID 1, reçoitSIGTERMdirectement (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
| Variable | Obligatoire | Source typique | Effet |
|---|---|---|---|
DATABASE_URL | ✅ (ou _FILE) | Compose / secret | Connexion Postgres |
DATABASE_URL_FILE | alt. au précédent | Compose secret | Chemin fichier lu par app.py au boot |
SLOW_SEARCH_URL | ✅ | Compose | URL amont, ex https://horizon.buchard.ch/api/travels |
EMBEDDING_URL | ✅ | Compose (legacy, encore assert dans app.py) | URL embeddings (préfixe template Infomaniak) |
INFOMANIAK_PID | ✅ | Secret | Product ID Infomaniak |
INFOMANIAK_TOKEN | ✅ | Secret | Token Bearer Infomaniak |
INFOMANIAK_PID_FILE | alt. | Secret Docker (prod) | Chemin fichier (non lu par app.py, à vérifier) |
INFOMANIAK_TOKEN_FILE | alt. | Secret Docker (prod) | Idem |
REINDEX_FREQUENCY | non | Compose | Secondes entre reindex auto (défaut 900) |
DEFAULT_RRF_VECTOR_WEIGHT | non | Compose | Pondération RRF par défaut ([0, 1]) |
DEFAULT_SEMANTIC_FLOOR | non | Compose | Plancher cosinus par défaut ([0, 1]) |
DEFAULT_TSVECTOR_FLOOR | non | Compose | Plancher ts_rank_cd par défaut (≥ 0) |
WITH_DOCS | non | Compose | true expose /docs /redoc /openapi.json |
VERSION_TAG | non | Docker build-arg (CI) | Affiché sur / |
BUILD_DATE | non | Docker build-arg (CI) | Affiché sur / |
TEST_DATABASE_URL | (tests) | Local | Override de DATABASE_URL pour pytest |
Note :
INFOMANIAK_PID_FILE/INFOMANIAK_TOKEN_FILEsont déclarés dansdocker-compose-deploy.ymlmaisapp.pyn'a pas (pour l'instant) la logique pour lire ces fichiers comme il le fait pourDATABASE_URL_FILE. À valider lors d'un déploiement prod — option safe : pré-injecter dansINFOMANIAK_PID/INFOMANIAK_TOKENdirectement, ou patcherapp.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|prodVoir 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.
- Staging :
- Variable CI obligatoire :
RELEASE_TOKEN(scopeswrite_repository+api, masked + protected). - Stage
test:- Tourne sur tous les pushs/MRs,
allow_failure: truepartout sauf surmasteretrelease/*. - Service Postgres 17 + pgvector en sidecar GitLab (alias
postgres). yoyo applypuispytest tests/ -v --cov=src --cov-fail-under=70.
- Tourne sur tous les pushs/MRs,
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_searchdans les Compose (interval: 5s, retries 5). - Métriques fonctionnelles :
last_reindex_time,db_size,n_results_in_search— voirmodules/monitoring.md. Une absence d'avancement delast_reindex_timeest 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-stagingsont 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.

