Déploiement sur un VPS avec Coolify
Ce guide déploie TimePick en production sur un VPS piloté par Coolify, une plateforme d'auto-hébergement open source. Un seul conteneur Node sert à la fois l'application React (fichiers statiques) et l'API (/api), les fichiers uploadés (/uploads) et le contrôle de santé (/health) — tout passe par le même domaine, sans configuration CORS à gérer. Une seule base PostgreSQL, gérée par Coolify à côté de l'application.
Cette fiche couvre la mise en ligne complète, du VPS nu à l'application accessible en HTTPS. Le détail du Dockerfile et du processus de build est décrit dans Installation en production (Docker) ; le détail de chaque variable d'environnement dans Variables d'environnement.
TIP
Coolify n'est pas obligatoire : le même Dockerfile fonctionne sur n'importe quelle plateforme capable de builder une image et d'exposer un port (Railway, Render, un VPS nu avec docker run…). Ce guide se concentre sur Coolify parce qu'il offre une interface graphique complète (base de données, variables, volumes, HTTPS automatique) sans frais de plateforme.
Prérequis
- Un VPS avec Coolify installé et son proxy actif (Coolify s'installe sur n'importe quel VPS via son script officiel — voir la documentation Coolify pour cette étape préalable, hors périmètre de ce guide).
- Un nom de domaine dont vous gérez la zone DNS (
votre-domaine.tld). - L'adresse IP publique du VPS (
<IP_DU_SERVEUR>). - Un dépôt GitHub contenant le code de TimePick.
Étape 1 — DNS
Chez votre fournisseur DNS, ajoutez un enregistrement de type A qui pointe le sous-domaine choisi vers le VPS :
| Type | Nom | Contenu |
|---|---|---|
| A | timepick | <IP_DU_SERVEUR> |
Ce qui donne timepick.votre-domaine.tld → <IP_DU_SERVEUR>.
Vérifiez la propagation avant de continuer :
dig timepick.votre-domaine.tld +shortLe résultat doit afficher l'IP du VPS.
WARNING
Si le proxy de votre fournisseur DNS (type Cloudflare) est activé sur cet enregistrement, désactivez-le temporairement (mode « DNS uniquement »). Coolify a besoin d'atteindre le VPS directement sur le port 80 pour émettre le certificat HTTPS via Let's Encrypt. Vous pourrez réactiver le proxy une fois le certificat émis — voir la section Durcissement post-déploiement.
Étape 2 — Créer la base de données PostgreSQL
Dans Coolify, toutes les ressources d'une application vivent dans un projet.
- Créez un projet (ex. « TimePick »).
- Dans ce projet → + Add Resource → Database → PostgreSQL → choisissez PostgreSQL 17.
- Renseignez :
- Name :
timepick-db - Initial Database :
timepick(sinon la base créée s'appellepostgrespar défaut)
- Name :
- Deploy — attendez que le statut passe à Running.
INFO
TimePick recommande PostgreSQL 16 au minimum (baseline testée du projet) ; choisissez 17, la version utilisée en production (voir Prérequis).
Une fois la base démarrée, récupérez son URL interne (Configuration → section Network → « Postgres URL (internal) ») — elle sera nécessaire à l'étape 6. Son format ressemble à :
postgres://postgres:<mot_de_passe>@timepick-db-<id>:5432/timepickÉtape 3 — Connecter le dépôt GitHub
Coolify a besoin d'un accès au dépôt pour builder l'image et déclencher les redéploiements automatiques sur git push.
- Barre latérale → Sources → + Add → GitHub App.
- Suivez l'assistant Coolify : il crée une GitHub App, redirige vers GitHub pour la validation, puis demande d'installer cette App sur le dépôt TimePick (choisir Only select repositories).
- Retour dans Coolify — la source est configurée et réutilisable pour d'autres projets.
Étape 4 — Créer l'application
Dans le même projet :
- + Add Resource → Application.
- Choisissez la source GitHub configurée à l'étape précédente → sélectionnez le dépôt TimePick → branche
main. - Build Pack : Dockerfile — Coolify détecte automatiquement le
Dockerfileà la racine du dépôt (détail du build : Installation en production (Docker)). - Vérifiez que Ports Exposes est bien réglé sur
3000.
Étape 5 — Domaine de l'application
Dans l'onglet General de l'application, section Domains, remplacez l'URL générée automatiquement par celle configurée à l'étape 1 :
https://timepick.votre-domaine.tldCoolify provisionne automatiquement le certificat TLS via Let's Encrypt dès que le DNS pointe correctement et que les ports 80/443 sont ouverts.
Étape 6 — Variables d'environnement
Dans l'onglet Environment Variables de l'application, renseignez les variables suivantes. Le rôle et les valeurs par défaut détaillés se trouvent dans Variables d'environnement — seuls les points spécifiques à un déploiement Coolify sont repris ici.
| Variable | Rôle | Exemple |
|---|---|---|
VITE_API_URL | URL de l'API vue par le navigateur (injectée au build) | https://timepick.votre-domaine.tld/api |
NODE_ENV | Mode d'exécution | production |
PORT | Port interne écouté par le conteneur | 3000 |
DATABASE_URL | URL interne de la base créée à l'étape 2 | postgres://postgres:...@timepick-db-xxxx:5432/timepick |
JWT_SECRET | Secret de signature des sessions — optionnel, auto-généré au premier démarrage si absent | optionnel (voir ci-dessous) |
ENCRYPTION_KEY | Clé de chiffrement (identifiants SMTP en base) — optionnel, auto-généré au premier démarrage si absent | optionnel (voir ci-dessous) |
APP_URL | URL publique de l'application | https://timepick.votre-domaine.tld |
EMAIL_FROM | Adresse d'expédition des emails | noreply@votre-domaine.tld |
SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_NAME, SMTP_FROM_EMAIL | Identifiants du fournisseur d'envoi d'emails | voir Étape 7 |
JWT_SECRET et ENCRYPTION_KEY sont optionnels : si vous ne les renseignez pas dans le tableau ci-dessus, TimePick les génère automatiquement au premier démarrage (32 octets aléatoires, 64 caractères hexadécimaux) et les écrit dans /app/server/data (jwt.secret, encryption.key, permissions 0600) — voir l'étape 8 ci-dessous pour le volume correspondant.
Précédence : variable d'environnement > fichier. Si la variable est définie et valide dans Coolify, elle est utilisée et le fichier généré sur le volume est ignoré (avertissement loggé). C'est la façon de « promouvoir » une clé auto-générée en variable gérée — pratique après un premier déploiement de test.
Pour promouvoir les secrets d'une installation déjà en service (fichiers présents sur le volume data), extraire d'abord les valeurs existantes et les reposer à l'identique — jamais des valeurs neuves : suivre la procédure disque → environnement.
Générer les secrets manuellement (optionnel)
Si vous préférez gérer les clés vous-même en variables d'environnement plutôt que de laisser TimePick les générer, lancez ces deux commandes séparément et notez les deux valeurs :
openssl rand -hex 32openssl rand -hex 32La première valeur va dans JWT_SECRET, la seconde (différente) dans ENCRYPTION_KEY.
Mise à l'échelle horizontale (plusieurs répliques)
Chaque conteneur qui ne voit ni variables d'environnement ni fichiers déjà générés (pas de volume data partagé, ou système de fichiers éphémère) génère sa propre paire de secrets au démarrage : les sessions et les données chiffrées d'une réplique deviennent alors illisibles par les autres. Avec le volume data de l'étape 8 monté, le chevauchement d'un redéploiement est sans risque — les conteneurs lisent les mêmes fichiers. Si vous faites coexister des conteneurs sans ce stockage partagé, fournissez JWT_SECRET et ENCRYPTION_KEY en variables d'environnement partagées. Dans tous les cas, un régime permanent à plusieurs instances n'est pas pris en charge (limitations de débit maintenues en mémoire, par processus) — voir la contrainte « 1 seule instance » dans Déployer sur un PaaS.
Variables build-time vs runtime
Deux réglages sont indispensables sur Coolify, sans quoi le déploiement échoue ou l'application appelle la mauvaise API :
VITE_API_URLdoit avoir la case Available at Buildtime cochée — Vite en a besoin au moment du build, pas seulement à l'exécution.NODE_ENVdoit avoir cette même case décochée (garder uniquement Available at Runtime) — sinon le build Docker saute les dépendances de développement nécessaires à la compilation et échoue.
Étape 7 — Fournisseurs SMTP
Les variables SMTP_* ne servent qu'au premier démarrage : si elles sont présentes, TimePick les copie une fois en base au démarrage pour pré-remplir l'assistant de configuration initiale. Ensuite, la configuration SMTP se gère exclusivement depuis l'application (Paramètres → Serveur d'email) — modifier ces variables dans Coolify après le premier démarrage n'a plus d'effet. Le détail par fournisseur (dont Gmail) est dans Configuration SMTP par fournisseur.
Deux repères rapides pour cette étape :
- Brevo — compte gratuit sur brevo.com, section SMTP & API du dashboard :
SMTP_HOST=smtp-relay.brevo.com,SMTP_PORT=587,SMTP_SECURE=false(STARTTLS)SMTP_USER= adresse email du compte Brevo,SMTP_PASSWORD= clé SMTP générée (pas le mot de passe du compte)
- OVH — boîte email existante chez OVH :
SMTP_HOST=ssl0.ovh.net,SMTP_PORT=465,SMTP_SECURE=true(SSL)SMTP_USERetSMTP_FROM_EMAIL= adresse email complète,SMTP_PASSWORD= mot de passe de cette boîte
À confirmer
Ces deux jeux de paramètres sont des repères fournis par les fournisseurs eux-mêmes, pas des valeurs vérifiées de bout en bout avec un compte réel sur l'instance de référence de cette documentation. Testez systématiquement l'envoi (bouton « Tester la connexion » de l'assistant de configuration, voir étape 11) avant de considérer le déploiement fonctionnel.
Étape 8 — Volumes persistants
Deux volumes doivent être montés dans l'onglet Persistent Storage (section Storages) de l'application : un pour les uploads, un pour les secrets auto-générés.
Volume 1 — Uploads
Les images insérées dans les emails sont stockées dans /app/server/uploads. Sans volume, elles disparaissent à chaque redéploiement.
- + Add → Volume Mount.
- Name :
timepick-uploads, Destination Path :/app/server/uploads(laisser Source Path vide). - Add.
Alternative sans volume : stockage objet
STORAGE_DRIVER=s3 externalise les images vers un object storage S3-compatible et rend ce volume inutile (voir Variables d'environnement). Sur un VPS avec disque persistant, le défaut local + volume reste le chemin le plus simple.
Volume 2 — Data (secrets auto-générés)
Si JWT_SECRET et/ou ENCRYPTION_KEY n'ont pas été fournis à l'étape 6, ils sont générés au premier démarrage et écrits dans /app/server/data.
- + Add → Volume Mount.
- Name :
timepick-data, Destination Path :/app/server/data(laisser Source Path vide). - Add.
Pourquoi ce volume est requis
Sans volume persistant sur /app/server/data, les secrets sont régénérés à chaque recréation du conteneur (chaque redéploiement Coolify) : le mot de passe SMTP stocké en base ne peut plus être déchiffré, et toutes les sessions / magic-links en attente sont invalidés — rupture silencieuse de l'envoi d'emails, avec un risque de verrouillage des administrateurs. Le recours de dernier ressort est les codes de récupération de connexion d'urgence.
Étape 9 — Healthcheck
Dans l'onglet Healthcheck, vérifiez les valeurs par défaut :
- Method
GET· Path/health· Port3000· Return Code200
Cliquez Save puis Enable Healthcheck (les deux boutons sont distincts — sans le second, Coolify ignore le contrôle de santé même si les valeurs sont enregistrées).
Étape 10 — Premier déploiement
Cliquez Deploy et suivez les logs. Au démarrage, le conteneur exécute automatiquement la préparation de la base (création de la table users si absente) puis les migrations, avant d'accepter des connexions — aucune commande manuelle n'est nécessaire pour ça.
Une fois le statut Running affiché, vérifiez :
curl -I https://timepick.votre-domaine.tldcurl https://timepick.votre-domaine.tld/healthLe healthcheck doit répondre avec un code HTTP 200.
Étape 11 — Premier démarrage : configuration initiale
Fenêtre de sécurité
Tant qu'aucun administrateur n'existe en base, les routes de configuration initiale (/api/setup/*) sont publiques — accessibles sans authentification, protégées seulement par un rate-limit par IP. Un tiers capable de joindre le serveur pendant cette fenêtre pourrait configurer le SMTP ou déclencher la création du premier administrateur à sa place.
Enchaînez immédiatement avec la création du premier administrateur après ce premier déploiement — voir Configuration initiale (première utilisation). Une fois un administrateur créé, ces routes redeviennent inaccessibles (404).
Durcissement post-déploiement
Une fois l'application accessible en HTTPS sans erreur :
- Proxy / CDN — si vous utilisiez un fournisseur type Cloudflare en mode « DNS uniquement » pour l'émission du certificat (étape 1), réactivez le proxy (mode Full ou Full strict). Le trafic passe alors par le CDN avant d'atteindre le VPS, ce qui masque l'IP d'origine.
- Pare-feu — ne laissez ouverts que les ports strictement nécessaires (22 pour SSH, 80/443 pour le web, plus les ports d'administration Coolify si vous y accédez encore depuis l'extérieur). Refermez le reste dans le groupe de sécurité de votre fournisseur VPS.
- HTTPS — vérifiez qu'aucune ressource ne charge encore en HTTP simple et que le certificat se renouvelle automatiquement (Coolify s'en charge via Let's Encrypt).
Accès à la base de données en production
Trois options, du plus rapide au plus confortable :
- Terminal intégré Coolify — onglet Terminal de la ressource
timepick-db, puispsql -U postgres -d timepick. Pratique pour une vérification ponctuelle, sans configuration. - Port temporaire — rendre la base brièvement accessible depuis un client GUI local (TablePlus, DBeaver…) via un port public, à refermer immédiatement après usage.
- pgAdmin — interface web permanente déployée comme service Coolify dans le même projet, accès à la base via le réseau interne Docker (aucun port public requis).
Le détail des sauvegardes régulières et de la restauration est couvert dans Sauvegarde et restauration.
Pour aller plus loin
- Variables d'environnement — référence complète.
- Déployer sur un PaaS (image GHCR) — modèle « image unique tirée », kits DigitalOcean App Platform / Heroku, variante Coolify en mode pull.
- Configuration SMTP par fournisseur — Gmail, Brevo, OVH en détail.
- Configuration initiale (première utilisation) — assistant de configuration.
- Mise à jour — redéploiements suivants.
- Sauvegarde et restauration — protéger les données en production.