Auth1 Blog Self-Hosted Authentication Guide
Self-Hosted Complete Guide · 18 min read

Self-Hosted Authentication:
The Complete Guide

Your authentication system touches every user, every session, every dollar of revenue. Handing that to a third-party SaaS means trusting them with your most sensitive data flows. This guide covers why you should self-host, what your options are, and how to deploy Auth1 with Docker in under 10 minutes.

100%
Data Sovereignty
$0
Vendor lock-in cost
<10min
Docker deploy
GDPR
Compliant

Why Self-Host Your Authentication?

Authentication is the single most security-critical piece of infrastructure in any application. It controls who gets in, what they can access, and how their identity is verified. Yet most companies outsource this entirely to SaaS providers like Auth0, Clerk, or Firebase Auth.

That works until it doesn't. Here's when it breaks down:

Data Sovereignty

When you use a hosted auth provider, every phone number, email address, session token, and authentication event flows through their servers. For companies subject to GDPR, HIPAA, SOC 2, or FedRAMP, this creates a compliance liability. You're trusting a third party with PII that regulators hold you accountable for.

Self-hosting means your user data never leaves your infrastructure. Phone numbers used for OTP verification stay in your database. Session tokens are generated and validated on your servers. Audit logs live in your monitoring stack. When a regulator asks "where does this data live?", the answer is simple: your VPC, your region, your control.

Compliance Requirements

Many industries have strict requirements about data residency and processing:

Cost Control

Auth0's pricing starts reasonable but scales aggressively. At 10,000 MAU, you're paying $240/month. At 50,000 MAU, it's $1,150/month. At 100,000 MAU, you're well past $2,000/month. These are authentication costs alone — before any business logic.

Self-hosted Auth1 runs on a single t3.medium instance ($30/month) handling up to 500,000 MAU. Add a managed PostgreSQL ($15/month) and Redis ($15/month) and you're looking at $60/month total for half a million users. That's a 97% cost reduction compared to Auth0 at the same scale.

No Vendor Lock-in

SaaS auth providers store your users in their database. Migrating away means exporting user data (if they even allow it), re-hashing passwords (if they expose hashes — most don't), and rewriting every integration point. Auth0 migrations routinely take 3-6 months.

With self-hosted auth, your user table is a PostgreSQL table you own. Want to switch to a different auth system? Export the table. Want to add custom fields? Alter the table. Want to run custom queries against auth data? Connect directly.

When SaaS Auth Makes Sense

To be fair: if you have fewer than 5,000 users, no compliance requirements, and no engineering team to maintain infrastructure, hosted auth is perfectly fine. Auth0 and Clerk are excellent products. This guide is for teams that have outgrown that model.


Self-Hosted Options Compared

There are several self-hosted authentication systems available today. Here's an honest comparison of the major options:

Feature Auth0 Keycloak Authelia Auth1
Self-hostable No (SaaS only) Yes Yes Yes
Setup complexity N/A High (Java, realm config) Medium (YAML config) Low (Docker one-liner)
SMS OTP built-in Yes Plugin required No Yes (Twilio + SNS)
Fraud detection Bot detection only No No VOIP blocking, risk scoring
Multi-tenant Yes Yes (realms) No Yes (API key per tenant)
White-label Enterprise only Theme system No Yes (per-tenant branding)
Memory usage N/A 512MB - 2GB (JVM) ~50MB ~80MB
Language N/A Java Go Node.js

Why Not Keycloak?

Keycloak is the most commonly recommended self-hosted auth solution, and it's genuinely powerful. It supports SAML, OIDC, LDAP federation, and complex realm hierarchies. But for most teams, it's massive overkill.

Keycloak requires a JVM (512MB minimum heap), a complex realm/client/role configuration, and deep understanding of SAML vs OIDC protocol flows. The admin console alone has hundreds of settings. Most startups need phone/email OTP, session management, and webhook callbacks. Keycloak can do that, but you'll spend weeks configuring what should take minutes.

Why Not Authelia?

Authelia is excellent as a reverse-proxy authentication layer. It's lightweight (Go binary), supports TOTP/WebAuthn, and integrates well with Traefik and Nginx. But it's designed as a gateway auth layer, not a user management API. It doesn't provide SMS OTP, user signup flows, API key management, or multi-tenant support. If you need a full authentication API (not just a proxy), Authelia isn't the right tool.


Deploy Auth1 with Docker

Auth1 ships as a single Docker image with all dependencies built in. Here's the fastest path to a working self-hosted auth system.

Prerequisites

Step 1: Docker Compose File

Create a docker-compose.yml that includes Auth1, PostgreSQL, and Redis. This gives you a complete auth stack in one file:

YAML docker-compose.yml
version: "3.8"

services:
  auth1:
    image: auth1/auth1-server:latest
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://auth1:secret@postgres:5432/auth1_db
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=your-256-bit-secret-here
      - TWILIO_ACCOUNT_SID=your_twilio_sid
      - TWILIO_AUTH_TOKEN=your_twilio_token
      - TWILIO_MESSAGING_SERVICE_SID=your_messaging_sid
      - APP_URL=https://auth.yourdomain.com
      - NODE_ENV=production
    depends_on:
      - postgres
      - redis
    restart: always

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=auth1
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=auth1_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: always

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
    restart: always

volumes:
  pgdata:

Step 2: Start the Stack

Shell Terminal
# Start everything
docker compose up -d

# Check logs
docker compose logs -f auth1

# You should see:
# Auth1 server running on port 3000
# Database connected: auth1_db
# Redis connected: redis://redis:6379
# Migrations applied: 85 migrations

Step 3: Create Your First Tenant

Auth1 is multi-tenant by default. Each tenant gets its own API key, user pool, and branding configuration. Create your first tenant:

Shell Create tenant
curl -X POST http://localhost:3000/api/admin/tenants \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
    "name": "My App",
    "slug": "myapp",
    "settings": {
      "otp_length": 6,
      "otp_expiry_minutes": 10,
      "max_otp_attempts": 5,
      "rate_limit_per_hour": 5,
      "voip_blocking": true,
      "sms_branding": "MyApp"
    }
  }'

