Déploiement Docker Compose
Cette page décrit le déploiement de FoxPlan sur un serveur unique avec Docker Compose, derrière Traefik qui assure le routage et la terminaison TLS.
Pour une cible de production avec montée en charge et haute disponibilité, voir le format Kubernetes.
Fichiers à télécharger
- docker-compose.yml — la pile complète. Ce fichier n'a pas à être modifié.
- install.sh — prépare le déploiement : certificat, configuration Traefik, contrôles de cohérence
- env.example — toutes les variables, à enregistrer sous le nom
.env
Placez les trois fichiers dans un même répertoire. Toute la configuration se fait dans .env : le docker-compose.yml reste intact, ce qui rend les montées de version indolores.
L'adresse du registre (FOXPLAN_REGISTRY) et les identifiants de lecture vous sont communiqués avec votre souscription on-premise, en même temps que votre licence. Ils ne figurent pas dans cette documentation.
Architecture
Internet ou réseau interne
(DNS : DOMAIN → ce serveur)
│ 80 / 443
▼
┌─────────┐ terminaison TLS
│ Traefik │ redirection 80 → 443
└────┬────┘
┌──────────────────┼──────────── ──────┐
▼ ▼ ▼
/ (app:5000) /docs (docs:8080) /gantt-export:3200
product-server docs gantt-export
│ │
├──► mongo (réseau interne) └──► gantt-msp (interne)
└──► redis (réseau interne)
| Service | Rôle | Indispensable |
|---|---|---|
product-server | Application FoxPlan | Oui |
docs | Cette documentation, servie sous /docs | Non |
gantt-export | Export des plannings en PDF / PNG / Excel / iCal | Non |
gantt-msp | Import et export MS Project et Primavera | Non |
mongo | Base de données | Oui |
redis | Cache et sessions partagées | Oui |
Sans gantt-export, l'application fonctionne normalement : seuls les exports graphiques du Gantt sont indisponibles.
Prérequis
- Un serveur Linux avec Docker et Docker Compose v2 ;
- Un nom de domaine dont l'enregistrement DNS pointe vers ce serveur ;
- Les ports 80 et 443 joignables par vos utilisateurs ;
- Un accès en lecture au registre d'images FoxPlan, et votre licence ;
- Selon le mode TLS retenu, un accès sortant vers l'autorité de certification (voir plus bas).
Installation
1. Préparation
chmod +x install.sh
./install.sh
Au premier lancement, le script crée .env depuis env.example et s'arrête pour vous laisser le renseigner.
| Variable | Description |
|---|---|
DOMAIN | Domaine servi, par exemple foxplan.mon-entreprise.fr |
FOXPLAN_REGISTRY | Registre d'images, communiqué avec votre souscription |
FOXPLAN_IMAGE | Nom de base des images (fourni avec le registre) |
FOXPLAN_VERSION | Version déployée — épinglez une version précise en production |
MONGO_IMAGE / REDIS_IMAGE | Versions validées des dépendances |
TLS_MODE | selfsigned, acme ou static — voir TLS |
ACME_EMAIL, ACME_CA_SERVER | Autorité de certification, si TLS_MODE=acme |
latest en productionFOXPLAN_VERSION=latest suit la dernière version publiée : pratique pour un pilote, déconseillé ensuite. Un simple redémarrage de conteneur peut alors changer de version sans que vous l'ayez décidé. Épinglez une version, et faites vos montées de version explicitement.
Relancez ensuite ./install.sh : il génère le certificat par défaut et la configuration Traefik correspondant à votre TLS_MODE. Le script est idempotent — vous pouvez le rejouer à volonté, il ne remplace jamais un certificat ni un .env existant.
2. Secrets applicatifs
Trois fichiers, fournis avec le package de déploiement sous forme de modèles .example :
cp .env_application.example .env_application # base de données, OAuth, S3, JWT…
cp .env_mongo.example .env_mongo # identifiants MongoDB
cp .env_redis.example .env_redis # mot de passe Redis
Cohérences à respecter, sous peine d'échec au démarrage :
URL_MONGOreprend l'utilisateur et le mot de passe de.env_mongo, avec l'hôtemongo;SPRING_REDIS_PASSWORDest identique àREDIS_PASSWORD;BASE_URLvauthttps://suivi deDOMAIN— c'est lui qui construit les liens contenus dans les emails.
.env est lu par Docker Compose pour assembler la pile ; il n'est pas transmis au conteneur applicatif. Les variables de l'application — base, emails, S3, JWT — vont dans .env_application. Une variable applicative placée dans .env reste sans effet, silencieusement.
Envoi des emails
FoxPlan envoie des invitations, des réinitialisations de mot de passe et des notifications. Sans configuration, aucun email ne part : l'application démarre et fonctionne, mais ces fonctions restent muettes.
À renseigner dans .env_application :
MAIL_PROXY_TYPE=smtp
MAIL_FROM=foxplan@mon-entreprise.fr
MAIL_SMTP_HOST=smtp.mon-entreprise.fr
MAIL_SMTP_PORT=587
MAIL_SMTP_USERNAME=
MAIL_SMTP_PASSWORD=
MAIL_SMTP_AUTH=true
MAIL_SMTP_STARTTLS=true
L'onglet Paramétrage → Entreprise → Emails montre, e-mail par e-mail, ce que votre serveur d'envoi a répondu, avec un bandeau vert / orange / rouge sur l'état de la plateforme. C'est le premier endroit à regarder quand un utilisateur dit n'avoir rien reçu — voir Journal des e-mails.
MAIL_PROXY_TYPE accepte aussi sendgrid ou gmail, pour un compte fourni par vos soins ; MAIL_PROXY_PARAMS porte alors <expéditeur>;<clé ou mot de passe>. Dans la très grande majorité des installations, smtp est le bon choix.
Deux points qui décident du sort de vos messages :
MAIL_FROMdoit être autorisé à émettre pour votre domaine (SPF, DKIM). Un expéditeur non aligné part en indésirables, ou est rejeté sans que l'application en sache rien.BASE_URLconstruit les liens des emails. S'il est absent ou faux, les messages partent correctement mais les liens qu'ils contiennent — invitation, réinitialisation — ne mènent nulle part.
Pour vérifier après démarrage, déclenchez une réinitialisation de mot de passe depuis l'écran de connexion et suivez les journaux :
docker compose logs -f product-server | grep -i mail
Un échec d'envoi y apparaît en avertissement : l'application ne s'interrompt jamais pour un email non parti.
3. Démarrage
docker login "$FOXPLAN_REGISTRY"
docker compose up -d
ou, en une fois : ./install.sh --start.
Vérification :
docker compose ps
curl -I https://VOTRE_DOMAINE/
curl -I https://VOTRE_DOMAINE/docs/
TLS : trois modes
Quel que soit le mode, install.sh installe un certificat par défaut. Traefik le sert dès le premier démarrage, et continue de le servir si l'obtention automatique échoue : le site ne tombe jamais par manque de certificat, il dégrade au pire vers un avertissement navigateur.
Le choix se fait par la variable TLS_MODE, et tient à une seule question : qui émet vos certificats, et Traefik peut-il lui parler ?
TLS_MODE | Autorité | Accès sortant | Renouvellement | Usage |
|---|---|---|---|---|
selfsigned | Aucune | Aucun | — | Évaluation, pilote |
acme | Let's Encrypt ou votre PKI | Vers l'autorité | Automatique | Production |
static | Quelconque | Aucun | À votre charge | Réseau fermé, wildcard d'entreprise |
selfsigned — démarrer immédiatement
Mode par défaut. install.sh génère un certificat auto-signé pour votre DOMAIN, valable 825 jours. L'application est joignable en HTTPS tout de suite, sans DNS public ni ouverture de pare-feu.
Les navigateurs afficheront un avertissement : c'est attendu. Ce mode sert à valider l'installation, pas à exposer le service à des utilisateurs.
acme — renouvellement automatique
TLS_MODE=acme
ACME_EMAIL=admin@mon-entreprise.fr
ACME_CA_SERVER=https://acme-v02.api.letsencrypt.org/directory
Traefik obtient le certificat par un challenge HTTP-01 et le renouvelle seul. Avec Let's Encrypt, deux conditions : le domaine doit résoudre publiquement vers ce serveur, et le port 80 doit être joignable depuis Internet — le challenge y passe, même si tout le trafic est ensuite redirigé vers 443.
Avec votre PKI interne, rien ne change sinon l'adresse. Si votre autorité expose un service ACME — step-ca, HashiCorp Vault, EJBCA — le protocole est le même (RFC 8555) :
ACME_CA_SERVER=https://ca.interne.mon-entreprise.fr/acme/acme/directory
Si cette autorité est privée, Traefik ne la connaît pas et la négociation échouerait sur un certificat inconnu. Fournissez-lui la chaîne de confiance :
cp /chemin/vers/ca-interne.crt traefik/ca/ca-interne.crt
puis, dans .env :
LEGO_CA_CERTIFICATES=/ca/ca-interne.crt
install.sh vérifie la présence du fichier et vous avertit s'il manque.
C'est le mode à privilégier dans un réseau fermé : vous gardez le renouvellement automatique sans aucune dépendance à Internet, à la seule condition que le serveur atteigne votre autorité.
Les PKI internes émettent souvent des certificats valables quelques jours. C'est sans conséquence : Traefik renouvelle d'autant plus souvent.
static — votre certificat, votre renouvellement
Pour un certificat obtenu hors ligne — wildcard d'entreprise, certificat commercial, autorité interne sans service ACME. Aucun ACME n'est configuré : Traefik ne sort jamais sur le réseau pour obtenir un certificat.
Écrasez simplement les fichiers générés par les vôtres :
cp votre-certificat.crt traefik/certs/tls.crt # chaîne complète
cp votre-cle.key traefik/certs/tls.key
chmod 600 traefik/certs/tls.key
tls.crt doit contenir le certificat du serveur suivi de ses intermédiaires, dans cet ordre. Un certificat servi sans sa chaîne fonctionne dans un navigateur de bureau — qui la complète souvent tout seul — mais échoue sur mobile, en ligne de commande et sur les appels d'API. Symptôme classique et déroutant : « ça marche chez moi ».
Le renouvellement est à votre charge. Traefik surveille le répertoire et relit les certificats à chaud : remplacer les deux fichiers suffit, aucun redémarrage n'est nécessaire.
Changer de mode
Modifiez TLS_MODE dans .env, relancez ./install.sh, puis :
docker compose up -d traefik
Le certificat existant est conservé : pour repartir d'un certificat neuf, supprimez traefik/certs/ avant de relancer le script.
Vérifier le certificat servi
echo | openssl s_client -connect VOTRE_DOMAINE:443 -servername VOTRE_DOMAINE 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Si l'émetteur est votre propre domaine alors que vous attendiez une autorité, c'est le certificat par défaut qui est servi : l'obtention a échoué. Dans l'ordre — le DNS (dig +short VOTRE_DOMAINE), l'ouverture des ports, puis docker compose logs traefik | grep -iE 'acme|certificate|error'.
Exploitation courante
docker compose ps # état des services
docker compose logs -f product-server # journaux de l'application
docker compose pull && docker compose up -d # montée de version
docker compose down # arrêt, données conservées
Les données MongoDB et Redis vivent dans des volumes nommés qui survivent aux arrêts et aux montées de version. Une montée de version consiste à changer FOXPLAN_VERSION dans .env, puis à relancer les deux commandes ci-dessus.
Les volumes Docker ne sont pas une sauvegarde : ils disparaissent avec un docker compose down -v. Voir Sauvegarde et restauration.
Sauvegarde et restauration
Ce qu'il faut sauvegarder
| Élément | À sauvegarder | Pourquoi |
|---|---|---|
| MongoDB | Oui | Toute la donnée métier : projets, tâches, budgets, utilisateurs |
Fichiers .env* | Oui | Contiennent des clés sans lesquelles la donnée restaurée est inexploitable |
| Redis | Non | Cache et sessions : se reconstruit seul, au prix d'une reconnexion des utilisateurs |
| Pièces jointes | Séparément | Stockées dans le stockage objet S3 configuré par FILE_S3_*, pas dans MongoDB |
.env_application avec la baseSECRET_KEY_JWT sert aussi, par défaut, de clé de chiffrement des secrets SSO stockés en base. Restaurer un dump MongoDB sans cette clé rend les configurations SSO indéchiffrables : elles sont là, illisibles, et à ressaisir intégralement. Une sauvegarde de base sans les secrets n'est pas une sauvegarde complète.
Sauvegarde
La commande s'exécute dans le conteneur MongoDB, qui embarque les outils nécessaires :
docker compose exec -T mongo sh -c \
'mongodump --username "$MONGO_INITDB_ROOT_USERNAME" \
--password "$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase admin --archive --gzip' \
> foxplan-$(date +%F-%H%M).archive.gz
Trois détails qui comptent :
- Les identifiants sont lus depuis l'environnement du conteneur, jamais écrits sur la ligne de commande — ils n'apparaissent donc ni dans
psni dans l'historique du shell. Un mot de passe contenant espaces ou caractères spéciaux passe ainsi sans échappement. --archiveproduit un seul fichier plutôt qu'une arborescence : plus simple à transférer, à chiffrer et à vérifier.-Tdésactive l'allocation d'un pseudo-terminal, faute de quoi le flux binaire serait corrompu à la redirection.
La sauvegarde se fait à chaud, sans arrêter l'application.
Restauration
docker compose exec -T mongo sh -c \
'mongorestore --username "$MONGO_INITDB_ROOT_USERNAME" \
--password "$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase admin --archive --gzip --drop' \
< foxplan-2026-09-15-0300.archive.gz
--drop remplace chaque collection par la version sauvegardée. Sans cette option, la restauration fusionnerait avec les données présentes, ce qui donne un état incohérent — c'est rarement ce que l'on veut.
Redis conserve des objets qui référencent les identifiants de l'ancienne base. Après une restauration, ces références ne correspondent plus à rien et l'application renvoie des erreurs 500 déroutantes — typiquement un objet « introuvable » alors qu'il est bien présent en base.
docker compose exec redis sh -c 'redis-cli -a "$REDIS_PASSWORD" FLUSHALL'
docker compose restart product-server
C'est l'oubli le plus fréquent après une restauration.
Automatiser
Une tâche cron quotidienne, avec rotation sur 30 jours :
# /etc/cron.d/foxplan-backup
0 3 * * * root cd /opt/foxplan && \
docker compose exec -T mongo sh -c 'mongodump --username "$MONGO_INITDB_ROOT_USERNAME" --password "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --archive --gzip' \
> /var/backups/foxplan/foxplan-$(date +\%F).archive.gz 2>/var/log/foxplan-backup.log && \
find /var/backups/foxplan -name 'foxplan-*.archive.gz' -mtime +30 -delete
Le % doit être échappé en \% dans une table cron, sans quoi la ligne est tronquée à cet endroit.
Une archive posée sur le serveur qu'elle sauvegarde disparaît avec lui. Transférez-la vers un stockage distinct — stockage objet, serveur de sauvegarde, bande. C'est la moitié du travail, et c'est celle qu'on oublie.
Vérifier que la sauvegarde est exploitable
Une sauvegarde jamais restaurée n'est pas une sauvegarde : c'est une hypothèse. Testez-la périodiquement sur un environnement séparé — jamais sur la production.
# Contrôle rapide de l'intégrité de l'archive, sans rien restaurer
gzip -t foxplan-2026-09-15-0300.archive.gz && echo "archive intègre"
Puis, une à deux fois par an, déroulez une restauration complète sur un serveur de test et connectez-vous à l'application : c'est le seul contrôle qui prouve quelque chose.
Alternative : sauvegarde à froid du volume
Pour une copie bit à bit plutôt qu'un export logique, arrêtez la base et archivez son volume.
Son nom est préfixé par le nom du projet Compose, lui-même dérivé du répertoire d'installation : relevez-le plutôt que de le deviner.
docker volume ls | grep mongo_data
Puis :
docker compose stop mongo
docker run --rm -v VOTRE_VOLUME:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/mongo-volume-$(date +%F).tar.gz -C /data .
docker compose start mongo
Plus rapide sur de gros volumes, mais avec deux contreparties : l'application est interrompue pendant l'opération, et l'archive n'est restaurable que sur une version de MongoDB identique. Le mongodump reste la méthode à privilégier ; réservez celle-ci aux migrations de serveur.
Mode maintenance
Une page de maintenance activable, qui laisse /docs accessible pendant l'intervention, est fournie avec le package de déploiement accompagné. Elle n'est pas incluse dans les fichiers téléchargeables ci-dessus. Contactez le support pour l'obtenir.
Dépannage
| Symptôme | Piste |
|---|---|
manifest unknown au démarrage | FOXPLAN_VERSION inexistante, ou docker login non effectué |
pull access denied | Identifiants du registre absents ou expirés |
404 sur / | DOMAIN incorrect, ou application non démarrée — voir les journaux |
| 502 / 503 | L'application démarre encore, ou s'est arrêtée sur une erreur |
| Certificat invalide | DNS, ports, journaux ACME — voir Vérifier le certificat servi |
| Aucun email reçu | MAIL_PROXY_TYPE ou MAIL_SMTP_* absents — voir Envoi des emails |
| Emails reçus mais liens invalides | BASE_URL absent ou différent du domaine public |
| Emails en indésirables | MAIL_FROM non autorisé à émettre pour le domaine (SPF/DKIM) |
| Erreur d'authentification Redis | SPRING_REDIS_PASSWORD différent de REDIS_PASSWORD |
| Échec d'authentification Mongo | URL_MONGO incohérent avec .env_mongo |
| Exports Gantt en échec | Service gantt-export arrêté, ou MSP_SERVICE_ENDPOINT inaccessible |
| Erreurs 500 « objet introuvable » après une restauration | Cache Redis non vidé — voir Restauration |
| Configuration SSO illisible après restauration | SECRET_KEY_JWT différent de celui en vigueur lors du dump |