Skip to main content
Version: 0.8

Install TokenTimer self-hosted

Choose an install path

PathBest forPrerequisites
Docker ComposeSingle host, fastest startDocker and Docker Compose
Kubernetes (Helm)Production clustersKubernetes >= 1.29, Helm >= 3.14

Both paths deploy the same components: the REST API (Node.js, port 4000), the React dashboard, PostgreSQL, and a set of workers (alert discovery, alert delivery, weekly digest, auto sync, endpoint/SSL check).

Option A - Docker Compose

Step 1 - Clone and configure

cd tokentimer-core/deploy/compose
cp .env.example .env

Step 2 - Edit .env

Minimal required configuration (production-safe baseline):

# Core runtime
NODE_ENV=production
SESSION_SECRET=replace_with_a_long_random_value

# Database
DB_HOST=postgres
DB_PORT=5432
DB_NAME=tokentimer
DB_USER=tokentimer
DB_PASSWORD=replace_with_secure_password

# Initial admin bootstrap (first start only)
ADMIN_EMAIL=admin@your-company.com
ADMIN_PASSWORD=ChangeThisSecurePassword123!
ADMIN_NAME=Administrator

# Public URLs (what users/browsers should use)
APP_URL=http://localhost:5173
API_URL=http://localhost:4000

Optional but common additions: SMTP sender identity (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, FROM_EMAIL, FROM_EMAIL_NAME) and host port remaps (API_PORT, DASHBOARD_PORT; keep APP_URL/API_URL in sync if you remap).

warning

If you run locally on http://localhost with NODE_ENV=production, authentication can fail because secure session cookies are not persisted or sent by the browser on plain local HTTP. Put HTTPS in front of the API and dashboard (reverse proxy with TLS), or use NODE_ENV=development for local non-TLS testing. As a last-resort local troubleshooting option only, set SESSION_COOKIE_SECURE_LOCALHOST_OVERRIDE=true.

note

If the API sits behind a reverse proxy or load balancer, set TRUST_PROXY_HOPS to the number of proxy hops (the Compose default is 2, covering LB plus reverse proxy; set 0 when nothing is in front).

Step 3 - Start all services

# A) Build from local source (default)
docker compose up -d

# B) Use prebuilt images (override file)
# Optional vars in .env: TT_IMAGE_REGISTRY, TT_IMAGE_OWNER, TT_IMAGE_TAG
docker compose -f docker-compose.yml -f docker-compose.images.yml up -d

The Compose stack starts PostgreSQL, runs database migrations as a one-shot migrations service, then starts the API, the dashboard, and the five worker containers (worker-discovery, worker-delivery, worker-weekly-digest, worker-auto-sync, worker-endpoint-check).

Step 4 - Verify

# View logs
docker compose logs -f

# Check health
curl http://localhost:4000/health

# View running services
docker compose ps

You should see: all services Up (API and dashboard with healthy healthchecks), and the health endpoint responding with:

{
"status": "healthy",
"timestamp": "2026-03-19T12:00:00.000Z",
"uptime": 123.456,
"memory": { "rss": 52428800 },
"environment": "production"
}

Open http://localhost:5173 in your browser to reach the dashboard.

Option B - Kubernetes 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

helm install tokentimer oci://ghcr.io/tokentimerch/charts/tokentimer \
--namespace tokentimer --create-namespace \
--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.your-domain.com"

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" # must contain SESSION_SECRET

postgresql:
external:
existingSecret: "my-db-secret" # all DB_* keys

smtp:
existingSecret: "my-smtp-secret" # all SMTP_* + FROM_* keys
note

The chart auto-generates SESSION_SECRET, DB_PASSWORD, and ADMIN_PASSWORD (when adminEmail is set) if you do not provide them, but auto-generated values change on each helm upgrade. Set explicit values or use existingSecret for production stability.

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.

# If using ingress
open https://tokentimer.your-domain.com

# Or port-forward
kubectl port-forward svc/tokentimer-dashboard 8080:80 -n tokentimer
open http://localhost:8080

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}}'
warning

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

Required environment variables

Minimum required for a working install (Docker Compose):

SESSION_SECRET=long-random-string # required - long random value
DB_PASSWORD=secure-db-password # required
ADMIN_EMAIL=admin@your-company.com # required - bootstraps the admin account
ADMIN_PASSWORD=secure-admin-password # required on first start; remove afterwards
APP_URL=http://localhost:5173
API_URL=http://localhost:4000

For every available variable (SMTP, Twilio, integrations, worker tuning, security overrides), see the Configuration Reference, the annotated deploy/compose/.env.example, and deploy/helm/values.yaml.

First login

TokenTimer creates the admin user automatically on first startup from ADMIN_EMAIL and ADMIN_PASSWORD.

  1. Navigate to your dashboard URL and open the login page.
  2. Log in with your configured ADMIN_EMAIL and ADMIN_PASSWORD.
  3. You should see your default admin workspace.
warning

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 Alert Preferences, and configure SMTP (via System Settings UI or env vars).

Upgrading

Docker Compose

# Pull latest images
docker compose pull

# Restart services
docker compose up -d

# Verify health
curl http://localhost:4000/health

Kubernetes

helm upgrade tokentimer oci://ghcr.io/tokentimerch/charts/tokentimer -f my-values.yaml -n tokentimer

# Monitor rollout
kubectl rollout status deployment/tokentimer-api

Upgrade notes:

  • API and dashboard Deployments roll automatically on helm upgrade (checksum annotations change when config, SMTP, Twilio, or DB values change).
  • CronJob spec changes apply on the next scheduled worker run.
  • Pods read ConfigMap/Secret env vars only at start; a rollout is required after changing config.baseUrl, config.apiUrl, or SSO env.
  • Back up the database before major upgrades:
docker compose exec postgres pg_dump -U tokentimer tokentimer > backup.sql

Troubleshooting

SymptomLikely causeFix
API won't startDatabase unreachable or credentials wrongTest with psql postgresql://tokentimer:password@localhost:5432/tokentimer; check docker compose logs api
Login returns 401 on localhostNODE_ENV=production with plain-HTTP local URLs; the browser drops secure session cookiesPut HTTPS in front, or use NODE_ENV=development locally; last resort: SESSION_COOKIE_SECURE_LOCALHOST_OVERRIDE=true
No admin account existsAdmin bootstrap skipped (no ADMIN_EMAIL/ADMIN_PASSWORD on first start, or DISABLE_ADMIN_BOOTSTRAP set)Set the bootstrap variables and restart the API once; remove ADMIN_PASSWORD afterwards
Workers not sending alertsSMTP unconfigured, or alerts still queuedCheck worker logs (docker compose logs worker-discovery worker-delivery); verify SMTP env (docker compose exec api env filtered on SMTP_/FROM_EMAIL); inspect the alert_queue table for pending rows
Dashboard loads but API calls failAPI_URL/APP_URL mismatch with actual public URLsAlign the URL variables with what browsers use, rebuild/restart the dashboard
Helm install stalls on databaseCloudNativePG operator not installed (default in-cluster PostgreSQL requires it)Install the CNPG operator first, or switch to postgresql.external
Settings changed but behavior unchanged (Kubernetes)Pods still hold the old environment; env vars are read at startRun a rollout restart of the affected Deployments

Uninstalling

# Docker Compose (keeps data)
docker compose down
# ... including volumes
docker compose down -v

# Kubernetes
helm uninstall tokentimer -n tokentimer
# CloudNativePG PVCs are retained by default; delete manually if no longer needed

Next steps

  • Enable CertOps - turn on the certificate operations layer with the CERTOPS_ENABLED flag.