# Response:
# {
#   "tenant_id": "tn_abc123",
#   "api_key": "auth1_pk_myapp_d1617d338c62...",
#   "name": "My App"
# }

Step 4: Test Authentication

Shell Test OTP flow
# Request an OTP
curl -X POST http://localhost:3000/api/auth/request \
  -H "Content-Type: application/json" \
  -H "x-api-key: auth1_pk_myapp_d1617d338c62..." \
  -d '{"phone": "+15551234567"}'

# Verify the OTP
curl -X POST http://localhost:3000/api/auth/verify \
  -H "Content-Type: application/json" \
  -H "x-api-key: auth1_pk_myapp_d1617d338c62..." \
  -d '{"phone": "+15551234567", "code": "482913"}'

# Response:
# {
#   "verified": true,
#   "token": "eyJhbGciOiJIUzI1NiIs...",
#   "user_id": "usr_xyz789"
# }

Environment Variables Reference

Auth1 is configured entirely through environment variables. No config files, no admin panels for core settings. Here's the complete reference:

Variable Description Default
DATABASE_URL PostgreSQL connection string required
REDIS_URL Redis connection string for session storage and rate limiting required
JWT_SECRET 256-bit secret for signing JWTs. Generate with openssl rand -hex 32 required
PORT Server listen port 3000
TWILIO_ACCOUNT_SID Twilio account SID for SMS OTP optional
TWILIO_AUTH_TOKEN Twilio auth token optional
TWILIO_MESSAGING_SERVICE_SID Twilio Messaging Service for branded SMS optional
AWS_SNS_REGION AWS region for SNS failover SMS us-east-1
SNS_ORIGINATION_NUMBER AWS SNS origination phone number optional
APP_URL Public URL for callbacks and email links http://localhost:3000
CORS_ORIGINS Comma-separated allowed origins *
LOG_LEVEL Logging verbosity: debug, info, warn, error info
RATE_LIMIT_WINDOW_MS Rate limit window in milliseconds 3600000
RATE_LIMIT_MAX Max requests per window per IP 100

Configure PostgreSQL

Auth1 uses PostgreSQL for all persistent data: users, tenants, sessions, audit logs, and OTP records. The Docker Compose setup above includes PostgreSQL, but for production you'll want a managed service or a properly tuned instance.

Production PostgreSQL Settings

SQL postgresql.conf (recommended)
-- Connection pooling
max_connections = 200
shared_buffers = '256MB'

-- Write performance
wal_buffers = '16MB'
checkpoint_completion_target = 0.9

-- Query planning
effective_cache_size = '768MB'
random_page_cost = 1.1  -- SSD storage

-- Logging (for audit compliance)
log_statement = 'mod'
log_connections = on
log_disconnections = on

Using AWS RDS

For most production deployments, AWS RDS or Google Cloud SQL is the simplest option. Use a db.t3.medium instance ($60/month) with 20GB SSD storage. Enable automatic backups and set retention to 7 days. Your connection string:

Shell .env
DATABASE_URL=postgresql://auth1:strong_password@your-rds-instance.region.rds.amazonaws.com:5432/auth1_db?sslmode=require

Configure Redis

Redis handles three critical functions in Auth1:

For production, use AWS ElastiCache or a managed Redis service. A cache.t3.micro instance ($12/month) handles up to 100,000 concurrent sessions comfortably.

Important: Redis Persistence

