Skip to content

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 :

  1. client-build — installe les dépendances npm (workspaces), construit le paquet partagé shared, puis compile le frontend React/Vite (vite build) dans client/dist. C'est à cette étape que VITE_API_URL est figée dans le bundle (voir plus bas).
  2. server-build — installe les dépendances, construit shared, puis compile le backend TypeScript (tsc) dans server/dist. Les fichiers .sql non compilables (migrations, bootstrap.sql) sont copiés à côté du code compilé.
  3. 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.
  4. runtime — image finale légère, basée sur node: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

bash
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.js

C'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 :

  1. Application du bootstrap de base (extension uuid-ossp, table users, fonction update_updated_at_column) si la base est vierge — sans effet si déjà en place.
  2. Application des migrations SQL en attente (mêmes migrations que npm run migrate).
  3. 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é avec ENCRYPTION_KEY). Ce provisionnement est un seed « one-shot » — au redémarrage suivant, s'il existe déjà une configuration SMTP en base, les variables SMTP_* 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 :

VariableRôle
DATABASE_URLConnexion à la base PostgreSQL externe
JWT_SECRETSignature des jetons de session
ENCRYPTION_KEYChiffrement des secrets stockés en base (mot de passe SMTP notamment)
APP_URLURL publique utilisée dans les liens envoyés par email
EMAIL_FROMAdresse 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 :

bash
docker volume create timepick-uploads

Construire l'image en fixant l'URL d'API publique :

bash
docker build --build-arg VITE_API_URL=https://timepick.example.org/api -t timepick .

Lancer le conteneur avec un fichier d'environnement :

bash
docker run -d --name timepick -p 3000:3000 --env-file server/.env -v timepick-uploads:/app/server/uploads timepick

Alternative avec des variables individuelles plutôt qu'un fichier :

bash
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 timepick

Vérifier que le conteneur répond :

bash
curl http://localhost:3000/health

Une 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.

Publié sous licence FSL-1.1-MIT.