Installation en production (Docker)
Cette fiche décrit comment construire et lancer l'image Docker officielle de TimePick de façon autonome, avec docker build et docker run. Pour un déploiement guidé pas à pas sur un VPS avec Coolify (interface graphique, HTTPS automatique, volumes, healthcheck), voir Déploiement sur un VPS avec Coolify — cette fiche-ci couvre les mécanismes Docker génériques valables sur n'importe quel orchestrateur.
Vue d'ensemble
Le Dockerfile à la racine du repo produit une image unique qui sert à la fois l'interface web (SPA React buildée) et l'API (/api) sur le même port. Il n'y a pas de PostgreSQL embarqué dans l'image : le conteneur attend une base PostgreSQL externe joignable via DATABASE_URL.
Construction de l'image (multi-stage)
Le Dockerfile est organisé en quatre étapes (stages), chacune nommée :
client-build— installe les dépendances npm (workspaces), construit le paquet partagéshared, puis compile le frontend React/Vite (vite build) dansclient/dist. C'est à cette étape queVITE_API_URLest figée dans le bundle (voir plus bas).server-build— installe les dépendances, construitshared, puis compile le backend TypeScript (tsc) dansserver/dist. Les fichiers.sqlnon compilables (migrations,bootstrap.sql) sont copiés à côté du code compilé.server-deps— installe uniquement les dépendances de production (npm ci --omit=dev), avec la toolchain de compilation native (python3,make,g++) nécessaire à certains modules npm.runtime— image finale légère, basée surnode:22-bookworm-slim: elle ne récupère que le strict nécessaire des trois étapes précédentes (dépendances de prod, build serveur, build client) — aucun outil de compilation ni code source ne s'y retrouve.
Toutes les étapes partent de la même image de base node:22-bookworm-slim.
Commande de build
docker build --build-arg VITE_API_URL=https://timepick.example.org/api -t timepick .Adapter VITE_API_URL au domaine réel de destination (voir avertissement ci-dessous) et timepick au nom d'image souhaité.
VITE_API_URL est figée au build, pas au run
VITE_API_URL est un argument de build (ARG Docker), pas une variable d'environnement runtime. Vite l'intègre en dur dans le bundle JavaScript du frontend au moment de vite build (stage client-build) — elle n'est plus lisible ni modifiable une fois l'image construite.
Conséquence : une image construite avec VITE_API_URL=https://a.example.org/api ne fonctionnera pas correctement si on la démarre pour servir b.example.org — le frontend continuera d'appeler a.example.org/api, même si toutes les variables d'environnement du conteneur sont changées au docker run.
Il faut donc reconstruire l'image (docker build --build-arg VITE_API_URL=...) à chaque changement de domaine public, ou dès qu'on déploie une nouvelle instance sur un autre domaine.
En architecture mono-conteneur (le même serveur Express sert le SPA et l'API sur le même port), VITE_API_URL pointe simplement vers <origine-publique>/api — pas besoin de sous-domaine séparé pour l'API.
Démarrage du conteneur
Le conteneur expose un port unique : 3000 (EXPOSE 3000 dans le Dockerfile). Il n'y a rien à exposer côté client — tout passe par ce même port.
Au démarrage, le conteneur exécute :
node dist/prepare-db.js && node dist/index.jsC'est la commande définie dans le CMD du Dockerfile — pas de dépendance à une fonctionnalité « pre-deploy » spécifique à un orchestrateur.
prepare-db.js est un bootstrap idempotent, rejoué à chaque démarrage du conteneur (redéploiement compris), qui effectue dans l'ordre :
- Application du bootstrap de base (extension
uuid-ossp, tableusers, fonctionupdate_updated_at_column) si la base est vierge — sans effet si déjà en place. - Application des migrations SQL en attente (mêmes migrations que
npm run migrate). - Provisionnement SMTP initial : si aucune configuration SMTP n'existe encore en base, les variables
SMTP_*fournies à l'image sont copiées en base (mot de passe chiffré avecENCRYPTION_KEY). Ce provisionnement est un seed « one-shot » — au redémarrage suivant, s'il existe déjà une configuration SMTP en base, les variablesSMTP_*sont ignorées (la configuration s'administre ensuite dans l'application, Paramètres → Serveur d'email).
Une fois prepare-db.js terminé, node dist/index.js démarre le serveur Express, qui se met à écouter sur le port 3000.
Base de données requise avant le premier démarrage
Le conteneur ne crée pas la base PostgreSQL elle-même (nom de base, rôle, etc.) — seulement son contenu (tables, extension, migrations). Une base PostgreSQL vide, accessible via DATABASE_URL, doit exister avant le premier docker run. Voir aussi Prérequis pour la version minimale de PostgreSQL requise.
Variables d'environnement minimales
Le détail complet de chaque variable (formats, contraintes, génération des secrets) est documenté dans Variables d'environnement. Pour un premier démarrage fonctionnel, le conteneur a besoin au minimum de :
| Variable | Rôle |
|---|---|
DATABASE_URL | Connexion à la base PostgreSQL externe |
JWT_SECRET | Signature des jetons de session |
ENCRYPTION_KEY | Chiffrement des secrets stockés en base (mot de passe SMTP notamment) |
APP_URL | URL publique utilisée dans les liens envoyés par email |
EMAIL_FROM | Adresse d'expédition par défaut des emails |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, … | Provisionnement initial du serveur d'email (premier démarrage uniquement — voir ci-dessus) |
TIP
Sans SMTP_* au premier démarrage, l'application reste utilisable, mais la configuration du serveur d'email devra être saisie manuellement dans l'interface (Paramètres → Serveur d'email) avant de pouvoir envoyer le moindre email de connexion.
Volume persistant pour les uploads
Les images uploadées dans l'éditeur d'emails (logos, illustrations) sont stockées sur le disque du conteneur, dans /app/server/uploads.
Volume obligatoire
Ce chemin n'est pas persistant par défaut : c'est un répertoire ordinaire à l'intérieur du système de fichiers du conteneur. Sans volume monté sur /app/server/uploads, toutes les images uploadées disparaissent au premier redéploiement ou à la première recréation du conteneur (mise à jour d'image comprise).
Il faut monter un volume Docker nommé (ou un bind mount) sur ce chemin avant tout usage en production.
Healthcheck applicatif
Le serveur expose un endpoint public GET /health, qui répond 200 avec un statut JSON (ok ou degraded selon l'état de la connexion SMTP). Le Dockerfile de TimePick ne définit pas d'instruction HEALTHCHECK native — c'est à l'orchestrateur (Coolify, Docker Compose, Kubernetes…) de configurer ce contrôle, en pointant vers GET /health sur le port 3000.
Exemple complet
Base PostgreSQL externe attendue — pas de PostgreSQL embarqué dans cette image. Adapter <IP_DU_SERVEUR> ou l'hôte réseau de la base selon votre environnement.
Créer d'abord le volume persistant :
docker volume create timepick-uploadsConstruire l'image en fixant l'URL d'API publique :
docker build --build-arg VITE_API_URL=https://timepick.example.org/api -t timepick .Lancer le conteneur avec un fichier d'environnement :
docker run -d --name timepick -p 3000:3000 --env-file server/.env -v timepick-uploads:/app/server/uploads timepickAlternative avec des variables individuelles plutôt qu'un fichier :
docker run -d --name timepick -p 3000:3000 -e DATABASE_URL=postgresql://user:password@<IP_DU_SERVEUR>:5432/timepick -e JWT_SECRET=<secret_genere> -e ENCRYPTION_KEY=<cle_64_hex> -e APP_URL=https://timepick.example.org -e EMAIL_FROM=noreply@timepick.example.org -v timepick-uploads:/app/server/uploads timepickVérifier que le conteneur répond :
curl http://localhost:3000/healthUne réponse 200 avec un statut ok (ou degraded si le SMTP n'est pas encore configuré) confirme que le serveur a démarré et que prepare-db.js s'est exécuté avec succès. Consulter ensuite Configuration initiale pour le premier assistant de configuration (création de l'administrateur).
localhost dans le conteneur
Dans le conteneur, localhost désigne le conteneur lui-même, pas la machine hôte. Si PostgreSQL tourne sur la machine hôte (hors conteneur), DATABASE_URL doit pointer vers une adresse joignable depuis l'intérieur du conteneur (IP de l'hôte, nom de service sur un réseau Docker partagé, ou équivalent selon la plateforme) — jamais localhost.