Auth1 uses Redis as a cache, not as primary storage. All critical data is in PostgreSQL. If Redis restarts, active OTPs will expire (users just request a new one) and rate limit counters reset. This is by design — Redis should be configured with maxmemory-policy allkeys-lru for optimal performance.


Configure SMS (Twilio)

Auth1 supports two SMS providers: Twilio (primary) and AWS SNS (failover). For most deployments, Twilio is recommended because it supports Messaging Services, which enable toll-free numbers and automatic carrier compliance.

Step 1: Create a Twilio Messaging Service

  1. Log in to the Twilio Console
  2. Go to Messaging → Services → Create Messaging Service
  3. Name it (e.g., "Auth1-OTP")
  4. Add a toll-free phone number as a sender
  5. Copy the Messaging Service SID (starts with MG)

Step 2: Set Environment Variables

Shell .env
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Auth1 automatically sends branded SMS messages: "Your {TenantName} verification code is: 482913. Expires in 10 minutes." The tenant name is pulled from your tenant configuration, so each app using your Auth1 instance gets its own branding.


Set Up HTTPS

Never run authentication over HTTP in production. Use a reverse proxy with TLS termination. Here's a minimal Nginx configuration with Let's Encrypt:

Nginx /etc/nginx/sites-available/auth1
server {
    listen 443 ssl http2;
    server_name auth.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        proxy_pass          http://127.0.0.1:3000;
        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;
    }
}

server {
    listen 80;
    server_name auth.yourdomain.com;
    return 301 https://$host$request_uri;
}
Shell Install cert
# Install certbot
sudo apt install certbot python3-certbot-nginx

# Get certificate
sudo certbot --nginx -d auth.yourdomain.com

# Auto-renewal is configured automatically

Kubernetes Deployment

For larger deployments or teams already running Kubernetes, Auth1 deploys cleanly as a Deployment with a Service and Ingress. Here's a production-ready manifest:

YAML auth1-k8s.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth1
  labels:
    app: auth1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: auth1
  template:
    metadata:
      labels:
        app: auth1
    spec:
      containers:
      - name: auth1
        image: auth1/auth1-server:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: auth1-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: auth1-secrets
              key: redis-url
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: auth1-secrets
              key: jwt-secret
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: auth1-service
spec:
  selector:
    app: auth1
  ports:
  - port: 80
    targetPort: 3000
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: auth1-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - auth.yourdomain.com
    secretName: auth1-tls
  rules:
  - host: auth.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: auth1-service
            port:
              number: 80
Shell Deploy to K8s
# Create secrets
kubectl create secret generic auth1-secrets \
  --from-literal=database-url="postgresql://auth1:pass@db:5432/auth1_db" \
  --from-literal=redis-url="redis://redis:6379" \
  --from-literal=jwt-secret="$(openssl rand -hex 32)"

# Apply manifests
kubectl apply -f auth1-k8s.yaml

# Check status
kubectl get pods -l app=auth1

Monitoring and Scaling

Health Check Endpoint

Auth1 exposes a /health endpoint that returns the status of all dependencies:

JSON GET /health
{
  "status": "healthy",
  "uptime": 847293,
  "database": "connected",
  "redis": "connected",
  "version": "2.4.1",
  "memory_mb": 78
}

Key Metrics to Monitor

Authentication Rate

Track OTP requests and verifications per minute. A sudden spike could indicate a brute-force attack. Auth1 logs these to stdout for ingestion by Datadog, CloudWatch, or Prometheus.

Fraud Detection Rate

Monitor the percentage of requests blocked by VOIP detection and risk scoring. Healthy applications typically see 2-5% block rates. Higher suggests you're under attack.

SMS Delivery Rate

Track Twilio delivery callbacks. Delivery rates below 95% indicate carrier issues or invalid phone numbers. Auth1 stores delivery status per OTP request.

Database Connection Pool

Monitor active PostgreSQL connections. Auth1 uses a connection pool of 20 by default. If you're consistently above 80% utilization, increase the pool or scale horizontally.

Horizontal Scaling

Auth1 is stateless — all state lives in PostgreSQL and Redis. This means you can run multiple instances behind a load balancer without sticky sessions. For most applications:

The bottleneck is almost always the database, not Auth1 itself. Use PgBouncer for connection pooling and read replicas for audit log queries if you're at enterprise scale.

Production Checklist

Before going live: (1) Set a strong JWT_SECRET with openssl rand -hex 32. (2) Enable HTTPS with a valid TLS certificate. (3) Set CORS_ORIGINS to your specific domains (not *). (4) Configure automated PostgreSQL backups. (5) Set up monitoring alerts on the /health endpoint. (6) Review rate limit settings for your expected traffic volume.

Deploy Auth1 in 10 Minutes

One Docker image. PostgreSQL + Redis. Full authentication with fraud protection, multi-tenant support, and zero vendor lock-in.

Start Free → Read the Docs
Free tier · 1,000 verifications/month · No credit card required