Kubernetes deployment
This page describes deploying FoxPlan on a Kubernetes cluster with its Helm chart. All configuration goes through a values file: the chart templates never need to be modified.
For a single server, without a cluster, see Docker Compose deployment.
Files to download
- foxplan-1.0.0.tgz — the Helm chart
- values.yaml — all values, with comments
curl -O https://app.fox-plan.com/docs/onpremise/kubernetes/foxplan-1.0.0.tgz
curl -o foxplan-values.yaml https://app.fox-plan.com/docs/onpremise/kubernetes/values.yaml
FoxPlan images are published on a freely accessible registry: no credentials to request, no imagePullSecrets to create. Its address is communicated to you by FoxPlan along with your license; it goes into the image.registry value.
Architecture
| Component | Kubernetes object | Role |
|---|---|---|
| Application | Deployment foxplan-app | FoxPlan server (port 5000), one or more replicas |
| Documentation | Deployment foxplan-docs | This documentation, served under /docs |
| Schedule export | Deployment foxplan-export | PDF, PNG, Excel, MS Project and Primavera, served under /gantt-export |
| MongoDB | StatefulSet foxplan-mongodb | Database, one node, persistent volume |
| Redis | StatefulSet foxplan-redis | Sessions and cache, persistent volume. Always deployed: FoxPlan does not start without it |
| Garage (optional) | StatefulSet foxplan-garage | S3 file storage, if you do not have one |
| Routing | Ingress foxplan | / → application, /docs → documentation, /gantt-export → export |
Names are prefixed with the Helm release name (foxplan in the examples). The export pod embeds a small nginx proxy that strips the /gantt-export prefix: routing therefore works with any Ingress controller, without a rewrite annotation specific to one of them.
Prerequisites
- A Kubernetes 1.27 or later cluster, and Helm 3.
- An Ingress controller: Traefik, ingress-nginx, HAProxy, the one from your managed offering…
- A default storage class (or a class named in the values) for the MongoDB, Redis and, where applicable, Garage volumes.
- At least one amd64 node: the export images are published for amd64 only. The other images also exist for arm64.
- A domain name pointing to your Ingress controller, and a TLS certificate: issued by cert-manager, or provided by you in a Secret.
- An SMTP server: sign-up codes, invitations and password resets depend on it. Without it, nobody can log in.
- For autoscaling only: metrics-server.
Image retrieval
The network flows are those of the Docker Compose installation, described in Step 1: download the images — network flows: the FoxPlan registry, and Docker Hub for third-party images. They originate from the cluster nodes, when they pull the images. Third-party images used by the chart:
| Image | Used |
|---|---|
mongo:8.2.7 | Always, except with an external MongoDB (mongodb.enabled: false) |
redis:7.4 | Always |
nginxinc/nginx-unprivileged:1.28-alpine | Export pod proxy |
dxflrs/garage:v2.1.0, curlimages/curl:8.14.1 | Only with garage.enabled: true |
Retrieve the images from an environment independent of your information system, as described for Docker Compose, then push them to your internal registry. Then reference it in the values: image.registry for the FoxPlan images, mongodb.image, redis.image, export.proxy.image, garage.image and garage.initImage for the others. If this registry requires authentication, add its Secret to imagePullSecrets. The nodes then have no flow to the Internet at all.
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://<host>/login/oauth2/code/…, through the Ingress, like the rest of the application: the identity provider never opens a connection to the cluster.
| Direction | Port | Between | Purpose |
|---|---|---|---|
| Inbound | 443 (TCP) | User workstations → Ingress controller | The application and the SSO return |
| Inbound | 80 (TCP) | User workstations → Ingress controller | Redirect to 443; cert-manager HTTP-01 challenge |
| Outbound | 443 (TCP) | foxplan-app pods → identity provider | Token exchange, signing keys, profile — domains in the network flow matrix |
Do not expose the pods' port 5000 (NodePort, LoadBalancer): the Ingress is enough. If your NetworkPolicy objects filter egress, allow the foxplan-app pods to reach the identity provider, on 443 or on its own port — Keycloak on 8443, for example.
- Standard SSO (Microsoft, Google): variables passed through
app.extraEnvandapp.extraSecretEnv. The redirect URL is built fromhost:https://<host>/login/oauth2/code/azureor…/google, to be declared exactly as is with Microsoft Entra or Google. - Custom SSO (Okta, Keycloak, any OIDC provider, configured in Enterprise SSO): the redirect URL is rebuilt from the
X-Forwarded-Proto,X-Forwarded-HostandX-Forwarded-Portheaders set by the Ingress controller. The Check configuration button shows the exact URL to declare.
If a load balancer in front of the Ingress controller terminates TLS, the controller receives HTTP and passes on X-Forwarded-Proto: http: FoxPlan then computes an http:// URL, which the provider rejects (redirect_uri mismatch). Have the load balancer's headers relayed — use-forwarded-headers: "true" in the ingress-nginx ConfigMap, forwardedHeaders.trustedIPs on the Traefik entry point — or keep the load balancer in TCP mode, without decrypting. The public port must remain 443.
Sizing
Default chart requests (requests) and limits (limits):
| Component | CPU requested | Memory requested | Memory limit | Volume |
|---|---|---|---|---|
| Application (per replica) | 1 | 4 Gi | 6 Gi | — |
| MongoDB | 0.5 | 2 Gi | 4 Gi | 50 Gi |
| Redis | 0.1 | 256 Mi | 1 Gi | 5 Gi |
| Export (3 containers) | 0.36 | 656 Mi | 2.6 Gi | — |
| Documentation | 0.01 | 32 Mi | 256 Mi | — |
| Garage (optional) | 0.1 | 256 Mi | 1 Gi | 1 Gi + 100 Gi |
Guidelines for the available capacity on the nodes:
| Users | CPU | Memory | Disk |
|---|---|---|---|
| Up to 100 | 4 vCPU | 16 GB | 100 GB |
| 100 to 500 | 8 vCPU | 32 GB, with 2 application replicas | 200 GB |
| Beyond | Application autoscaling, MongoDB as a replica set |
The application JVM takes 75% of its memory limit (app.javaOptions): to give it more memory, raise app.resources.limits.memory, with no other setting.
Installation
1. Namespace and certificate
kubectl create namespace foxplan
With cert-manager, there is nothing to create: the issuer annotation in the values is enough (see the next step). Otherwise, create the domain's TLS Secret, including the full chain:
kubectl -n foxplan create secret tls foxplan-tls --cert=fullchain.pem --key=privkey.pem
2. Values
Start from the downloaded values.yaml, or from a file containing only what changes. The minimum:
# foxplan-values.yaml
image:
registry: <address communicated by FoxPlan>
tag: latest # pin the version deployed in production
host: foxplan.mon-entreprise.fr
ingress:
className: traefik # or nginx, haproxy… empty = cluster default class
annotations:
cert-manager.io/cluster-issuer: letsencrypt # if you use cert-manager
app:
replicas: 1 # number of application instances — see "Number of instances"
mail:
from: foxplan@mon-entreprise.fr
host: smtp.mon-entreprise.fr
port: 587
username: foxplan@mon-entreprise.fr
password: <SMTP password>
Without image.registry, host, mail.from or mail.host, Helm refuses the installation with a message naming the missing value.
By default, ingress-nginx limits uploads to 1 MB, which blocks attachments. Add these annotations:
ingress:
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# Large headers in SSO responses (SAML, OIDC)
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
Traefik imposes no limit: no annotation is needed.
3. Deployment
helm upgrade --install foxplan foxplan-1.0.0.tgz -n foxplan -f foxplan-values.yaml
kubectl -n foxplan get pods -w
The first start pulls the images and initializes the database: allow a few minutes before the foxplan-app pod is ready.
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. The sign-up code is sent through your SMTP server; enter it, follow the wizard, then choose your password.
Afterwards, at any time, the diagnostic icon to the right of the language selector, on the login page, reopens these diagnostics.
Main values
| Value | Default | Role |
|---|---|---|
image.registry | — | FoxPlan registry address. Required |
image.tag | latest | FoxPlan image version |
image.pullPolicy | Always | With latest, pulls the image again at each pod start |
host | — | Domain name. Required |
ingress.className, ingress.annotations | — | Ingress controller and its settings |
ingress.tls.secretName | foxplan-tls | Domain TLS Secret |
mail.* | — | SMTP server. Required |
s3.* | empty | Your S3 storage — see File storage |
app.replicas | 1 | Application replicas |
autoscaling.enabled | false | CPU-based autoscaling |
app.extraEnv, app.extraSecretEnv | — | Additional variables: Microsoft or Google SSO… |
mongodb.enabled, mongodb.externalUri | true | Built-in MongoDB, or your own |
export.amd64Only | true | Places the export pod on an amd64 node |
garage.enabled | false | Built-in S3 storage |
secrets.* | generated | JWT key and passwords — see Secrets |
The downloaded values.yaml file documents each value.
File storage
Project documents, contract attachments and profile photos are kept in an S3-compatible object storage. They remain private there: the application serves them itself, after checking access rights.
Your existing storage — Ceph, NetApp StorageGRID, Scality, MinIO, AWS… Create a bucket and credentials dedicated to FoxPlan, then:
s3:
url: https://s3.mon-entreprise.local # without the bucket name
bucket: foxplan
accessKey: <access key ID>
secretKey: <secret key>
region: <region expected by the storage>
The region is part of each request's signature: if it differs from the storage's, all requests are refused and the diagnostics show the storage in error. The bucket is addressed in the URL path: no per-bucket DNS is needed.
Garage, if you do not have one — garage.enabled: true deploys Garage, an open source S3 storage developed in France. A Job creates the FoxPlan bucket and access key at installation; the credentials are generated and placed in the Secret. The s3 section is then ignored. Garage runs on a single node: back it up (see Backup).
Without storage, the application works; only files are unavailable. No quota is applied on premise.
Number of instances
The number of application instances is set in your values file, in one of two ways.
Fixed number — app.replicas:
app:
replicas: 3
Automatic — based on CPU load, between a minimum and a maximum (requires metrics-server). app.replicas is then ignored:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
Apply with helm upgrade (see Upgrade for the full command), then check:
kubectl -n foxplan get pods -l app.kubernetes.io/component=app
kubectl -n foxplan get hpa # with autoscaling
Always set this number in the values: a kubectl scale is overwritten by the next upgrade. Each instance requests 1 vCPU and 4 Gi of memory (app.resources): your nodes must be able to host all of them, including the autoscaling maximum. From 2 instances on, the chart adds a PodDisruptionBudget that keeps at least one running during node operations. Rule of thumb: 2 instances from 100 to 500 users, autoscaling beyond.
Sessions are stored in Redis and scheduled tasks take a lock in Redis: instances share users without session affinity, and a task runs only once.
High availability
- Application: 2 instances or more — see Number of instances.
- MongoDB: the chart's MongoDB is a single node. For high availability, use a replica set (MongoDB operator, managed service) with
mongodb.enabled: falseandmongodb.externalUri: mongodb://utilisateur:mot-de-passe@hote1,hote2,hote3/foxplanweb?replicaSet=rs0&authSource=admin. - Redis: always deployed by the chart, with on-disk persistence. A Redis outage logs users out without data loss: it only holds sessions and cache.
Inbound and outbound certificates
FoxPlan uses two different certificates, one per traffic direction:
| Inbound flows | Outbound flows | |
|---|---|---|
| Purpose | Prove to browsers that the server really is host | Let the application recognise your internal services: AD FS, SMTP over TLS, S3… |
| Used by | The Ingress controller | The foxplan-app pods (Java) |
| Content | The FoxPlan domain certificate and its private key | The public certificate of the authority that signed these services — root and intermediates. Never a private key |
| Location | TLS Secret foxplan-tls (ingress.tls.secretName), or cert-manager | Value app.outboundCaCerts, or an existing ConfigMap named by app.outboundCaConfigMap |
| Format | PEM: --cert = server certificate followed by the intermediates, --key = unencrypted key | PEM, key names ending in .crt. One file may hold several certificates |
| Required | Yes | Only if a called service is signed by a non-public authority |
| Taken into account | The controller reloads the Secret when it is updated | At pod startup. helm upgrade restarts them; a modified external ConfigMap requires kubectl -n foxplan rollout restart deployment foxplan-app |
They are not the same certificates, but they often come from the same PKI: the root authority certificate that signed FoxPlan's is then also the one to provide for outbound flows.
For outbound flows:
# foxplan-values.yaml
app:
outboundCaCerts:
ca-interne.crt: |
-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIU...
-----END CERTIFICATE-----
or, without copying the file into the values:
helm upgrade --install foxplan foxplan-1.0.0.tgz -n foxplan -f foxplan-values.yaml \
--set-file 'app.outboundCaCerts.ca-interne\.crt=ca-interne.crt'
The chart rejects a name that does not end in .crt and content carrying a private key; it only keeps the BEGIN/END CERTIFICATE blocks. To find out whether an authority is needed and to convert a .cer, .p7b or .pfx file to PEM, see Docker Compose: the commands are the same.
Secrets
When left empty in the values, the JWT key and the MongoDB and Redis passwords (and the Garage keys) are generated at first installation, stored in the foxplan-secrets Secret, then kept at each upgrade. This Secret also survives helm uninstall.
SECRET_KEY_JWT also encrypts the SSO configurations stored in the database. Restoring a database without this key makes these configurations unreadable.
kubectl -n foxplan get secret foxplan-secrets -o yaml > foxplan-secrets.yaml
To manage secrets yourself (Vault, External Secrets…), create a Secret holding SECRET_KEY_JWT, MONGO_PASSWORD, REDIS_PASSWORD, MAIL_SMTP_PASSWORD, FILE_S3_ACCESS_KEY and FILE_S3_SECRET_KEY (plus URL_MONGO with an external MongoDB), and reference it with secrets.existingSecret. MongoDB passwords must remain alphanumeric: they go as-is into the connection URL.
Backup
Back up together, at the same time: the database, the files and the Secret.
Database — hot, from the MongoDB pod, which embeds the tools:
kubectl -n foxplan exec foxplan-mongodb-0 -- sh -c \
'mongodump --username "$MONGO_INITDB_ROOT_USERNAME" \
--password "$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase admin --archive --gzip' \
> foxplan-$(date +%F-%H%M).archive.gz
To restore, use the same command with mongorestore --drop and kubectl exec -i … < archive. Then flush the Redis cache, otherwise the application serves stale data:
kubectl -n foxplan exec foxplan-redis-0 -- sh -c 'redis-cli -a "$REDIS_PASSWORD" --no-auth-warning FLUSHALL'
Files in Garage — hot, with rclone from an administration workstation:
kubectl -n foxplan port-forward svc/foxplan-garage 3900:3900 &
KEY=$(kubectl -n foxplan get secret foxplan-secrets -o jsonpath='{.data.FILE_S3_ACCESS_KEY}' | base64 -d)
SECRET=$(kubectl -n foxplan get secret foxplan-secrets -o jsonpath='{.data.FILE_S3_SECRET_KEY}' | base64 -d)
rclone sync :s3:foxplan ./foxplan-files \
--s3-provider Other --s3-endpoint http://127.0.0.1:3900 --s3-region garage \
--s3-access-key-id "$KEY" --s3-secret-access-key "$SECRET"
To restore, swap the source and the destination. With your own S3 storage, apply its usual backup policy. Your platform's volume snapshots (VolumeSnapshot) are also suitable, provided MongoDB and Garage are captured at the same time.
Upgrade
helm upgrade foxplan foxplan-1.0.0.tgz -n foxplan -f foxplan-values.yaml
- Pinned version (
image.tag): change the value, then run the command again. - With
latest: the command alone changes nothing if the values are identical. Restart the pods so they pull the image again:
kubectl -n foxplan rollout restart deployment
The application rollout is progressive: a new replica starts and becomes ready before the old one stops.
Uninstallation
helm uninstall foxplan -n foxplan
The volumes (MongoDB, Redis, Garage) and the Secret are kept: a reinstallation finds the data again. To erase everything, then delete the PersistentVolumeClaims and the Secret from the namespace.
Troubleshooting
| Symptom | Probable cause |
|---|---|
helm refuses: "image.registry est obligatoire" | Required value missing: image.registry, host, mail.from or mail.host |
foxplan-export pod Pending | No amd64 node: add one, or export.amd64Only: false if your ARM nodes emulate amd64 |
foxplan-app pod restarting in a loop | Logs: kubectl -n foxplan logs deploy/foxplan-app. Most often MongoDB or Redis unreachable |
Attachment refused, 413 error | Ingress controller upload limit: see the ingress-nginx box |
| Storage in error in the diagnostics | s3.region region differs from the storage's, or storage unreachable from the pods |
| No sign-up code received | SMTP: the login page diagnostics show whether it is reachable |
latest does not update | kubectl -n foxplan rollout restart deployment: see Upgrade |
| SSO configuration unreadable after a restore | foxplan-secrets Secret differs from the one in effect at backup time |
PKIX path building failed in the foxplan-app logs | Internal authority of AD FS, SMTP or S3 missing from app.outboundCaCerts: see Inbound and outbound certificates |