HelloJohn / docs
Self-Hosting

Kubernetes Deployment

Deploy HelloJohn on Kubernetes using Helm or raw manifests, with PostgreSQL, Redis, and ingress configuration.

Kubernetes Deployment

HelloJohn runs as a stateless container and is well-suited for Kubernetes. This guide covers deploying with Helm (recommended) or raw manifests.


Prerequisites

Before deploying:

  • Kubernetes 1.25+
  • kubectl configured for your cluster
  • Helm 3 (if using Helm)
  • PostgreSQL accessible from the cluster
  • TLS certificate or cert-manager installed
  • An ingress controller (nginx, Traefik, etc.)

See Prerequisites for full infrastructure requirements.


1. Add the HelloJohn Helm repository

helm repo add hellojohn https://charts.hellojohn.dev
helm repo update

2. Create a values file

# values.yaml
replicaCount: 2

image:
  repository: ghcr.io/hellojohn/hellojohn
  tag: "latest"
  pullPolicy: IfNotPresent

env:
  HELLOJOHN_ENV: production
  HELLOJOHN_PORT: "8080"

envFromSecret: hellojohn-secrets   # Kubernetes secret with sensitive values

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: auth.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: hellojohn-tls
      hosts:
        - auth.example.com

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

podDisruptionBudget:
  enabled: true
  minAvailable: 1

3. Create the secrets

Store sensitive configuration in a Kubernetes Secret:

kubectl create secret generic hellojohn-secrets \
  --from-literal=HELLOJOHN_DATABASE_URL="postgresql://user:pass@host:5432/hellojohn?sslmode=require" \
  --from-literal=HELLOJOHN_JWT_SIGNING_KEY="$(cat signing_key.pem)" \
  --from-literal=HELLOJOHN_MASTER_KEY="mk_live_your_master_key" \
  --from-literal=HELLOJOHN_SMTP_PASSWORD="your_smtp_password" \
  --namespace=hellojohn

For production, use External Secrets Operator or Sealed Secrets to sync secrets from AWS Secrets Manager, Vault, or GCP Secret Manager.

4. Run database migrations

Before the first deploy, run migrations as a Kubernetes Job:

helm install hellojohn hellojohn/hellojohn \
  --namespace hellojohn \
  --create-namespace \
  --values values.yaml \
  --set migrate.enabled=true \
  --wait

Or run migrations separately:

kubectl run hellojohn-migrate \
  --image=ghcr.io/hellojohn/hellojohn:latest \
  --restart=Never \
  --env-from=secret/hellojohn-secrets \
  --command -- /app/hellojohn migrate

5. Install the chart

helm install hellojohn hellojohn/hellojohn \
  --namespace hellojohn \
  --create-namespace \
  --values values.yaml

6. Upgrade

helm upgrade hellojohn hellojohn/hellojohn \
  --namespace hellojohn \
  --values values.yaml

Raw Manifests

If you prefer not to use Helm, here are the core manifests.

Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: hellojohn

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellojohn
  namespace: hellojohn
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hellojohn
  template:
    metadata:
      labels:
        app: hellojohn
    spec:
      containers:
        - name: hellojohn
          image: ghcr.io/hellojohn/hellojohn:latest
          ports:
            - containerPort: 8080
          envFrom:
            - secretRef:
                name: hellojohn-secrets
          env:
            - name: HELLOJOHN_ENV
              value: production
            - name: HELLOJOHN_PORT
              value: "8080"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: hellojohn

Service

apiVersion: v1
kind: Service
metadata:
  name: hellojohn
  namespace: hellojohn
spec:
  selector:
    app: hellojohn
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Ingress (nginx)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hellojohn
  namespace: hellojohn
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "1m"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - auth.example.com
      secretName: hellojohn-tls
  rules:
    - host: auth.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hellojohn
                port:
                  number: 80

HorizontalPodAutoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hellojohn
  namespace: hellojohn
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hellojohn
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: hellojohn
  namespace: hellojohn
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: hellojohn

Health Check Endpoint

HelloJohn exposes a health check at GET /health:

{
  "status": "ok",
  "database": "ok",
  "redis": "ok",
  "version": "1.4.2"
}

Use this for readiness and liveness probes.


Multi-Region Deployment

For multi-region deployments:

  1. Deploy HelloJohn in each region
  2. Use a globally distributed PostgreSQL (e.g., CockroachDB, PlanetScale, Neon Branching, or AWS Aurora Global)
  3. Use Redis per-region for rate limiting (each region has its own Redis)
  4. Use a global load balancer (Cloudflare Load Balancing, AWS Global Accelerator) to route to the nearest region

JWTs are stateless — any region can verify a token issued by another region, since all regions share the same signing key.


Upgrading

# Pull latest chart
helm repo update

# Upgrade (migrations run automatically if migrate.enabled=true)
helm upgrade hellojohn hellojohn/hellojohn \
  --namespace hellojohn \
  --values values.yaml

HelloJohn uses rolling deployments by default. Old pods are replaced one at a time to maintain zero downtime. Database migrations are backward-compatible with the previous version to support this.


On this page