Prerequisites: Kubernetes 1.29+, Helm 3.14+, and access to a published Core chart. The commands use Bash.
Install Core with Helm
The chart deploys the API and dashboard as Deployments, the workers as CronJobs, and PostgreSQL as a CloudNativePG Cluster by default (an existing external PostgreSQL is also supported). Optional resources (disabled by default): Ingress, HPA, PDB, ServiceMonitor, PrometheusRule, NetworkPolicy.
Step 1 - Install the CloudNativePG operator (if using in-cluster PostgreSQL)
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm install cnpg-operator cnpg/cloudnative-pg \
--namespace cnpg-system --create-namespace \
--version 0.23.0 \
--wait
Verify the CRDs are registered:
kubectl get crd clusters.postgresql.cnpg.io
If you already have PostgreSQL, skip the operator and point the chart at your database:
postgresql:
cloudnative:
enabled: false
external:
enabled: true
host: "db.example.com"
port: 5432
database: tokentimer
username: tokentimer
password: "secret" # or use existingSecret
sslMode: require
Step 2 - Install the chart
Select a published chart version and save it with your deployment configuration. Point DNS at your Ingress controller, create the tokentimer namespace, and create a TLS Secret named tokentimer-tls in the tokentimer namespace for your hostname before installing. Replace the example hostname and secret name consistently. The chart does not issue the Ingress certificate for you.
read -r -p 'Core chart version: ' CORE_CHART_VERSION
helm install tokentimer oci://ghcr.io/tokentimerch/charts/tokentimer \
--namespace tokentimer --create-namespace \
--version "$CORE_CHART_VERSION" \
--set config.baseUrl="https://tokentimer.example.com" \
--set config.apiUrl="https://tokentimer.example.com" \
--set config.adminEmail="admin@your-company.com" \
--set config.adminPassword="SecurePassword123!" \
--set config.sessionSecret="replace-with-long-random-value" \
--set postgresql.auth.password="your-db-password" \
--set ingress.enabled=true \
--set 'ingress.hosts[0].host=tokentimer.example.com' \
--set 'ingress.tls[0].hosts[0]=tokentimer.example.com' \
--set 'ingress.tls[0].secretName=tokentimer-tls'
For production, prefer a values file (cp deploy/helm/values.yaml my-values.yaml, edit, then -f my-values.yaml) and pre-existing Kubernetes Secrets instead of plaintext values:
config:
existingSecret: "my-tokentimer-secrets" # see the note below for the required keys
postgresql:
external:
existingSecret: "my-db-secret" # DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, DB_SSL
smtp:
existingSecret: "my-smtp-secret" # all SMTP_* + FROM_* keys
Two further variants exist:
postgresql.auth.existingSecretsupplies the credentials for the in-cluster CloudNativePG cluster. It must be of typekubernetes.io/basic-authwithusernameandpasswordkeys, and because it only feeds CNPG bootstrap you must also provideDB_PASSWORDto the app throughconfig.existingSecret.twilio.existingSecretsupplies the Twilio credentials and template SIDs.
Setting any existingSecret makes the chart stop emitting that group's generated keys entirely, so your Secret has to be complete for that group.
config.existingSecret replaces the whole generated SecretSetting it makes the chart skip its entire generated Secret, not just SESSION_SECRET. Your own Secret must therefore carry every key the chart would have produced:
| Key | When required |
|---|---|
SESSION_SECRET | Always |
CERTOPS_SIGNING_ENCRYPTION_KEY | While CertOps is enabled (config.certopsEnabled defaults to true) |
CERTOPS_REGISTRATION_ENCRYPTION_KEY | While CertOps is enabled |
ADMIN_PASSWORD | When you bootstrap an admin with config.adminEmail |
DB_PASSWORD | Unless supplied through postgresql.external.existingSecret; a CNPG postgresql.auth.existingSecret does not provide the application environment |
|
SMTP_PASS| Whensmtp.passwordis set andsmtp.existingSecretis absent | |TWILIO_AUTH_TOKEN| Whentwilio.authTokenis set andtwilio.existingSecretis absent | |WEEKLY_DIGEST_RECIPIENT_KEY| Whenconfig.weeklyDigestRecipientKeyis set |
The CertOps pair is the easy one to miss: CERTOPS_ENABLED still renders as "true", so the deployment comes up looking healthy and then fails closed on every agent register and job dispatch. Verify with kubectl get secret my-tokentimer-secrets -n tokentimer -o jsonpath='{.data}' before cutting over, or set config.certopsEnabled=false if you are not using CertOps.
The chart auto-generates SESSION_SECRET, DB_PASSWORD, and ADMIN_PASSWORD (when adminEmail is set) if you do not provide them. Generated values are preserved across helm upgrade: the chart reads them back from the existing release Secret. Set explicit values or use existingSecret when you manage secrets externally, or when you render manifests with helm template (which cannot read the existing Secret back and therefore produces a new value on each render).
config.certopsEnabled defaults to true, and the API fails closed without CERTOPS_SIGNING_ENCRYPTION_KEY and CERTOPS_REGISTRATION_ENCRYPTION_KEY: agent registration and signed job dispatch reject with CERTOPS_REGISTRATION_ENCRYPTION_KEY_MISSING / CERTOPS_SIGNING_ENCRYPTION_KEY_MISSING, so the CertOps UI appears but no agent can ever register.
The chart generates both when unset and preserves them on upgrade like the secrets above. To set them explicitly:
config:
certopsSigningEncryptionKey: "<64 hex chars>" # openssl rand -hex 32
certopsRegistrationEncryptionKey: "<64 hex chars>"
CERTOPS_SIGNING_ENCRYPTION_KEY wraps the stored job-signing private keys. Preserve it with backups; replacing it alone makes those keys unreadable. Operational signing-key rotation does not change this wrapping key; replacing it requires a separate encrypted-record migration.
CERTOPS_REGISTRATION_ENCRYPTION_KEY wraps only short-lived registration-replay credentials used to recover a lost registration response. Changing it can invalidate outstanding replay recovery, but enrolled-agent credentials are stored as hashes: existing agents do not inherently need to re-register. Preserve the matching key during backup/recovery.
Step 3 - Wait for pods and access the dashboard
kubectl get pods -n tokentimer -w
You should see: API and dashboard pods reach Running/Ready, and the PostgreSQL cluster become healthy.
Also confirm all six worker CronJobs exist, since a missing one fails silently (nothing crashes, the work just never runs):
kubectl get cronjobs -n tokentimer
You should see six entries: tokentimer-alert-discovery, tokentimer-alert-delivery, tokentimer-weekly-digest, tokentimer-endpoint-check, tokentimer-auto-sync and tokentimer-certops. If tokentimer-certops is absent, certificates never renew automatically and offline agents are never marked offline; re-check worker.cronjobs.certops.enabled (default true).
Open the configured HTTPS dashboard URL. For local diagnostics, use kubectl port-forward svc/tokentimer-dashboard 8080:80 -n tokentimer and open http://localhost:8080; the API also needs a reachable local URL for browser calls. See the URL and cookie reference.
If the admin password was auto-generated, retrieve it with:
kubectl get secret -n tokentimer tokentimer-secrets \
-o go-template='{{index .data "ADMIN_PASSWORD" | base64decode}}{{println}}'
With NODE_ENV=production, the API sets the Secure flag on session cookies. Browsers will not persist or send secure cookies over plain HTTP. For anything beyond local port-forwarding, place HTTPS in front of the API and dashboard (Ingress with TLS or a reverse proxy).
First login
TokenTimer creates the admin user automatically on first startup from ADMIN_EMAIL and ADMIN_PASSWORD.
- Navigate to your dashboard URL and open the login page.
- Log in with your configured
ADMIN_EMAILandADMIN_PASSWORD. - You should see your default admin workspace.
After first login, remove ADMIN_PASSWORD from your .env file (or values). It is only needed for the bootstrap on first start.
Recommended next steps: invite team members (Workspace Settings, Members), add your first token, configure alert thresholds and channels in Expiry reminders and thresholds, and configure SMTP (via System Settings UI or env vars).
Continue with First asset and alert check. Optional network policy, monitoring, and scaling are covered in Kubernetes operations.