HelloJohn / docs
Self-Hosting

Reverse Proxy

Configure nginx or Caddy to proxy HTTPS traffic to HelloJohn, including TLS termination, WebSocket support, and rate limiting.

Reverse Proxy

HelloJohn listens on plain HTTP. A reverse proxy handles TLS termination, HTTP/2, and acts as a security boundary between the internet and HelloJohn.

Caddy automatically provisions and renews TLS certificates via Let's Encrypt. It requires zero SSL configuration.

Caddyfile

auth.yourdomain.com {
    reverse_proxy localhost:3000 {
        # Pass real client IP to HelloJohn
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}

        # Health check for load balancer
        health_uri /health
        health_interval 30s
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # Gzip compression
    encode gzip

    # Rate limiting (requires caddy-ratelimit plugin)
    # rate_limit {
    #     zone login {
    #         key {remote_host}
    #         events 20
    #         window 1m
    #     }
    #     match path /v1/auth/sign-in /v1/auth/sign-up /v1/auth/magic-link
    # }

    log {
        output file /var/log/caddy/auth.yourdomain.com.log
        format json
    }
}

Start Caddy:

caddy run --config /etc/caddy/Caddyfile

Caddy handles certificate renewal automatically. No cron job needed.

nginx

Installation

# Ubuntu/Debian
apt install nginx certbot python3-certbot-nginx

# Obtain certificate
certbot --nginx -d auth.yourdomain.com

nginx.conf snippet

Create /etc/nginx/sites-available/hellojohn:

upstream hellojohn {
    server 127.0.0.1:3000;
    keepalive 32;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name auth.yourdomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name auth.yourdomain.com;

    # TLS (managed by certbot)
    ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    server_tokens off;

    # Logging
    access_log /var/log/nginx/hellojohn.access.log;
    error_log /var/log/nginx/hellojohn.error.log;

    # Proxy to HelloJohn
    location / {
        proxy_pass http://hellojohn;
        proxy_http_version 1.1;

        # Headers
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Connection keep-alive
        proxy_set_header Connection "";

        # Timeouts
        proxy_connect_timeout 10s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 8k;
    }

    # Health check endpoint (no auth, no logging)
    location = /health {
        proxy_pass http://hellojohn;
        access_log off;
    }

    # Request size limit (e.g., for avatar uploads)
    client_max_body_size 10M;
}

Enable and reload:

ln -s /etc/nginx/sites-available/hellojohn /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

Renew certificates automatically:

# Already added by certbot, verify with:
systemctl status certbot.timer

Traefik

For Kubernetes or Docker Swarm deployments, Traefik is a popular choice.

Docker Compose labels:

services:
  hellojohn:
    image: ghcr.io/hellojohn/hellojohn:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.hellojohn.rule=Host(`auth.yourdomain.com`)"
      - "traefik.http.routers.hellojohn.entrypoints=websecure"
      - "traefik.http.routers.hellojohn.tls.certresolver=letsencrypt"
      - "traefik.http.services.hellojohn.loadbalancer.server.port=3000"
      - "traefik.http.middlewares.hellojohn-compress.compress=true"
      - "traefik.http.routers.hellojohn.middlewares=hellojohn-compress"

Configuring HelloJohn to trust the proxy

When running behind a reverse proxy, set these environment variables so HelloJohn reads the real client IP correctly:

TRUST_PROXY=true
TRUST_PROXY_HOPS=1   # number of proxy hops between internet and HelloJohn

This enables correct IP-based rate limiting and audit logging.

Multiple instances

For horizontal scaling, all instances must share:

  1. The same DATABASE_URL — PostgreSQL is the source of truth
  2. The same REDIS_URL — for shared rate limiting and session state
  3. The same JWT keys — so tokens issued by one instance are valid on another
upstream hellojohn {
    # Sticky sessions are NOT required — stateless JWT auth
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;
    keepalive 64;
}

Testing your setup

# Check TLS grade
curl https://www.ssllabs.com/ssltest/analyze.html?d=auth.yourdomain.com

# Verify HSTS header
curl -I https://auth.yourdomain.com | grep -i strict

# Check security headers
curl -I https://auth.yourdomain.com

# Test health endpoint
curl https://auth.yourdomain.com/health
# {"status":"ok","version":"1.2.3","db":"connected"}

On this page