Deploy on Kubernetes
The Helm chart deploys the full stack on any Kubernetes 1.24+ cluster. It runs the published images, with built-in PostgreSQL and RustFS that can each be swapped for an external service.
AGPL-3.0 · Kubernetes 1.24+ · Helm 3.10+
Requirements
- Kubernetes 1.24+Any cluster, managed or your own.
- Helm 3.10+The chart is in
charts/itsaplan/.
The chart runs the images published to GHCR, with built-in PostgreSQL and RustFS. Each of those can be swapped for an external service.
Install
Create a values file for the deployment:
api:
env:
API_URL: "https://api.example.com"
APP_URL: "https://app.example.com"
S3_BUCKET: "planner-attachments"
secrets:
postgresPassword: "" # openssl rand -base64 32
betterAuthSecret: "" # openssl rand -base64 32
appEncryptionKey: "" # openssl rand -base64 32
s3AccessKeyId: "minioadmin"
s3SecretAccessKey: "minioadmin"Install the chart:
helm install itsaplan charts/itsaplan -f values.yamlEach api pod applies the database migrations in its migrate init container before the server starts. With several replicas, one applies them and the others wait on a Postgres advisory lock. Worker and bot pods wait in their own init container until the migrations are applied. The first account registered becomes the instance admin.
What gets deployed
- apiDeployment and Service. Always.
- webDeployment and Service. Always.
- workerDeployment. Always.
- botDeployment, on
bot.enabled(defaulttrue). - PostgreSQLStatefulSet, Service and PVC, on
postgresql.enabled(defaulttrue). - RustFSDeployment, Service and PVC, plus a bucket init Job, on
minio.enabled(defaulttrue).
Ingress, TLS certificates and a ServiceAccount are available but disabled by default.
Ingress
The chart’s ingress has two modes for the api.
Separate host. The web app and the api each get their own hostname. Works with any ingress controller.
ingress:
enabled: true
className: nginx
host: app.example.com
tls:
enabled: true
secretName: app-tls
api:
mode: separate-host
host: api.example.com
tls:
enabled: true
secretName: api-tlsTraefik entrypoint. The api shares the web hostname on a dedicated entrypoint, which is a separate port. Needs Traefik and the IngressRoute CRD.
ingress:
enabled: true
className: traefik
host: app.example.com
tls:
enabled: true
secretName: app-tls
api:
mode: traefik-entrypoint
entryPoint: apisecure
publicPort: 8443
tls:
enabled: true
secretName: app-tlsTLS with cert-manager
certificate:
enabled: true
issuerName: letsencrypt-prod
issuerKind: ClusterIssuer
dnsNames:
- app.example.com
- api.example.comThe generated secret is named <release>-itsaplan-tls. Reference it in ingress.tls.secretName and ingress.api.tls.secretName.
External database and S3
Disable the built-in PostgreSQL and give a connection string:
postgresql:
enabled: false
externalDatabase:
url: "postgres://user:[email protected]:5432/itsaplan"Disable the built-in RustFS and point at an S3-compatible store:
minio:
enabled: false
externalS3:
endpoint: "https://s3.us-east-1.amazonaws.com"
api:
env:
S3_BUCKET: "my-bucket"
S3_REGION: "us-east-1"
S3_FORCE_PATH_STYLE: "false"
secrets:
s3AccessKeyId: "AKIA..."
s3SecretAccessKey: "..."S3_FORCE_PATH_STYLE is false for AWS and Cloudflare R2, and true for RustFS.
Secrets in production
The chart creates a Kubernetes Secret with plaintext stringData. For production, keep the secrets outside the values file (Sealed Secrets, External Secrets Operator, SOPS) and inject them through a values override or a secret store CSI driver.
Upgrades
helm upgrade itsaplan charts/itsaplan -f values.yamlConfig and secret changes trigger a rolling restart on their own: the deployments carry a checksum annotation on the ConfigMap and the Secret. The new api pods apply the new migrations in their init container.
To leave the Telegram bot out, set bot.enabled: false.
Troubleshooting
Sign-in returns to the login page.
The two hostnames are not under one registrable domain, so the browser drops the session cookie. Give the api and the web app hostnames under the same domain.
Attachments fail against an external S3 store.
S3_FORCE_PATH_STYLE does not match the store. AWS and Cloudflare R2 need false, RustFS needs true.
A values change did nothing.
Only the ConfigMap and the Secret carry the checksum annotation. Run helm upgrade again and check the values reached the release with helm get values itsaplan.
Support
- Values referenceEvery value with its default is in charts/itsaplan.
- BugsReport them in GitHub issues.
- Questions and setup helpAsk in GitHub discussions.
- This guideIt ships with the source as docs/helm.md.
Other ways to run it: Railway, Docker Compose, Coolify.