Skip to main content

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​

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 image registry

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​

ComponentKubernetes objectRole
ApplicationDeployment foxplan-appFoxPlan server (port 5000), one or more replicas
DocumentationDeployment foxplan-docsThis documentation, served under /docs
Schedule exportDeployment foxplan-exportPDF, PNG, Excel, MS Project and Primavera, served under /gantt-export
MongoDBStatefulSet foxplan-mongodbDatabase, one node, persistent volume
RedisStatefulSet foxplan-redisSessions and cache, persistent volume. Always deployed: FoxPlan does not start without it
Garage (optional)StatefulSet foxplan-garageS3 file storage, if you do not have one
RoutingIngress 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:

ImageUsed
mongo:8.2.7Always, except with an external MongoDB (mongodb.enabled: false)
redis:7.4Always
nginxinc/nginx-unprivileged:1.28-alpineExport pod proxy
dxflrs/garage:v2.1.0, curlimages/curl:8.14.1Only with garage.enabled: true
Sensitive sector: go through your internal registry

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.

DirectionPortBetweenPurpose
Inbound443 (TCP)User workstations → Ingress controllerThe application and the SSO return
Inbound80 (TCP)User workstations → Ingress controllerRedirect to 443; cert-manager HTTP-01 challenge
Outbound443 (TCP)foxplan-app pods → identity providerToken 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.extraEnv and app.extraSecretEnv. The redirect URL is built from host: https://<host>/login/oauth2/code/azure or …/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-Host and X-Forwarded-Port headers set by the Ingress controller. The Check configuration button shows the exact URL to declare.
TLS terminated before the Ingress

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):

ComponentCPU requestedMemory requestedMemory limitVolume
Application (per replica)14 Gi6 Gi—
MongoDB0.52 Gi4 Gi50 Gi
Redis0.1256 Mi1 Gi5 Gi
Export (3 containers)0.36656 Mi2.6 Gi—
Documentation0.0132 Mi256 Mi—
Garage (optional)0.1256 Mi1 Gi1 Gi + 100 Gi

Guidelines for the available capacity on the nodes:

UsersCPUMemoryDisk
Up to 1004 vCPU16 GB100 GB
100 to 5008 vCPU32 GB, with 2 application replicas200 GB
BeyondApplication 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.

ingress-nginx

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​

ValueDefaultRole
image.registry—FoxPlan registry address. Required
image.taglatestFoxPlan image version
image.pullPolicyAlwaysWith latest, pulls the image again at each pod start
host—Domain name. Required
ingress.className, ingress.annotations—Ingress controller and its settings
ingress.tls.secretNamefoxplan-tlsDomain TLS Secret
mail.*—SMTP server. Required
s3.*emptyYour S3 storage — see File storage
app.replicas1Application replicas
autoscaling.enabledfalseCPU-based autoscaling
app.extraEnv, app.extraSecretEnv—Additional variables: Microsoft or Google SSO…
mongodb.enabled, mongodb.externalUritrueBuilt-in MongoDB, or your own
export.amd64OnlytruePlaces the export pod on an amd64 node
garage.enabledfalseBuilt-in S3 storage
secrets.*generatedJWT 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: false and mongodb.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 flowsOutbound flows
PurposeProve to browsers that the server really is hostLet the application recognise your internal services: AD FS, SMTP over TLS, S3…
Used byThe Ingress controllerThe foxplan-app pods (Java)
ContentThe FoxPlan domain certificate and its private keyThe public certificate of the authority that signed these services — root and intermediates. Never a private key
LocationTLS Secret foxplan-tls (ingress.tls.secretName), or cert-managerValue app.outboundCaCerts, or an existing ConfigMap named by app.outboundCaConfigMap
FormatPEM: --cert = server certificate followed by the intermediates, --key = unencrypted keyPEM, key names ending in .crt. One file may hold several certificates
RequiredYesOnly if a called service is signed by a non-public authority
Taken into accountThe controller reloads the Secret when it is updatedAt 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.

Back up the Secret with the database

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​

SymptomProbable cause
helm refuses: "image.registry est obligatoire"Required value missing: image.registry, host, mail.from or mail.host
foxplan-export pod PendingNo amd64 node: add one, or export.amd64Only: false if your ARM nodes emulate amd64
foxplan-app pod restarting in a loopLogs: kubectl -n foxplan logs deploy/foxplan-app. Most often MongoDB or Redis unreachable
Attachment refused, 413 errorIngress controller upload limit: see the ingress-nginx box
Storage in error in the diagnosticss3.region region differs from the storage's, or storage unreachable from the pods
No sign-up code receivedSMTP: the login page diagnostics show whether it is reachable
latest does not updatekubectl -n foxplan rollout restart deployment: see Upgrade
SSO configuration unreadable after a restorefoxplan-secrets Secret differs from the one in effect at backup time
PKIX path building failed in the foxplan-app logsInternal authority of AD FS, SMTP or S3 missing from app.outboundCaCerts: see Inbound and outbound certificates