Docker Compose deployment
This page describes how to deploy FoxPlan on a single server with Docker Compose, behind Traefik, which handles routing and TLS termination.
For a production target with scaling and high availability, see Kubernetes deployment.
Two versions
| Local trial | Enterprise installation | |
|---|---|---|
| For | Evaluating FoxPlan on a workstation | A shared server, production |
| Files | a single docker-compose.yml | docker-compose.yml, install.sh and .env files |
| Configuration | None | Domain, TLS, SMTP, secrets |
| Access | http://localhost:8090, from the workstation only | https:// your domain |
| Emails | Captured by a built-in test mailbox | Sent by your SMTP server |
Both versions use the same images and are installed in two distinct steps:
- Download the images: the only time Internet access is needed, from the server itself or from a transfer machine;
- Setup — local trial or enterprise installation: the stack starts only on the images already present. No registry is contacted; if an image is missing, nothing starts.
The registry address is communicated to you by FoxPlan. Set it in the FOXPLAN_REGISTRY variable, in the .env file placed next to docker-compose.yml — for the trial as for the enterprise installation. Read access is open: no credentials and no docker login are needed.
| Image | Role |
|---|---|
foxplan_prod | Application |
foxplan_prod-docs | This documentation |
foxplan_prod-export | Schedule export (PDF, PNG, Excel, iCal) |
foxplan_prod-msp | MS Project and Primavera import and export |
latest follows the most recent published version; each version also carries an immutable tag, to pin in production. Third-party images (MongoDB, Redis, Traefik…) are pulled from Docker Hub, not from this registry.
Step 1: download the images
Downloading the images is the only time the stack needs Internet access. Once the images are on the server, FoxPlan no longer contacts any registry: neither at startup, nor during operation, nor when a container restarts.
Images to download
The four FoxPlan images above, in the version you deploy, and the following third-party images, all pulled from Docker Hub:
| Image | Local trial | Enterprise installation |
|---|---|---|
traefik:v3.6.4 | Yes | Yes |
mongo:8.2.7 | Yes | Yes (MONGO_IMAGE) |
redis:7.4 | Yes | Yes (REDIS_IMAGE) |
axllent/mailpit:v1.27 | Yes | No: your emails go through your SMTP server |
dxflrs/garage:v2.1.0 | Yes | Only if you add Garage — see File storage |
curlimages/curl:8.14.1 | Yes: initialises Garage on first start | No |
rclone/rclone:1.68.2 | No | Only to back up Garage |
With your own S3 storage, neither Garage, curl nor rclone is needed. If you change MONGO_IMAGE or REDIS_IMAGE in .env, download the image you set there.
Network flows
Outbound flows issued by the Docker engine during the download, all over HTTPS (TCP/443):
| Destination | Role | Images concerned |
|---|---|---|
| FoxPlan registry host | Image index | foxplan_prod, -docs, -export, -msp |
| FoxPlan registry token service | Anonymous access token | same |
| FoxPlan registry object storage | Image layers: the registry redirects downloads to this storage | same |
registry-1.docker.io, auth.docker.io | Docker Hub: image index and access token | Third-party images |
production.cloudflare.docker.com, production.cloudfront.docker.com | Docker Hub image layers (CDN) | same |
app.fox-plan.com | Download of the deployment files on this page | Local trial (curl) |
If your firewall filters by image (registry proxy, Harbor, Nexus or Artifactory allow-list…), allow the names and versions above.
The three FoxPlan registry hosts are communicated to you along with its address. A firewall that only allows the registry domain lets the index through but blocks the layers: the pull then hangs or fails on a timeout. The redirects to the object storage and the CDN must be allowed as well.
The operational flows — ACME certificate authority, SMTP server, S3 storage, SSO providers — are described in the network flow matrix.
From the server
If the server can reach the registries for the duration of the download:
FOXPLAN_REGISTRY=<address communicated by FoxPlan>
V=latest # in production: the version pinned in FOXPLAN_VERSION
IMAGES=(traefik:v3.6.4 mongo:8.2.7 redis:7.4)
for i in foxplan_prod foxplan_prod-docs foxplan_prod-export foxplan_prod-msp; do
IMAGES+=("$FOXPLAN_REGISTRY/$i:$V")
done
# Local trial: add the test mailbox and the built-in storage.
# IMAGES+=(axllent/mailpit:v1.27 dxflrs/garage:v2.1.0 curlimages/curl:8.14.1)
# Enterprise installation with Garage:
# IMAGES+=(dxflrs/garage:v2.1.0 rclone/rclone:1.68.2)
for i in "${IMAGES[@]}"; do
case "$i" in
*-export:*|*-msp:*) docker pull --platform linux/amd64 "$i" ;;
*) docker pull "$i" ;;
esac || echo "ÉCHEC : $i"
done
The foxplan_prod-export and foxplan_prod-msp images exist for amd64 only: the loop always pulls them for that architecture. On an ARM server or an Apple Silicon Mac, they run under emulation. An ÉCHEC line flags an image that was not downloaded: fix it before moving on to step 2.
Access to the registries can then be closed again: step 2 no longer needs it.
From an independent environment
If your organization belongs to a sensitive sector — defense, healthcare, energy, finance, public administration, operator of vital importance — do not open these flows from the production server or from your internal network.
Retrieve the images on a machine independent of your information system (a dedicated workstation or transfer environment, with no access to your internal networks), check them, then bring them in through your usual channel: internal registry, or an archive transferred on controlled media.
# 1. On the transfer machine, outside your information system.
# Build IMAGES exactly as above (trial or enterprise, with or without Garage):
# an image forgotten here will be missing at startup.
# --platform: the architecture of your target server (amd64 in most cases).
for i in "${IMAGES[@]}"; do docker pull --platform linux/amd64 "$i"; done
docker save -o foxplan-images.tar "${IMAGES[@]}"
sha256sum foxplan-images.tar > foxplan-images.tar.sha256
# 2. Checks according to your policy: antivirus, vulnerability scanning
# (Trivy, Grype…), image digests (docker images --digests).
# 3. On the target server, without Internet access.
sha256sum -c foxplan-images.tar.sha256
docker load -i foxplan-images.tar
The loaded images keep their original name: step 2 finds them locally. If you go through an internal registry, retag them to it (docker tag, docker push), pull them from the server, and set this registry in FOXPLAN_REGISTRY, MONGO_IMAGE and REDIS_IMAGE. The production server then has no Internet flow at all for its images.
Step 2: setup
Both versions start with --pull never: Docker Compose uses the images present on the machine and contacts no registry. If one is missing, the stack does not start and the missing image is named: redo step 1 with the right list, then start again.
Without this option, a missing image would be downloaded silently — or, on a server without Internet access, startup would hang on a timeout.
Local trial
On a workstation with Docker Desktop — or Docker and Docker Compose v2 — a single file is enough. The only value to provide is the registry address, communicated by FoxPlan:
mkdir foxplan-essai && cd foxplan-essai
curl -O https://app.fox-plan.com/docs/onpremise/essai/docker-compose.yml
echo "FOXPLAN_REGISTRY=<address communicated by FoxPlan>" > .env
# Checks that all the images are present, then starts without downloading anything.
MISSING=$(docker compose config --images | while read -r i; do
docker image inspect "$i" >/dev/null 2>&1 || echo "$i"
done)
if [ -n "$MISSING" ]; then echo "Missing images, nothing was started:"; echo "$MISSING"
else docker compose up -d --pull never; fi
Without FOXPLAN_REGISTRY, docker compose stops with a message asking for it. The preliminary check stops everything before a single container is created; --pull never alone would also refuse to download, but only after creating part of the stack.
Startup takes about a minute. Then:
| Address | Content |
|---|---|
http://localhost:8090 | FoxPlan — on first launch, the diagnostic page, then the creation of your account |
http://localhost:8090/mailpit/ | The emails sent by FoxPlan, including the sign-up code |
http://localhost:8090/docs/ | This documentation |
No email leaves the workstation: a test mailbox, Mailpit, captures them all.
The trial ships a file storage, Garage: project documents, client contract attachments and profile photos work without any configuration. On an Apple Silicon Mac, the two export services run under amd64 emulation: this is expected, and a little slower.
Only the two export services need to run as amd64. If the other images were pulled or loaded as amd64 — for example from an archive prepared for an amd64 server — MongoDB does not start: from version 5 on, it requires AVX instructions, which emulation does not provide. The mongo log then shows "MongoDB 5.0+ requires a CPU with AVX support", the application fails with MongoTimeoutException and http://localhost:8090 returns 502.
A plain docker pull fixes nothing: the amd64 image already present is considered up to date. Explicitly request the arm64 version, then restart:
. ./.env # FOXPLAN_REGISTRY
for i in traefik:v3.6.4 mongo:8.2.7 redis:7.4 \
$FOXPLAN_REGISTRY/foxplan_prod:latest $FOXPLAN_REGISTRY/foxplan_prod-docs:latest; do
docker pull --platform linux/arm64 "$i"
done
docker compose up -d --pull never
Create the first account
The trial sends no real email: the sign-up code is read in Mailpit.
- Open
http://localhost:8090: a diagnostic page opens (First setup screen) and shows you whether everything works correctly, without asking you anything — see what is checked. - If everything responds, click Create the first instance with my account and enter an email address whose domain exists — your work address, for example; a made-up domain is refused. The message stays in Mailpit: nothing leaves the workstation.
- In another tab, open
http://localhost:8090/mailpit/: the message containing the sign-up code arrives there within a few seconds. - Go back to FoxPlan and enter this code. The wizard then walks you through your profile, your company, the modules and the invitations, and finally asks you to choose your password.
This first account is the administrator of the instance. Invitations and password resets sent afterwards also arrive in Mailpit.
The trial listens on port 8090 rather than 80: macOS does not let a non-administrator user open port 80 on the 127.0.0.1 address alone, and Docker then shows no error — the page simply does not respond.
Port 8090 already taken? Choose another one at startup:
FOXPLAN_PORT=8091 docker compose up -d --pull never
then open http://localhost:8091.
To stop everything: docker compose down (data kept), or docker compose down -v to start from scratch.
To update the trial, redo step 1, then run docker compose up -d --pull never again. A docker compose up -d without the option downloads the latest version of the FoxPlan images itself: handy on a connected workstation, but step 2 is then no longer offline.
The trial data is attached to the project name foxplan-essai, not to the folder: a new folder resumes the existing trial, with its accounts. To start from scratch, use docker compose down -v.
The trial only listens on 127.0.0.1 — it can only be reached from the workstation itself — and uses well-known passwords, written in the file. Do not expose it on a network and do not enter real data into it: to welcome users, move to the enterprise installation below.
Enterprise installation
Files to download:
- docker-compose.yml — the complete stack. This file does not need to be modified.
- install.sh — prepares the deployment: certificate, Traefik configuration, consistency checks; checks the images then starts the stack
- env.example — the infrastructure variables, to save as
.env - env_application.example — the application variables, to save as
.env_application - env_mongo.example and env_redis.example — the MongoDB and Redis credentials, to save as
.env_mongoand.env_redis
Place the files in the same directory. All configuration happens in the .env* files: docker-compose.yml stays untouched, which makes upgrades painless.
The rest of this page covers the enterprise installation: architecture, prerequisites, sizing, then the installation procedure itself.
Architecture
Internet or internal network
(DNS: DOMAIN → this server)
│ 80 / 443
▼
┌─────────┐ TLS termination
│ Traefik │ redirect 80 → 443
└────┬────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
/ (app:5000) /docs (docs:8080) /gantt-export:3200
product-server docs gantt-export
│ │
├──► mongo (internal network) └──► gantt-msp (internal)
└──► redis (internal network)
| Service | Role | Required |
|---|---|---|
product-server | FoxPlan application | Yes |
docs | This documentation, served under /docs | No |
gantt-export | Schedule export to PDF / PNG / Excel / iCal | No |
gantt-msp | MS Project and Primavera import and export | No |
mongo | Database | Yes |
redis | Cache and shared sessions | Yes |
| S3 storage | Files: documents, attachments, profile photos — yours or Garage, see File storage | No |
Without gantt-export, the application works normally: only the graphical Gantt exports are unavailable. Without S3 storage, only the files are.
Prerequisites
- A Linux server with Docker and Docker Compose v2;
- A domain name whose DNS record points to this server;
- Ports 80 and 443 reachable by your users — no others, SSO included: see Ports and SSO;
- The images, downloaded in step 1: from the server, with temporary outbound access to the FoxPlan registry and Docker Hub, or imported from a transfer environment;
- Your license;
- For files, an S3-compatible storage — yours or Garage, see File storage;
- Depending on the chosen TLS mode, outbound access to the certificate authority (see below).
Ports and SSO
Enterprise SSO requires no additional port, whether it is the standard Microsoft or Google sign-in or a custom SSO. The return of a sign-in is carried by the user's browser to https://YOUR_DOMAIN/login/oauth2/code/…, on the same port 443 as the rest of the application: the identity provider never opens a connection to your server.
| Direction | Port | Between | Purpose |
|---|---|---|---|
| Inbound | 443 (TCP) | User workstations → Traefik | The application and the SSO return |
| Inbound | 80 (TCP) | User workstations → Traefik | Redirect to 443; HTTP-01 challenge in acme mode |
| Outbound | 443 (TCP) | product-server container → identity provider | Token exchange, signing keys, profile — domains in the network flow matrix |
Do not publish the application's port 5000: everything goes through Traefik, which terminates TLS. If your identity provider listens on a port other than 443 — Keycloak on 8443, for example — that is the port to open outbound, from the server to the provider.
Standard SSO: Microsoft and Google
Enabled by the TENANTID_AZURE, CLIENT_ID_AZURE, GOOGLE_CLIENT_ID… variables in .env_application. The redirect URL is built from BASE_URL:
https://YOUR_DOMAIN/login/oauth2/code/azure
https://YOUR_DOMAIN/login/oauth2/code/google
Declare it exactly as is in the application registered with Microsoft Entra or Google. Microsoft and Google only accept https URLs.
Custom SSO: configured in the application
Okta, Keycloak, Microsoft, Google or any OIDC provider declared in Enterprise SSO. The redirect URL is then rebuilt from the incoming request: Traefik passes the original address to the application — X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Port headers — and FoxPlan derives https://YOUR_DOMAIN/login/oauth2/code/<identifier> from it. The Check configuration button shows the exact URL to declare.
Traefik replaces the X-Forwarded-* headers with what it sees itself. If users reach FoxPlan through another public port (:8443), or if a device placed in front of Traefik terminates TLS and forwards plain HTTP to it, FoxPlan computes a redirect URL that differs from the browser's: the provider rejects the sign-in (redirect_uri mismatch). A load balancer placed in front of Traefik must therefore forward traffic over HTTPS on port 443, or as TCP without decrypting it, keeping the original domain name.
Sizing
The values below are slightly above what FoxPlan measures in operation, to keep some headroom on the main components.
For the local trial
| Resource | Recommended |
|---|---|
| Processor | 4 vCPU |
| Memory allocated to Docker | 8 GB (6 GB minimum) |
| Disk | 15 GB free (images included) |
When idle, the trial uses about 1.5 GB, of which 1.1 GB for the application. In Docker Desktop, the allocated memory is set in Settings → Resources. The trial sets no per-container limit: it adapts to the memory given to Docker.
For the enterprise installation
| Users | Processor | Memory | Disk (SSD) |
|---|---|---|---|
| Up to 100 | 4 vCPU | 16 GB | 100 GB |
| 100 to 500 | 8 vCPU | 32 GB, with 2 application instances | 200 GB |
| Beyond | See Kubernetes deployment |
The disk includes the database, the files stored in Garage if it runs on the same server, and room for local backups before they are transferred. To place the database on a dedicated disk, see Volumes and disks.
Container memory limits
The enterprise docker-compose.yml sets a memory limit per container. Without a limit, the application's JVM and MongoDB each size themselves on all of the server's memory — a quarter for the JVM, half for the MongoDB cache — and end up competing for it. With a limit, each adapts to its own.
| Container | Default limit | Variable (.env) |
|---|---|---|
product-server (application) | 6 GB | FOXPLAN_MEM_APP |
mongo | 4 GB | FOXPLAN_MEM_MONGO |
gantt-export | 2 GB | FOXPLAN_MEM_EXPORT |
redis | 1 GB | FOXPLAN_MEM_REDIS |
gantt-msp | 512 MB | FOXPLAN_MEM_MSP |
docs | 256 MB | FOXPLAN_MEM_DOCS |
traefik | 256 MB | FOXPLAN_MEM_TRAEFIK |
That is 14 GB, for a 16 GB server: enough is left for the operating system. On a 32 GB server, double FOXPLAN_MEM_APP and FOXPLAN_MEM_MONGO. The application uses 75% of its limit for its Java heap; MongoDB automatically reserves half of its own, minus 1 GB, for its cache.
The processor is not limited: application startup and export generation benefit from it, and a limit would slow them down without protecting anything.
Several application instances
For a large number of users, run several instances of the application on the same server. Traefik detects them and spreads requests across them; sessions are shared in Redis, so a user can move from one instance to another without being logged out. Scheduled tasks (reports, routines, synchronisations) take a lock in Redis: only one instance runs them, however many there are.
In .env:
FOXPLAN_APP_REPLICAS=2
then docker compose up -d. Each instance has its own FOXPLAN_MEM_APP limit: the memory required grows accordingly. Example for a 32 GB server with 2 instances:
FOXPLAN_APP_REPLICAS=2
FOXPLAN_MEM_APP=6g # per instance: 12 GB in total
FOXPLAN_MEM_MONGO=8g
That is about 24 GB of limits in total, with the other containers: there is still headroom for the system.
When should you add an instance? When the application container's processor stays saturated at peak hours (docker stats), in practice beyond a hundred or so users active at the same time. A second instance also keeps the service available if one of them restarts.
Two limits, specific to a single server:
- an upgrade recreates all instances at the same time: the interruption is short, but it exists;
- MongoDB remains a single node, on the same server.
Beyond 500 users, or for real high availability (several servers, rolling deployment without interruption), move to Kubernetes deployment.
Volumes and disks
The MongoDB database and the files stored in Garage are kept in host directories, never in Docker volumes. A Docker volume disappears with docker compose down -v or a docker system prune --volumes; a host directory does not. Placed on a dedicated disk, it also survives a reinstallation of Docker or of the system.
| Data | In the container | Location on the host | Variable (.env) |
|---|---|---|---|
| MongoDB database | /data/db | Directory, mandatory | FOXPLAN_MONGO_DATA |
| Files in Garage (optional) | /var/lib/garage/meta, /var/lib/garage/data | Directory, meta and data subfolders | FOXPLAN_GARAGE_DATA |
| Redis (cache, sessions) | /data | redis_data Docker volume, or a directory | FOXPLAN_REDIS_DATA (optional) |
| Traefik certificates | /certs, /acme | ./traefik/ folder of the installation | — |
Without FOXPLAN_MONGO_DATA, docker compose refuses to start, with a message asking for it. The env.example suggests ./data/mongo, in the installation folder: that is fine for a pilot. In production, put the database on a dedicated disk, as described below. Redis can stay in a Docker volume: it only holds the cache and the sessions, which rebuild themselves (if lost, they only log users out).
Mounting a dedicated disk for the database
The following commands run on the server, as administrator, before the first startup of FoxPlan. They assume an additional empty disk, on SSD; a storage array volume (SAN over iSCSI or Fibre Channel) is also suitable. Avoid NFS or SMB network shares: MongoDB loses performance and reliability on them.
1. Identify the disk. It appears without a partition or mount point, for example sdb:
lsblk -f
2. Format it as XFS, the file system recommended by MongoDB (xfsprogs package). This command erases the disk: double-check its name.
sudo mkfs.xfs -L foxplan-data /dev/sdb
3. Mount it permanently, by its identifier (UUID), which does not change if the order of the disks changes at reboot:
sudo mkdir -p /srv/foxplan
UUID=$(sudo blkid -s UUID -o value /dev/sdb)
echo "UUID=$UUID /srv/foxplan xfs defaults,noatime 0 2" | sudo tee -a /etc/fstab
sudo mount -a
findmnt /srv/foxplan # must show /dev/sdb and xfs
4. Make Docker wait for the disk to be mounted. Without this precaution, if the disk is not mounted when the server starts, /srv/foxplan/mongo is just an empty folder on the system disk: MongoDB starts an empty database there, and FoxPlan appears to have lost everything.
sudo mkdir -p /etc/systemd/system/docker.service.d
printf '[Unit]\nRequiresMountsFor=/srv/foxplan\n' | sudo tee /etc/systemd/system/docker.service.d/foxplan-data.conf
sudo systemctl daemon-reload
Docker then does not start as long as /srv/foxplan is not mounted.
5. Set the directories in .env:
FOXPLAN_MONGO_DATA=/srv/foxplan/mongo
FOXPLAN_GARAGE_DATA=/srv/foxplan/garage # only if you add Garage
6. Prepare and start. ./install.sh creates the directories (with meta and data for Garage); the MongoDB image sets their permissions itself.
./install.sh
docker compose up -d
7. Check that the database really writes to the dedicated disk:
docker inspect foxplan-mongo --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'
# /srv/foxplan/mongo -> /data/db
sudo ls /srv/foxplan/mongo # WiredTiger* files, collection-*…
df -h /srv/foxplan # free space on the data disk
Then monitor free space: MongoDB stops dead when it runs out of space, and the application with it.
Existing installation: moving the database out of a Docker volume
Earlier installations kept the database in the mongo_data Docker volume. After updating the docker-compose.yml, set FOXPLAN_MONGO_DATA then run ./install.sh: if it finds the directory empty while a database exists in the old volume, it stops and displays the copy command to run. In detail:
docker compose stop product-server mongo
mkdir -p /srv/foxplan/mongo
docker run --rm -v YOUR_PROJECT_mongo_data:/from:ro -v /srv/foxplan/mongo:/to alpine \
sh -c 'cp -a /from/. /to/'
./install.sh
docker compose up -d
The exact volume name can be read with docker volume ls | grep mongo_data. Keep the old volume until you have checked the application, then delete it (docker volume rm). Same principle for Garage: copy garage_meta into $FOXPLAN_GARAGE_DATA/meta and garage_data into $FOXPLAN_GARAGE_DATA/data.
Installation
1. Preparation
chmod +x install.sh
./install.sh
On the first run, the script creates .env from env.example and stops so you can fill it in.
| Variable | Description |
|---|---|
DOMAIN | Domain served, for example foxplan.my-company.com |
FOXPLAN_REGISTRY | Image registry address, communicated by FoxPlan |
FOXPLAN_IMAGE | Base name of the images — already set: foxplan_prod |
FOXPLAN_VERSION | Deployed version — pin a specific version in production |
MONGO_IMAGE / REDIS_IMAGE | Validated versions of the dependencies |
FOXPLAN_MONGO_DATA | Directory of the database on the host — preferably a dedicated disk, see Volumes and disks |
TLS_MODE | selfsigned, acme or static — see TLS |
ACME_EMAIL, ACME_CA_SERVER | Certificate authority, if TLS_MODE=acme |
latest in productionFOXPLAN_VERSION=latest follows the most recent published version: handy for a pilot, not recommended afterwards. A simple container restart can then change the version without you deciding it. Pin a version, and perform your upgrades explicitly.
Then run ./install.sh again: it generates the default certificate and the Traefik configuration matching your TLS_MODE. The script is idempotent — you can run it again at will, it never replaces an existing certificate or .env.
2. Application secrets
Three files, to create from the templates downloaded above:
cp env_application.example .env_application # database, emails, S3, JWT…
cp env_mongo.example .env_mongo # MongoDB credentials
cp env_redis.example .env_redis # Redis password
SECRET_KEY_JWT is generated once and for all:
openssl rand -base64 64 | tr -d '\n'
Consistency rules to respect, or startup fails:
URL_MONGOreuses the user and password from.env_mongo, with the hostmongo;SPRING_REDIS_PASSWORDis identical toREDIS_PASSWORD;BASE_URLishttps://followed byDOMAIN— it builds the links contained in emails.
.env is read by Docker Compose to assemble the stack; it is not passed to the application container. Application variables — database, emails, S3, JWT — go into .env_application. An application variable placed in .env silently has no effect.
Sending emails
FoxPlan sends invitations, password resets and notifications. Without configuration, no email is sent: the application starts and works, but these features stay silent.
To fill in .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
The Settings → Company → Emails tab shows, email by email, what your sending server replied, with a green / orange / red banner on the platform status. It is the first place to look when a user says they received nothing — see Email log.
MAIL_PROXY_TYPE also accepts sendgrid or gmail, for an account you provide; MAIL_PROXY_PARAMS then holds <sender>;<key or password>. In the vast majority of installations, smtp is the right choice.
Two points decide the fate of your messages:
MAIL_FROMmust be authorized to send for your domain (SPF, DKIM). A misaligned sender ends up in spam, or is rejected without the application knowing anything about it.BASE_URLbuilds the links in emails. If it is missing or wrong, messages are sent correctly but the links they contain — invitation, reset — lead nowhere.
To check after startup, trigger a password reset from the login screen and follow the logs:
docker compose logs -f product-server | grep -i mail
A sending failure appears there as a warning: the application never stops because of an email that was not sent.
3. Startup
./install.sh --start
The script first checks that all the images of the stack are present on the server. If one is missing, it names it and stops without starting anything: download it (step 1), then run it again. Otherwise, it starts the stack with docker compose up -d --pull never, without contacting any registry.
Verification:
docker compose ps
curl -I https://YOUR_DOMAIN/
curl -I https://YOUR_DOMAIN/docs/
Once logged in as an administrator, the Settings → FoxPlan account → Diagnostics tab tests the database, Redis, email delivery (a test message is sent to you), S3 storage and the export service — see FoxPlan account.
4. Create the first account
Open https://YOUR_DOMAIN: as long as no instance exists, a diagnostic page opens (First setup screen) and shows you whether everything works correctly — see what is checked. If everything responds, click Create the first instance with my account. FoxPlan sends a sign-up code to the address entered, through the SMTP server configured in .env_application: it arrives in the recipient's real inbox. Enter this code, follow the wizard (profile, company, modules, invitations), then choose your password.
This first account is the administrator of the instance: use an address belonging to the team that will run FoxPlan, not a temporary personal account.
Without a working SMTP server, the sign-up code is not sent and no account can be created. This is why the first-setup screen only offers account creation once the sending server has been reached. Check the Sending emails section, then follow the logs during sign-up: docker compose logs -f product-server | grep -i mail.
5. Check the installation: diagnostics
The diagnostics check, for the trial as for the enterprise installation:
| Item | Check |
|---|---|
| Application | The FoxPlan server responds |
| Database | MongoDB responds |
| Cache | Redis responds |
| Email sending server | The SMTP server is reachable (no email is sent) |
| Attachment storage | The S3 bucket is accessible — "not configured" if there is none |
| Schedule export | The gantt-export service responds, tested from your browser |
| Documentation | The docs service responds, tested from your browser |
They can be opened in two ways:
- On first launch, as long as no instance has been created: the First setup screen runs them on its own, then offers to create the first instance if no item is in error. A "not configured" item, such as storage in the trial, does not block.
- Afterwards, at any time: the diagnostics icon, to the right of the language selector on the sign-in page. It only appears on an on-premise installation, and works without being signed in — precisely when nobody manages to sign in.
Without signing in, the diagnostics only show statuses, with no server name or technical message: the sign-in page is visible to anyone who reaches it. The detail of each error, and a real test email, are in the full diagnostics, once signed in: Settings → FoxPlan account → Diagnostics — see FoxPlan account.
File storage
FoxPlan stores its files in an S3-compatible object storage: project documents, client contract attachments, profile photos. The files remain private: the application reads them back and serves them itself, after checking access rights. The storage therefore does not need to be reachable from the Internet, nor to manage public permissions.
Variable (.env_application) | Role |
|---|---|
FILE_S3_URL | Address of the S3 service, for example http://garage:3900 |
FILE_S3_BUCKET | Bucket for the files |
FILE_S3_ACCESS_KEY, FILE_S3_SECRET_KEY | Credentials for the bucket |
FILE_S3_REGION | Region signed into each request — the one your storage expects. Default: fr-par |
Without storage, leave these variables empty: the application works, only the files are unavailable. A file cannot exceed 10 MB. FoxPlan applies no quota on premise: the space is yours.
Your existing storage
Any S3-compatible storage will do: Ceph, NetApp StorageGRID, Scality, MinIO, AWS… Create a bucket and credentials dedicated to FoxPlan, with read, write and delete rights on that bucket, then fill in the variables above.
docker-compose.ymlThe enterprise installation does not include Garage: it connects to the storage you designate. Leave docker-compose.yml as it is and do not create a docker-compose.override.yml; the whole configuration goes into .env_application:
# .env_application — your S3 storage
FILE_S3_URL=https://s3.my-company.local # address of the S3 service, without the bucket name
FILE_S3_BUCKET=foxplan # bucket created for FoxPlan
FILE_S3_ACCESS_KEY=<access key ID>
FILE_S3_SECRET_KEY=<secret key>
FILE_S3_REGION=<region expected by the storage> # e.g. us-east-1, eu-west-3, or that of your Ceph/MinIO
Then apply the configuration: docker compose up -d product-server (the container is recreated with the new values).
- The Docker server must reach the
FILE_S3_URLaddress (DNS, firewall): the application reads and writes the files, never the browser. - Over
https, the storage certificate must be signed by a recognised authority; an unrecognised internal certificate makes the connection fail. - The application addresses the bucket in the path (
https://s3.my-company.local/foxplan/…): no per-bucket DNS is needed. - Check the result in the diagnostics: the Storage line must be green.
The region is part of the signature of every request. If FILE_S3_REGION differs from the one the storage expects, all requests are refused (error 400 in the logs). The diagnostics report it: storage in error.
Garage, if you don't have one
Garage is a lightweight, open-source S3 storage developed in France. The local trial already includes it. To add it to the enterprise installation without touching docker-compose.yml, create next to it a docker-compose.override.yml file, which Docker Compose reads automatically:
services:
garage:
image: dxflrs/garage:v2.1.0
restart: always
volumes:
- ./garage.toml:/etc/garage.toml:ro
# Host directories (FOXPLAN_GARAGE_DATA in .env): they survive "docker compose down -v".
- ${FOXPLAN_GARAGE_DATA:?Set FOXPLAN_GARAGE_DATA in .env}/meta:/var/lib/garage/meta
- ${FOXPLAN_GARAGE_DATA:?Set FOXPLAN_GARAGE_DATA in .env}/data:/var/lib/garage/data
networks:
- foxplan-network
deploy:
resources:
limits:
memory: 1g
Set the files directory in .env, preferably on the data disk (see Volumes and disks), then run ./install.sh again, which creates its meta and data subfolders:
FOXPLAN_GARAGE_DATA=/srv/foxplan/garage
Then create the garage.toml file, with two secrets generated for your installation:
RPC_SECRET=$(openssl rand -hex 32)
ADMIN_TOKEN=$(openssl rand -base64 32)
printf '%s\n' \
'metadata_dir = "/var/lib/garage/meta"' \
'data_dir = "/var/lib/garage/data"' \
'db_engine = "sqlite"' \
'replication_factor = 1' \
'rpc_bind_addr = "[::]:3901"' \
'rpc_public_addr = "127.0.0.1:3901"' \
"rpc_secret = \"$RPC_SECRET\"" \
'' \
'[s3_api]' \
's3_region = "garage"' \
'api_bind_addr = "[::]:3900"' \
'' \
'[admin]' \
'api_bind_addr = "[::]:3903"' \
"admin_token = \"$ADMIN_TOKEN\"" > garage.toml
chmod 600 garage.toml
Start Garage, then prepare it — topology, bucket and access key:
docker compose up -d garage
docker compose exec garage /garage status # note the node ID (first column)
docker compose exec garage /garage layout assign -z dc1 -c 100G <NODE_ID>
docker compose exec garage /garage layout apply --version 1
docker compose exec garage /garage bucket create foxplan
docker compose exec garage /garage key create foxplan # displays "Key ID" and "Secret key"
docker compose exec garage /garage bucket allow --read --write --owner foxplan --key foxplan
-c 100G is the announced capacity of the node: adjust it to the disk space reserved for files. Finally, copy the key into .env_application:
FILE_S3_URL=http://garage:3900
FILE_S3_BUCKET=foxplan
FILE_S3_ACCESS_KEY=<Key ID>
FILE_S3_SECRET_KEY=<Secret key>
FILE_S3_REGION=garage
then restart the application:
docker compose up -d product-server
The diagnostics (icon on the sign-in page, or FoxPlan account → Diagnostics) should show the storage as OK. Garage is only reachable from the compose internal network: no port is published.
In a YAML file, a value made only of digits is read as a number: a key 0000…01 would become 1. If you place a key in a docker-compose file, put it in quotes. In .env_application, the problem does not arise.
Inbound and outbound certificates
FoxPlan uses two different certificates, one per traffic direction. They go neither in the same place nor in the same component.
INBOUND flows browser ──HTTPS──► Traefik ──► application
Traefik presents FOXPLAN'S certificate → traefik/certs/
OUTBOUND flows application ──HTTPS, SMTP TLS──► internal AD FS, SMTP, S3…
the application checks THEIR certificate using → ca-sortant/
the authority that signed them
| Inbound flows | Outbound flows | |
|---|---|---|
| Purpose | Prove to browsers that the server really is DOMAIN | Let the application recognise your internal services |
| Used by | Traefik | The application (Java) |
| Content | The FoxPlan server certificate and its private key | The public certificate of the authority — root, plus intermediates if any. Never a private key |
| Location | traefik/certs/tls.crt and traefik/certs/tls.key | ca-sortant/<name>.crt — one or more files; another directory can be set with FOXPLAN_OUTBOUND_CA_DIR in .env |
| Format | PEM. tls.crt: server certificate followed by its intermediates. tls.key: unencrypted key, no passphrase | PEM, .crt extension required. One file may hold several certificates |
| Required | Yes — install.sh generates a self-signed one until yours is in place | Only if a called service is signed by a non-public authority |
| Taken into account | On the fly, after touch traefik/dynamic/tls.yml — see the static mode | At startup: ./install.sh (checks the files), then docker compose restart product-server |
A PEM file is a text file that only contains -----BEGIN CERTIFICATE----- … -----END CERTIFICATE----- blocks (and, for the key, -----BEGIN PRIVATE KEY-----).
Is it the same certificate?
No. The inbound certificate is FoxPlan's own, together with its private key. The outbound certificate is that of the authority that signed the certificates of AD FS, SMTP or S3: public, with no key.
They do, however, often come from the same PKI. If the internal authority signed both FoxPlan's certificate and AD FS's, its root authority certificate is used:
- in
ca-sortant/, so that the application recognises AD FS, SMTP…; - in
traefik/ca/, only inacmemode with an internal PKI, so that Traefik recognises the ACME server.
Never copy tls.crt or tls.key into ca-sortant/: install.sh rejects any file containing a private key.
Is an outbound authority needed?
From the FoxPlan server, display the issuer of each internal service's certificate:
# AD FS, S3, APIs: HTTPS
echo | openssl s_client -connect adfs.my-company.local:443 -servername adfs.my-company.local 2>/dev/null \
| openssl x509 -noout -issuer
# SMTP with STARTTLS
echo | openssl s_client -connect smtp.my-company.local:587 -starttls smtp 2>/dev/null \
| openssl x509 -noout -issuer
Public issuer (DigiCert, Sectigo, GlobalSign, Let's Encrypt…): nothing to do. Internal issuer: ask the PKI team for the root authority certificate and its intermediates, in PEM format, and drop it into ca-sortant/.
If it is missing, SSO sign-in fails on return from the provider, or emails are not sent, with PKIX path building failed in docker compose logs product-server.
Converting to PEM
| File received | Command |
|---|---|
Binary .cer or .der | openssl x509 -inform DER -in authority.cer -out ca-sortant/authority.crt |
.p7b (authority chain) | openssl pkcs7 -print_certs -in chain.p7b | sed -n '/-----BEGIN/,/-----END/p' > ca-sortant/authority.crt |
.pfx or .p12 (FoxPlan certificate with its key) | openssl pkcs12 -in foxplan.pfx -nokeys | sed -n '/-----BEGIN/,/-----END/p' > traefik/certs/tls.crtopenssl pkcs12 -in foxplan.pfx -nocerts -nodes | sed -n '/-----BEGIN/,/-----END/p' > traefik/certs/tls.key |
The sed removes the descriptive lines (Bag Attributes, subject=…) that openssl adds. In ca-sortant/, they would prevent the application from starting: install.sh detects them and stops, giving the clean-up command. With a .pfx, check that tls.crt starts with the server certificate, before its intermediates.
TLS: three modes
Whatever the mode, install.sh installs a default certificate. Traefik serves it from the first startup, and keeps serving it if automatic issuance fails: the site never goes down for lack of a certificate; at worst it degrades to a browser warning.
The choice is made with the TLS_MODE variable, and comes down to a single question: who issues your certificates, and can Traefik talk to it?
TLS_MODE | Authority | Outbound access | Renewal | Use |
|---|---|---|---|---|
selfsigned | None | None | — | Evaluation, pilot |
acme | Let's Encrypt or your PKI | To the authority | Automatic | Production |
static | Any | None | Your responsibility | Closed network, corporate wildcard |
selfsigned — start immediately
Default mode. install.sh generates a self-signed certificate for your DOMAIN, valid for 825 days. The application is reachable over HTTPS right away, without public DNS or firewall opening.
Browsers will display a warning: this is expected. This mode is for validating the installation, not for exposing the service to users.
acme — automatic renewal
TLS_MODE=acme
ACME_EMAIL=admin@mon-entreprise.fr
ACME_CA_SERVER=https://acme-v02.api.letsencrypt.org/directory
Traefik obtains the certificate through an HTTP-01 challenge and renews it on its own. With Let's Encrypt, two conditions apply: the domain must resolve publicly to this server, and port 80 must be reachable from the Internet — the challenge goes through it, even if all traffic is then redirected to 443.
With your internal PKI, nothing changes but the address. If your authority exposes an ACME service — step-ca, HashiCorp Vault, EJBCA — the protocol is the same (RFC 8555):
ACME_CA_SERVER=https://ca.interne.mon-entreprise.fr/acme/acme/directory
If this authority is private, Traefik does not know it and the negotiation would fail on an unknown certificate. Give it the chain of trust:
cp /path/to/ca-interne.crt traefik/ca/ca-interne.crt
then, in .env:
LEGO_CA_CERTIFICATES=/ca/ca-interne.crt
install.sh checks that the file is present and warns you if it is missing.
This is the preferred mode on a closed network: you keep automatic renewal with no dependency on the Internet, provided only that the server can reach your authority.
Internal PKIs often issue certificates valid for a few days. This has no consequence: Traefik simply renews more often.
static — your certificate, your renewal
For a certificate obtained offline — corporate wildcard, commercial certificate, internal authority without an ACME service. No ACME is configured: Traefik never goes out on the network to obtain a certificate.
Simply overwrite the generated files with your own:
cp your-certificate.crt traefik/certs/tls.crt # full chain
cp your-key.key traefik/certs/tls.key
chmod 600 traefik/certs/tls.key
tls.crt must contain the server certificate followed by its intermediates, in that order. A certificate served without its chain works in a desktop browser — which often completes it on its own — but fails on mobile, on the command line and for API calls. A classic, puzzling symptom: "it works on my machine".
Renewal is your responsibility. Traefik does not watch the certificate files, only its configuration in traefik/dynamic/: after replacing both files, touch that configuration so that it reloads them on the fly, with no restart and no downtime.
touch traefik/dynamic/tls.yml
Otherwise, after the replacement Traefik keeps serving the old certificate until its next restart — or until it expires. If your PKI drops renewed certificates into a directory, the task that copies them into traefik/certs/ must end with this touch.
Changing mode
Change TLS_MODE in .env, run ./install.sh again, then:
docker compose up -d traefik
The existing certificate is kept: to start again from a fresh certificate, delete traefik/certs/ before running the script again.
Check the served certificate
echo | openssl s_client -connect YOUR_DOMAIN:443 -servername YOUR_DOMAIN 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
If the issuer is your own domain when you expected an authority, the default certificate is being served: issuance failed. In order — DNS (dig +short YOUR_DOMAIN), open ports, then docker compose logs traefik | grep -iE 'acme|certificate|error'.
Routine operations
docker compose ps # services status
docker compose logs -f product-server # application logs
./install.sh --start # upgrade, after step 1
docker compose down # stop, data kept
The MongoDB database and the Garage files live in host directories (FOXPLAN_MONGO_DATA, FOXPLAN_GARAGE_DATA, see Volumes and disks): they survive stops, upgrades and docker compose down -v. An upgrade is also done in two steps: download the images of the new version (step 1), change FOXPLAN_VERSION in .env, then run ./install.sh --start again.
A data directory is not a backup: it protects neither against a disk failure nor against an accidental deletion. See Backup and restore.
Backup and restore
What to back up
| Item | Back up | Why |
|---|---|---|
| MongoDB | Yes | All business data: projects, tasks, budgets, users |
.env* files | Yes | Contain keys without which restored data is unusable |
| Redis | No | Cache and sessions: rebuilds itself, at the cost of users logging in again |
| Files | Yes, at the same time as the database | Documents, attachments, profile photos: in the S3 storage (FILE_S3_*), not in MongoDB. With Garage, see Files stored in Garage |
.env_application with the databaseBy default, SECRET_KEY_JWT is also the encryption key for the SSO secrets stored in the database. Restoring a MongoDB dump without this key makes the SSO configurations undecryptable: they are there, unreadable, and must be entered again in full. A database backup without the secrets is not a complete backup.
Backup
The command runs inside the MongoDB container, which ships the necessary tools:
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
Three details that matter:
- Credentials are read from the container's environment, never written on the command line — so they appear neither in
psnor in the shell history. A password containing spaces or special characters thus goes through without escaping. --archiveproduces a single file rather than a directory tree: easier to transfer, encrypt and verify.-Tdisables pseudo-terminal allocation, otherwise the binary stream would be corrupted by the redirection.
The backup is done hot, without stopping the application.
Restore
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 replaces each collection with the backed-up version. Without this option, the restore would merge with the existing data, resulting in an inconsistent state — rarely what you want.
Redis keeps objects that reference identifiers from the old database. After a restore, these references no longer match anything and the application returns puzzling 500 errors — typically an object "not found" even though it is present in the database.
docker compose exec redis sh -c 'redis-cli -a "$REDIS_PASSWORD" FLUSHALL'
docker compose restart product-server
This is the most frequently forgotten step after a restore.
Files stored in Garage
MongoDB references files by their key: back up the database and the files at the same time, so you can restore them together.
Hot, file by file — the preferred method. rclone copies the bucket's content into a directory without stopping Garage; subsequent runs only transfer what has changed. With the credentials from .env_application:
NET=$(docker network ls --format '{{.Name}}' | grep foxplan-network)
KEY=$(grep ^FILE_S3_ACCESS_KEY= .env_application | cut -d= -f2)
SECRET=$(grep ^FILE_S3_SECRET_KEY= .env_application | cut -d= -f2)
mkdir -p /var/backups/foxplan/files
docker run --rm --network "$NET" -v /var/backups/foxplan/files:/backup \
rclone/rclone:1.68.2 sync :s3:foxplan /backup \
--s3-provider Other --s3-endpoint http://garage:3900 --s3-region garage \
--s3-access-key-id "$KEY" --s3-secret-access-key "$SECRET"
To restore, swap source and destination (sync /backup :s3:foxplan), on a Garage prepared as during installation.
Cold, full directory — for a server migration. Stop Garage, archive its directory (FOXPLAN_GARAGE_DATA, with its meta and data subfolders), then restart it:
docker compose stop garage
sudo tar czf garage-$(date +%F).tar.gz -C /srv/foxplan/garage meta data
docker compose start garage
The two subfolders go together: data without its metadata is unusable. As with MongoDB, transfer the backups off the server.
Automate
A daily cron job, with 30-day rotation:
# /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
The % must be escaped as \% in a cron table, otherwise the line is truncated at that point.
An archive stored on the server it backs up disappears with it. Transfer it to separate storage — object storage, backup server, tape. That is half the work, and the half that gets forgotten.
Check that the backup is usable
A backup that has never been restored is not a backup: it is a hypothesis. Test it periodically on a separate environment — never on production.
# Quick archive integrity check, without restoring anything
gzip -t foxplan-2026-09-15-0300.archive.gz && echo "archive OK"
Then, once or twice a year, run a full restore on a test server and log in to the application: it is the only check that proves anything.
Alternative: cold backup of the database
For a bit-for-bit copy rather than a logical export, stop the database and archive its directory (FOXPLAN_MONGO_DATA):
docker compose stop product-server mongo
sudo tar czf mongo-$(date +%F).tar.gz -C /srv/foxplan/mongo .
docker compose start mongo product-server
Faster on a large database, but with two trade-offs: the application is interrupted during the operation, and the archive can only be restored on an identical MongoDB version. mongodump remains the preferred method; keep this one for server migrations.
Maintenance mode
An activatable maintenance page, which keeps /docs accessible during the intervention, is provided with the assisted deployment package. It is not included in the downloadable files above. Contact support to obtain it.
Troubleshooting
| Symptom | Lead |
|---|---|
manifest unknown at startup | FOXPLAN_VERSION does not exist |
pull access denied | Wrong FOXPLAN_REGISTRY or FOXPLAN_IMAGE — the image is called foxplan_prod — or an old docker login refused by the registry: docker logout <registry host> |
required variable FOXPLAN_REGISTRY is missing | Registry address missing from .env: set the value communicated by FoxPlan |
env file .env_mongo not found | The secret files have not been created — see Application secrets |
404 on / | Incorrect DOMAIN, or application not started — see the logs |
| 502 / 503 | The application is still starting, or stopped on an error |
| Invalid certificate | DNS, ports, ACME logs — see Check the served certificate |
| Doubt about an integration | FoxPlan account → Diagnostics: database, Redis, emails, S3 and export tested in one click |
| No email received | MAIL_PROXY_TYPE or MAIL_SMTP_* missing — see Sending emails |
| Emails received but links invalid | BASE_URL missing or different from the public domain |
| Emails in spam | MAIL_FROM not authorized to send for the domain (SPF/DKIM) |
| Redis authentication error | SPRING_REDIS_PASSWORD different from REDIS_PASSWORD |
| Mongo authentication failure | URL_MONGO inconsistent with .env_mongo |
| Gantt exports failing | gantt-export service stopped, or MSP_SERVICE_ENDPOINT unreachable |
| 500 "object not found" errors after a restore | Redis cache not flushed — see Restore |
| SSO configuration unreadable after a restore | SECRET_KEY_JWT different from the one in use at dump time |
Storage in error in the diagnostics, 400 on S3 requests | FILE_S3_REGION differs from the storage's region (s3_region for Garage) — see File storage |
| Storage in error right after a Garage key change | The key in .env_application is not Garage's: docker compose exec garage /garage key info foxplan --show-secret |
| "File too large" | A file exceeds 10 MB |
| "Storage quota reached" | SaaS only: the instance has used up its space; a license can extend it |
no matching manifest for linux/arm64 | Export images are published for amd64 only: keep platform: linux/amd64 on gantt-export and gantt-msp (Docker Desktop emulates them) |
The application restarts in a loop, Client id of registration 'google' must not be empty in the logs | Image older than October 2026 with Microsoft or Google SSO lines declared but empty in .env_application: update the image, or comment out these lines |
| FoxPlan starts again without data after a server restart | Data disk not mounted: MongoDB started on the empty folder of the system disk. findmnt /srv/foxplan, then step 4 of Volumes and disks |
required variable FOXPLAN_MONGO_DATA is missing a value | Variable missing from .env: see Volumes and disks |
MongoDB empty after setting FOXPLAN_MONGO_DATA | Data left in the old Docker volume: see Existing installation |
The application restarts in a loop, https://${FILE_S3_URL} in the logs | FILE_S3_* variables missing from .env_application: declare them, even empty — attachments will then be unavailable |
PKIX path building failed in the application logs | Internal authority of AD FS, SMTP or S3 missing from ca-sortant/: see Inbound and outbound certificates |