Skip to content
Descoped Source Code Portalv2.2.13
PollingSign in

Deployment Guide for Authly

Up to date

docs/deployment-guide.md on GitHub

⚠️ Warning: Authly is a work in progress with ~90% OIDC compliance. Not recommended for production use without extensive testing and security audit.

This guide covers deployment scenarios for Authly OAuth 2.1 Authorization Server. Use at your own risk in production environments.

Table of Contents

  1. Deployment Overview
  2. Docker Deployment
  3. Production Infrastructure
  4. Kubernetes Deployment
  5. Security Configuration
  6. Monitoring and Observability
  7. Operations and Maintenance
  8. Troubleshooting

Deployment Overview

Architecture Options

Authly supports multiple deployment architectures:

graph TB
    subgraph "Development"
        DEV_LOCAL[Local Source Code]
        DEV_DOCKER[Docker Compose Dev]
    end
    
    subgraph "Testing"
        TEST_HUB[Docker Hub Images]
        TEST_STAGING[Staging Environment]
    end
    
    subgraph "Production"
        PROD_DOCKER[Docker Compose Production]
        PROD_K8S[Kubernetes Cluster]
        PROD_CLOUD[Cloud Services]
    end
    
    DEV_LOCAL --> TEST_HUB
    DEV_DOCKER --> TEST_STAGING
    TEST_HUB --> PROD_DOCKER
    TEST_STAGING --> PROD_K8S
    PROD_DOCKER --> PROD_CLOUD

Feature Matrix

Feature Development Docker Hub Production Kubernetes
Source Code Mounting ✅ ❌ ❌ ❌
Admin Tools ✅ Optional ❌ Optional
SSL/TLS Termination Self-signed Self-signed Required Required
Monitoring Stack Optional Optional Recommended Required
High Availability ❌ ❌ Optional ✅
Auto-scaling ❌ ❌ ❌ ✅
Secret Management Environment Environment Docker Secrets K8s Secrets

Docker Deployment

Quick Start Matrix

Scenario Command Use Case
Development docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d Local development with tools
Docker Hub Testing docker compose -f docker-compose.hub.yml up -d Test released versions
Production docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d Production deployment
Full Monitoring docker compose -f docker-compose.hub.yml --profile monitoring up -d With Prometheus/Grafana

Development Environment

Perfect for local development with hot reloading and debugging tools.

# Clone repository
git clone <repository-url>
cd authly

# Generate development SSL certificates
./scripts/setup-dev-certs.sh

# Start development environment
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# Access services
echo "Authly API: http://localhost:8000"
echo "pgAdmin: http://localhost:5050 (admin@authly.dev / admin)"
echo "Redis Commander: http://localhost:8081 (admin / admin)"
echo "Mailhog: http://localhost:8025"

Development Features:

  • Hot code reloading (source mounted)
  • Debug logging enabled
  • Relaxed rate limiting
  • Development admin tools
  • Email testing with Mailhog

Docker Hub Deployment

Use pre-built images for testing and production without building from source.

# Setup environment
cp .env.hub.example .env.hub
vim .env.hub  # Configure for your environment

# Basic deployment
docker compose -f docker-compose.hub.yml up -d

# With admin tools
docker compose -f docker-compose.hub.yml --profile admin up -d

# With monitoring
docker compose -f docker-compose.hub.yml --profile monitoring up -d

# Full stack
docker compose -f docker-compose.hub.yml --profile admin --profile monitoring up -d

Version Management

# Use specific version (recommended for production)
AUTHLY_VERSION=0.5.3 docker compose -f docker-compose.hub.yml up -d

# Use latest (testing only)
AUTHLY_VERSION=latest docker compose -f docker-compose.hub.yml up -d

# Use minor version (automatic patch updates)
AUTHLY_VERSION=0.5 docker compose -f docker-compose.hub.yml up -d

Production Docker Deployment

Enterprise-ready deployment with security, monitoring, and high availability.

# 1. Setup environment
cp .env.example .env
vim .env  # Configure production values

# 2. Generate secure secrets
echo "$(openssl rand -base64 32)" | docker secret create postgres_password -
echo "$(openssl rand -base64 32)" | docker secret create redis_password -
echo "$(openssl rand -base64 32)" | docker secret create jwt_secret_key -
echo "$(openssl rand -base64 32)" | docker secret create jwt_refresh_secret_key -

# 3. Deploy production stack
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 4. Deploy with monitoring
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
  --profile production --profile monitoring up -d

Production Features

  • SSL/TLS termination with Nginx
  • Docker secrets for sensitive data
  • Stricter rate limiting and security headers
  • Resource limits and health checks
  • Log aggregation with structured JSON
  • Admin endpoints access control

SSL/TLS Configuration

Development (Self-signed)

# Automatic generation
./scripts/setup-dev-certs.sh

Production (Let's Encrypt)

# Generate certificates
sudo certbot certonly --standalone -d auth.yourdomain.com

# Copy to Docker volumes
sudo cp /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem docker-compose/nginx/ssl/cert.pem
sudo cp /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem docker-compose/nginx/ssl/key.pem

# Set proper permissions
sudo chown $USER:$USER docker-compose/nginx/ssl/*.pem

Environment Configuration Templates

Development (.env.dev)

# Development defaults - insecure but convenient
POSTGRES_PASSWORD=dev_password
REDIS_PASSWORD=redis_dev_password
JWT_SECRET_KEY=dev-secret-key-not-for-production
JWT_REFRESH_SECRET_KEY=dev-refresh-secret-not-for-production
AUTHLY_BOOTSTRAP_DEV_MODE=true
AUTHLY_LOG_LEVEL=DEBUG
RATE_LIMIT_MAX_REQUESTS=1000

Production (.env)

# Core Configuration
AUTHLY_VERSION=0.5.3
DEFAULT_API_URL=https://auth.yourdomain.com
DEFAULT_ISSUER_URL=https://auth.yourdomain.com

# Database
POSTGRES_PASSWORD=your_secure_database_password
DATABASE_URL=postgresql://authly:${POSTGRES_PASSWORD}@postgres:5432/authly

# Redis
REDIS_PASSWORD=your_secure_redis_password
AUTHLY_REDIS_CACHE=true
AUTHLY_REDIS_RATE_LIMIT=true

# JWT Configuration (CRITICAL - must be secure)
JWT_SECRET_KEY=your-256-bit-secret-key
JWT_REFRESH_SECRET_KEY=your-256-bit-refresh-secret-key
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=7

# Security
AUTHLY_BOOTSTRAP_DEV_MODE=false
AUTHLY_LOG_LEVEL=INFO
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_WINDOW_SECONDS=60

# Admin
AUTHLY_ADMIN_USERNAME=admin
AUTHLY_ADMIN_PASSWORD=your_secure_admin_password

Production Infrastructure

Minimum Requirements

Application Servers (per instance)

  • CPU: 4 cores (8 threads)
  • Memory: 8GB RAM
  • Storage: 50GB SSD
  • Network: 1Gbps connection

Database Server

  • CPU: 8 cores (16 threads)
  • Memory: 16GB RAM (min), 32GB recommended
  • Storage: 500GB SSD with high IOPS
  • Network: 10Gbps connection

Load Balancer

  • CPU: 2 cores
  • Memory: 4GB RAM
  • Network: 10Gbps connection
  • SSL: Hardware or software SSL acceleration

Production Architecture

graph TB
    subgraph "External"
        USERS[Users/Clients]
        ADMIN[Administrators]
    end
    
    subgraph "Edge Layer"
        LB[Load Balancer<br/>nginx/HAProxy]
        SSL[SSL Termination]
        WAF[Web Application Firewall]
    end
    
    subgraph "Application Layer"
        APP1[Authly Instance 1]
        APP2[Authly Instance 2]
        APP3[Authly Instance 3]
    end
    
    subgraph "Data Layer"
        DB_PRIMARY[(PostgreSQL Primary)]
        DB_REPLICA[(PostgreSQL Replica)]
        REDIS[(Redis Cluster)]
        VAULT[HashiCorp Vault]
    end
    
    subgraph "Monitoring"
        PROMETHEUS[Prometheus]
        GRAFANA[Grafana]
        ALERTS[AlertManager]
    end
    
    USERS --> LB
    ADMIN --> LB
    LB --> SSL
    SSL --> WAF
    WAF --> APP1
    WAF --> APP2
    WAF --> APP3
    
    APP1 --> DB_PRIMARY
    APP2 --> DB_PRIMARY
    APP3 --> DB_PRIMARY
    
    APP1 --> DB_REPLICA
    APP2 --> DB_REPLICA
    APP3 --> DB_REPLICA
    
    APP1 --> REDIS
    APP2 --> REDIS
    APP3 --> REDIS
    
    APP1 --> VAULT
    APP2 --> VAULT
    APP3 --> VAULT
    
    APP1 --> PROMETHEUS
    APP2 --> PROMETHEUS
    APP3 --> PROMETHEUS
    
    PROMETHEUS --> GRAFANA
    PROMETHEUS --> ALERTS

Database Configuration

Schema Setup

-- Core extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    username VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(255),
    phone_number VARCHAR(50),
    locale VARCHAR(10) DEFAULT 'en',
    picture VARCHAR(512),
    is_verified BOOLEAN DEFAULT false,
    is_active BOOLEAN DEFAULT true,
    is_superuser BOOLEAN DEFAULT false,
    requires_password_change BOOLEAN DEFAULT false,
    last_login TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- OAuth clients table
CREATE TABLE clients (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    client_id VARCHAR(255) UNIQUE NOT NULL,
    client_name VARCHAR(255) NOT NULL,
    client_secret_hash VARCHAR(255),
    client_type VARCHAR(20) NOT NULL CHECK (client_type IN ('confidential', 'public')),
    redirect_uris TEXT[] NOT NULL,
    client_uri VARCHAR(255),
    logo_uri VARCHAR(255),
    contacts TEXT[],
    tos_uri VARCHAR(255),
    policy_uri VARCHAR(255),
    jwks_uri VARCHAR(255),
    software_id VARCHAR(255),
    software_version VARCHAR(255),
    token_endpoint_auth_method VARCHAR(50) DEFAULT 'client_secret_basic',
    response_types TEXT[] DEFAULT ARRAY['code'],
    grant_types TEXT[] DEFAULT ARRAY['authorization_code'],
    require_pkce BOOLEAN DEFAULT true,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- OAuth scopes table
CREATE TABLE scopes (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    scope_name VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,
    is_default BOOLEAN DEFAULT false,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Client-scope associations
CREATE TABLE client_scopes (
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    scope_id UUID NOT NULL REFERENCES scopes(id) ON DELETE CASCADE,
    PRIMARY KEY (client_id, scope_id)
);

-- Tokens table
CREATE TABLE tokens (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    jti VARCHAR(64) UNIQUE NOT NULL,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
    token_type VARCHAR(20) NOT NULL CHECK (token_type IN ('access', 'refresh')),
    scopes TEXT[],
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    last_used_at TIMESTAMP WITH TIME ZONE,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Authorization codes table
CREATE TABLE authorization_codes (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    code VARCHAR(255) UNIQUE NOT NULL,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    scopes TEXT[] NOT NULL,
    code_challenge VARCHAR(255) NOT NULL,
    code_challenge_method VARCHAR(10) DEFAULT 'S256',
    redirect_uri TEXT NOT NULL,
    nonce VARCHAR(255),
    state VARCHAR(255),
    max_age INTEGER,
    acr_values TEXT[],
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    used BOOLEAN DEFAULT false,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- JWKS table
CREATE TABLE jwks (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    kid VARCHAR(255) UNIQUE NOT NULL,
    kty VARCHAR(20) NOT NULL,
    use_type VARCHAR(20) DEFAULT 'sig',
    alg VARCHAR(20) DEFAULT 'RS256',
    key_data JSONB NOT NULL,
    is_active BOOLEAN DEFAULT true,
    expires_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Performance Indexes

-- User indexes
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_users_username ON users(username);
CREATE INDEX CONCURRENTLY idx_users_active ON users(is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_users_last_login ON users(last_login);

-- Client indexes
CREATE INDEX CONCURRENTLY idx_clients_client_id ON clients(client_id);
CREATE INDEX CONCURRENTLY idx_clients_active ON clients(is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_clients_type ON clients(client_type);

-- Scope indexes
CREATE INDEX CONCURRENTLY idx_scopes_name ON scopes(scope_name);
CREATE INDEX CONCURRENTLY idx_scopes_active ON scopes(is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_scopes_default ON scopes(is_default) WHERE is_default = true;

-- Token indexes (critical for performance)
CREATE INDEX CONCURRENTLY idx_tokens_jti ON tokens(jti);
CREATE INDEX CONCURRENTLY idx_tokens_user_id ON tokens(user_id);
CREATE INDEX CONCURRENTLY idx_tokens_client_id ON tokens(client_id);
CREATE INDEX CONCURRENTLY idx_tokens_expires_at ON tokens(expires_at);
CREATE INDEX CONCURRENTLY idx_tokens_active ON tokens(is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_tokens_user_type_active ON tokens(user_id, token_type, is_active);

-- Authorization code indexes
CREATE INDEX CONCURRENTLY idx_auth_codes_code ON authorization_codes(code);
CREATE INDEX CONCURRENTLY idx_auth_codes_expires_at ON authorization_codes(expires_at);
CREATE INDEX CONCURRENTLY idx_auth_codes_client_user ON authorization_codes(client_id, user_id);
CREATE INDEX CONCURRENTLY idx_auth_codes_unused ON authorization_codes(used) WHERE used = false;

-- JWKS indexes
CREATE INDEX CONCURRENTLY idx_jwks_kid ON jwks(kid);
CREATE INDEX CONCURRENTLY idx_jwks_active ON jwks(is_active) WHERE is_active = true;

Kubernetes Deployment

Namespace and Configuration

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: authly
  labels:
    name: authly
    app.kubernetes.io/name: authly
    app.kubernetes.io/version: "0.5.3"

---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: authly-config
  namespace: authly
data:
  ENVIRONMENT: "production"
  LOG_LEVEL: "info"
  OAUTH_ENABLED: "true"
  PKCE_REQUIRED: "true"
  REQUIRE_USER_CONSENT: "true"
  RATE_LIMIT_ENABLED: "true"
  RATE_LIMIT_MAX_REQUESTS: "100"
  RATE_LIMIT_WINDOW_SECONDS: "60"
  ACCESS_TOKEN_EXPIRE_MINUTES: "15"
  REFRESH_TOKEN_EXPIRE_DAYS: "7"
  AUTHORIZATION_CODE_EXPIRE_MINUTES: "10"
  CORS_ORIGINS: "https://app.example.com,https://admin.example.com"
  ALLOWED_HOSTS: "auth.example.com,api.example.com"
  METRICS_ENABLED: "true"
  HEALTH_CHECK_ENABLED: "true"

Secrets Management

# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: authly-secrets
  namespace: authly
type: Opaque
data:
  # Base64 encoded values - generate with: echo -n "value" | base64
  DATABASE_URL: <base64-encoded-database-url>
  JWT_SECRET_KEY: <base64-encoded-jwt-secret>
  JWT_REFRESH_SECRET_KEY: <base64-encoded-refresh-secret>
  ADMIN_PASSWORD: <base64-encoded-admin-password>
  SMTP_PASSWORD: <base64-encoded-smtp-password>
  REDIS_PASSWORD: <base64-encoded-redis-password>

Application Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: authly-deployment
  namespace: authly
  labels:
    app: authly
    version: "0.5.3"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: authly
  template:
    metadata:
      labels:
        app: authly
        version: "0.5.3"
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8000"
        prometheus.io/path: "/metrics"
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
      - name: authly
        image: descoped/authly:0.5.3
        ports:
        - containerPort: 8000
          name: http
          protocol: TCP
        envFrom:
        - configMapRef:
            name: authly-config
        - secretRef:
            name: authly-secrets
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          successThreshold: 1
          failureThreshold: 3
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
      terminationGracePeriodSeconds: 30
      restartPolicy: Always

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: authly-service
  namespace: authly
  labels:
    app: authly
spec:
  selector:
    app: authly
  ports:
  - port: 80
    targetPort: 8000
    protocol: TCP
    name: http
  type: ClusterIP

---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: authly-ingress
  namespace: authly
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "1m"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "30"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
  tls:
  - hosts:
    - auth.example.com
    secretName: authly-tls
  rules:
  - host: auth.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: authly-service
            port:
              number: 80

Database Deployment

# k8s/postgres.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: authly
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: fast-ssd

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
  namespace: authly
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      securityContext:
        runAsUser: 999
        runAsGroup: 999
        fsGroup: 999
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          value: authly
        - name: POSTGRES_USER
          value: authly
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"
        livenessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - authly
            - -d
            - authly
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - authly
            - -d
            - authly
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
  namespace: authly
  labels:
    app: postgres
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
  type: ClusterIP

HorizontalPodAutoscaler

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: authly-hpa
  namespace: authly
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: authly-deployment
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60

Security Configuration

SSL/TLS Configuration

Nginx Production Configuration

# nginx/nginx.conf
events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Security headers
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self'; object-src 'none'; media-src 'self'; frame-src 'none'; child-src 'none'; worker-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; manifest-src 'self';" always;
    
    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    
    # Performance optimization
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 1M;
    server_tokens off;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1000;
    gzip_comp_level 6;
    gzip_types
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml
        text/plain
        text/css
        text/xml
        text/javascript;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m;
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
    limit_req_zone $binary_remote_addr zone=discovery:10m rate=60r/m;
    
    # IP whitelist for admin endpoints
    geo $admin_allowed {
        default 0;
        10.0.0.0/8 1;
        172.16.0.0/12 1;
        192.168.0.0/16 1;
        # Add your admin IP ranges here
    }
    
    upstream authly_backend {
        least_conn;
        server authly:8000 max_fails=3 fail_timeout=30s;
        keepalive 32;
    }
    
    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name auth.example.com;
        return 301 https://$server_name$request_uri;
    }
    
    # Main HTTPS server
    server {
        listen 443 ssl http2;
        server_name auth.example.com;
        
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        
        # OAuth authorization endpoints (strict rate limiting)
        location ~ ^/(oauth/authorize|auth/token|auth/refresh|oauth/revoke) {
            limit_req zone=auth burst=5 nodelay;
            
            proxy_pass http://authly_backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            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;
            
            proxy_connect_timeout 5s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
            
            proxy_buffering off;
            proxy_request_buffering off;
        }
        
        # Discovery endpoints (cached, higher rate limit)
        location ~ ^/\.well-known/(oauth-authorization-server|openid_configuration|jwks\.json) {
            limit_req zone=discovery burst=20 nodelay;
            
            proxy_pass http://authly_backend;
            proxy_cache_valid 200 1h;
            add_header Cache-Control "public, max-age=3600";
            expires 1h;
            
            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;
        }
        
        # OIDC endpoints
        location ~ ^/(oidc/userinfo|oidc/logout) {
            limit_req zone=api burst=20 nodelay;
            
            proxy_pass http://authly_backend;
            proxy_http_version 1.1;
            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;
        }
        
        # API endpoints
        location /api {
            limit_req zone=api burst=20 nodelay;
            
            proxy_pass http://authly_backend;
            proxy_http_version 1.1;
            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;
        }
        
        # Admin endpoints (IP restricted)
        location /admin {
            if ($admin_allowed = 0) {
                return 403;
            }
            
            proxy_pass http://authly_backend;
            proxy_http_version 1.1;
            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;
        }
        
        # Health check (no rate limiting)
        location /health {
            proxy_pass http://authly_backend;
            access_log off;
            
            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;
        }
        
        # Metrics endpoint (IP restricted)
        location /metrics {
            if ($admin_allowed = 0) {
                return 403;
            }
            
            proxy_pass http://authly_backend;
            access_log off;
            
            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;
        }
        
        # Default location
        location / {
            proxy_pass http://authly_backend;
            proxy_http_version 1.1;
            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;
        }
    }
}

Firewall Configuration

#!/bin/bash
# scripts/configure_firewall.sh

# Configure UFW firewall for production Authly deployment
ufw --force reset

# Default policies
ufw default deny incoming
ufw default allow outgoing

# SSH access (customize port as needed)
ufw allow 22/tcp

# HTTP/HTTPS
ufw allow 80/tcp
ufw allow 443/tcp

# Database access (internal networks only)
ufw allow from 10.0.0.0/8 to any port 5432
ufw allow from 172.16.0.0/12 to any port 5432
ufw allow from 192.168.0.0/16 to any port 5432

# Redis access (internal networks only)
ufw allow from 10.0.0.0/8 to any port 6379
ufw allow from 172.16.0.0/12 to any port 6379
ufw allow from 192.168.0.0/16 to any port 6379

# Monitoring (internal networks only)
ufw allow from 10.0.0.0/8 to any port 9090  # Prometheus
ufw allow from 10.0.0.0/8 to any port 3000  # Grafana

# Enable firewall
ufw --force enable
ufw status verbose

Monitoring and Observability

Prometheus Configuration

# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s
  external_labels:
    cluster: 'authly-production'
    env: 'production'

rule_files:
  - "authly_rules.yml"

scrape_configs:
  - job_name: 'authly'
    static_configs:
      - targets: ['authly-service:80']
    metrics_path: /metrics
    scrape_interval: 30s
    scrape_timeout: 10s
    
  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']
    scrape_interval: 60s
    
  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']
    scrape_interval: 30s
    
  - job_name: 'nginx'
    static_configs:
      - targets: ['nginx-exporter:9113']
    scrape_interval: 30s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093

Alert Rules

# monitoring/authly_rules.yml
groups:
  - name: authly_alerts
    rules:
      - alert: AuthlyHighErrorRate
        expr: rate(authly_http_requests_total{status=~"5.."}[5m]) > 0.1
        for: 2m
        labels:
          severity: warning
          service: authly
        annotations:
          summary: "Authly high error rate detected"
          description: "Error rate is {{ $value }} errors per second"
          
      - alert: AuthlyHighLatency
        expr: histogram_quantile(0.95, rate(authly_http_request_duration_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: warning
          service: authly
        annotations:
          summary: "Authly high latency detected"
          description: "95th percentile latency is {{ $value }}s"
          
      - alert: AuthlyDatabaseDown
        expr: up{job="postgres"} == 0
        for: 1m
        labels:
          severity: critical
          service: authly
        annotations:
          summary: "Authly database connection lost"
          description: "PostgreSQL database is unreachable"
          
      - alert: AuthlyRedisDown
        expr: up{job="redis"} == 0
        for: 2m
        labels:
          severity: warning
          service: authly
        annotations:
          summary: "Authly Redis connection lost"
          description: "Redis cache is unreachable"
          
      - alert: AuthlyLowAuthSuccess
        expr: rate(authly_oauth_token_requests_total{status="success"}[5m]) / rate(authly_oauth_token_requests_total[5m]) < 0.9
        for: 5m
        labels:
          severity: warning
          service: authly
        annotations:
          summary: "Authly low authentication success rate"
          description: "Authentication success rate is {{ $value | humanizePercentage }}"

Grafana Dashboard

{
  "dashboard": {
    "id": null,
    "title": "Authly OAuth 2.1 Production Dashboard",
    "tags": ["authly", "oauth", "oidc"],
    "timezone": "browser",
    "panels": [
      {
        "id": 1,
        "title": "Request Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "sum(rate(authly_http_requests_total[5m])) by (endpoint)",
            "legendFormat": "{{endpoint}}"
          }
        ],
        "yAxes": [
          {
            "label": "Requests/sec"
          }
        ]
      },
      {
        "id": 2,
        "title": "Response Time",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.50, rate(authly_http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "50th percentile"
          },
          {
            "expr": "histogram_quantile(0.95, rate(authly_http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "95th percentile"
          },
          {
            "expr": "histogram_quantile(0.99, rate(authly_http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "99th percentile"
          }
        ],
        "yAxes": [
          {
            "label": "Seconds"
          }
        ]
      },
      {
        "id": 3,
        "title": "OAuth Token Requests",
        "type": "graph",
        "targets": [
          {
            "expr": "sum(rate(authly_oauth_token_requests_total[5m])) by (grant_type, status)",
            "legendFormat": "{{grant_type}} - {{status}}"
          }
        ]
      },
      {
        "id": 4,
        "title": "Authentication Success Rate",
        "type": "singlestat",
        "targets": [
          {
            "expr": "rate(authly_auth_login_attempts_total{status=\"success\"}[5m]) / rate(authly_auth_login_attempts_total[5m])",
            "legendFormat": "Success Rate"
          }
        ],
        "format": "percentunit",
        "valueName": "current"
      },
      {
        "id": 5,
        "title": "Database Connections",
        "type": "graph",
        "targets": [
          {
            "expr": "authly_database_connections_active",
            "legendFormat": "Active"
          },
          {
            "expr": "authly_database_connections_idle",
            "legendFormat": "Idle"
          }
        ]
      },
      {
        "id": 6,
        "title": "Redis Memory Usage",
        "type": "graph",
        "targets": [
          {
            "expr": "authly_cache_memory_usage_bytes",
            "legendFormat": "Memory Usage"
          }
        ],
        "yAxes": [
          {
            "label": "Bytes"
          }
        ]
      }
    ],
    "time": {
      "from": "now-1h",
      "to": "now"
    },
    "refresh": "5s"
  }
}

Operations and Maintenance

Deployment Scripts

Automated Deployment

#!/bin/bash
# scripts/deploy_production.sh

set -e

ENVIRONMENT=${ENVIRONMENT:-production}
VERSION=${VERSION:-0.5.3}
NAMESPACE=${NAMESPACE:-authly}

echo "Starting Authly production deployment..."
echo "Environment: $ENVIRONMENT"
echo "Version: $VERSION"
echo "Namespace: $NAMESPACE"

# Pre-deployment checks
echo "Running pre-deployment checks..."

# Check Kubernetes connectivity
kubectl cluster-info > /dev/null 2>&1 || {
    echo "Error: Cannot connect to Kubernetes cluster"
    exit 1
}

# Check if namespace exists
kubectl get namespace $NAMESPACE > /dev/null 2>&1 || {
    echo "Creating namespace $NAMESPACE..."
    kubectl create namespace $NAMESPACE
}

# Validate configurations
echo "Validating Kubernetes configurations..."
kubectl apply --dry-run=client -f k8s/ > /dev/null 2>&1 || {
    echo "Error: Invalid Kubernetes configuration"
    exit 1
}

# Database migration
echo "Running database migrations..."
kubectl apply -f k8s/migration-job.yaml
kubectl wait --for=condition=complete job/database-migration -n $NAMESPACE --timeout=300s

# Deploy services
echo "Deploying services..."
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secrets.yaml

# Deploy database
if [ "$DATABASE_EXTERNAL" != "true" ]; then
    kubectl apply -f k8s/postgres.yaml
    kubectl wait --for=condition=available deployment/postgres-deployment -n $NAMESPACE --timeout=300s
fi

# Deploy Redis
if [ "$REDIS_ENABLED" = "true" ]; then
    kubectl apply -f k8s/redis.yaml
    kubectl wait --for=condition=available deployment/redis-deployment -n $NAMESPACE --timeout=300s
fi

# Deploy main application
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/ingress.yaml
kubectl apply -f k8s/hpa.yaml

# Wait for deployment
echo "Waiting for deployment to be ready..."
kubectl wait --for=condition=available deployment/authly-deployment -n $NAMESPACE --timeout=600s

# Verify deployment
echo "Verifying deployment..."
kubectl get pods -n $NAMESPACE
kubectl get services -n $NAMESPACE
kubectl get ingress -n $NAMESPACE

# Run post-deployment tests
echo "Running post-deployment tests..."
./scripts/post_deployment_tests.sh

echo "Deployment completed successfully!"

# Display access information
INGRESS_IP=$(kubectl get ingress authly-ingress -n $NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Application available at: https://auth.example.com"
echo "Ingress IP: $INGRESS_IP"

Post-Deployment Testing

#!/bin/bash
# scripts/post_deployment_tests.sh

set -e

BASE_URL=${BASE_URL:-https://auth.example.com}
TIMEOUT=30

echo "Running post-deployment tests for $BASE_URL..."

# Test 1: Health check
echo "Testing health endpoint..."
curl -f -s --max-time $TIMEOUT "$BASE_URL/health" > /dev/null && {
    echo "✓ Health check passed"
} || {
    echo "✗ Health check failed"
    exit 1
}

# Test 2: OAuth discovery
echo "Testing OAuth discovery endpoint..."
DISCOVERY_RESPONSE=$(curl -f -s --max-time $TIMEOUT "$BASE_URL/.well-known/oauth-authorization-server")
echo "$DISCOVERY_RESPONSE" | jq -e '.authorization_endpoint' > /dev/null 2>&1 && {
    echo "✓ OAuth discovery endpoint working"
} || {
    echo "✗ OAuth discovery endpoint failed"
    exit 1
}

# Test 3: OIDC discovery
echo "Testing OIDC discovery endpoint..."
OIDC_RESPONSE=$(curl -f -s --max-time $TIMEOUT "$BASE_URL/.well-known/openid_configuration")
echo "$OIDC_RESPONSE" | jq -e '.userinfo_endpoint' > /dev/null 2>&1 && {
    echo "✓ OIDC discovery endpoint working"
} || {
    echo "✗ OIDC discovery endpoint failed"
    exit 1
}

# Test 4: JWKS endpoint
echo "Testing JWKS endpoint..."
JWKS_RESPONSE=$(curl -f -s --max-time $TIMEOUT "$BASE_URL/.well-known/jwks.json")
echo "$JWKS_RESPONSE" | jq -e '.keys' > /dev/null 2>&1 && {
    echo "✓ JWKS endpoint working"
} || {
    echo "✗ JWKS endpoint failed"
    exit 1
}

# Test 5: Authorization endpoint (should return 400 for invalid params)
echo "Testing authorization endpoint..."
AUTH_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" --max-time $TIMEOUT \
    "$BASE_URL/oauth/authorize?response_type=code&client_id=invalid")
[ "$AUTH_RESPONSE" = "400" ] && {
    echo "✓ Authorization endpoint responding correctly"
} || {
    echo "✗ Authorization endpoint failed (HTTP $AUTH_RESPONSE)"
    exit 1
}

# Test 6: SSL certificate
echo "Testing SSL certificate..."
SSL_CHECK=$(echo | openssl s_client -servername auth.example.com -connect auth.example.com:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null)
[ -n "$SSL_CHECK" ] && {
    echo "✓ SSL certificate valid"
} || {
    echo "✗ SSL certificate check failed"
    exit 1
}

echo "All post-deployment tests passed successfully!"

Backup and Recovery

Database Backup

#!/bin/bash
# scripts/backup_database.sh

set -e

BACKUP_DIR=${BACKUP_DIR:-/backups}
RETENTION_DAYS=${RETENTION_DAYS:-30}
S3_BUCKET=${S3_BUCKET:-authly-backups}

mkdir -p $BACKUP_DIR

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="authly_backup_${TIMESTAMP}.sql"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_FILE}"

echo "Starting database backup..."

# Create database backup
pg_dump "$DATABASE_URL" --clean --if-exists --create > "$BACKUP_PATH"

if [ $? -eq 0 ]; then
    echo "Database backup created: $BACKUP_PATH"
    
    # Compress backup
    gzip "$BACKUP_PATH"
    BACKUP_PATH="${BACKUP_PATH}.gz"
    
    # Upload to S3 if configured
    if [ -n "$S3_BUCKET" ]; then
        aws s3 cp "$BACKUP_PATH" "s3://${S3_BUCKET}/database/" --storage-class STANDARD_IA
        echo "Backup uploaded to S3: s3://${S3_BUCKET}/database/$(basename $BACKUP_PATH)"
    fi
    
    # Clean up old backups
    find "$BACKUP_DIR" -name "authly_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    echo "Backup completed successfully!"
else
    echo "Backup failed!"
    exit 1
fi

Disaster Recovery

#!/bin/bash
# scripts/disaster_recovery.sh

set -e

BACKUP_FILE=${1}
ENVIRONMENT=${2:-production}

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file> [environment]"
    echo "Example: $0 authly_backup_20240101_120000.sql.gz production"
    exit 1
fi

echo "Starting disaster recovery for environment: $ENVIRONMENT"
echo "Backup file: $BACKUP_FILE"

# Confirm before proceeding
read -p "This will replace the current database. Are you sure? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
    echo "Recovery cancelled."
    exit 1
fi

# Download backup from S3 if needed
if [[ "$BACKUP_FILE" == s3://* ]]; then
    echo "Downloading backup from S3..."
    aws s3 cp "$BACKUP_FILE" "/tmp/$(basename $BACKUP_FILE)"
    BACKUP_FILE="/tmp/$(basename $BACKUP_FILE)"
fi

# Decompress if needed
if [[ "$BACKUP_FILE" == *.gz ]]; then
    echo "Decompressing backup..."
    gunzip -c "$BACKUP_FILE" > "/tmp/recovery.sql"
    BACKUP_FILE="/tmp/recovery.sql"
fi

# Stop application
echo "Stopping application..."
kubectl scale deployment authly-deployment --replicas=0 -n authly

# Restore database
echo "Restoring database..."
psql "$DATABASE_URL" < "$BACKUP_FILE"

# Restart application
echo "Restarting application..."
kubectl scale deployment authly-deployment --replicas=3 -n authly

# Wait for application to be ready
kubectl wait --for=condition=available deployment/authly-deployment -n authly --timeout=300s

# Verify recovery
echo "Verifying recovery..."
./scripts/post_deployment_tests.sh

echo "Disaster recovery completed successfully!"

Maintenance Procedures

#!/bin/bash
# scripts/maintenance.sh

set -e

echo "Starting routine maintenance..."

# Database maintenance
echo "Running database maintenance..."
psql "$DATABASE_URL" << EOF
-- Clean up expired tokens
DELETE FROM tokens WHERE expires_at < NOW() - INTERVAL '1 day';

-- Clean up expired authorization codes
DELETE FROM authorization_codes WHERE expires_at < NOW() - INTERVAL '1 hour';

-- Update table statistics
VACUUM ANALYZE tokens;
VACUUM ANALYZE authorization_codes;
VACUUM ANALYZE users;
VACUUM ANALYZE clients;

-- Reindex if needed
REINDEX INDEX CONCURRENTLY idx_tokens_expires_at;
REINDEX INDEX CONCURRENTLY idx_auth_codes_expires_at;
EOF

# Application cache cleanup (if Redis enabled)
if [ "$REDIS_ENABLED" = "true" ]; then
    echo "Redis maintenance..."
    redis-cli --scan --pattern "authly:cache:*" | head -1000 | xargs redis-cli del
fi

# Log cleanup
echo "Cleaning up logs..."
find /var/log/authly -name "*.log" -mtime +7 -delete
find /var/log/authly -name "*.log.*" -mtime +30 -delete

# Backup cleanup
echo "Cleaning up old backups..."
find /backups -name "authly_backup_*.sql.gz" -mtime +30 -delete

# Container cleanup
echo "Docker system cleanup..."
docker system prune -f --volumes --filter "until=168h"

echo "Maintenance completed successfully!"

Troubleshooting

Common Issues and Solutions

Service Won't Start

# Check service status
docker compose ps
kubectl get pods -n authly

# Check service logs
docker compose logs authly
kubectl logs -f deployment/authly-deployment -n authly

# Check resource usage
docker stats
kubectl top pods -n authly

Database Connection Issues

# Test database connectivity (Docker)
docker compose exec authly python -c "
import asyncpg
import asyncio
async def test():
    conn = await asyncpg.connect('postgresql://authly:password@postgres:5432/authly')
    print('Database connected!')
    await conn.close()
asyncio.run(test())
"

# Test database connectivity (Kubernetes)
kubectl exec -it deployment/authly-deployment -n authly -- python -c "
import asyncpg
import asyncio
import os
async def test():
    conn = await asyncpg.connect(os.environ['DATABASE_URL'])
    print('Database connected!')
    await conn.close()
asyncio.run(test())
"

SSL/TLS Issues

# Test SSL configuration
openssl s_client -connect auth.example.com:443 -servername auth.example.com

# Check certificate validity
openssl x509 -in docker-compose/nginx/ssl/cert.pem -text -noout

# Verify certificate chain
curl -I https://auth.example.com

Performance Issues

# Check application metrics
curl http://localhost:8000/metrics

# Database performance analysis
psql "$DATABASE_URL" -c "
SELECT query, calls, total_time, mean_time 
FROM pg_stat_statements 
ORDER BY total_time DESC LIMIT 10;"

# Redis performance
redis-cli info memory
redis-cli slowlog get 10

Monitoring Queries

Key Performance Indicators

# Request rate
sum(rate(authly_http_requests_total[5m]))

# Error rate
sum(rate(authly_http_requests_total{status=~"5.."}[5m])) / sum(rate(authly_http_requests_total[5m]))

# Response time (95th percentile)
histogram_quantile(0.95, rate(authly_http_request_duration_seconds_bucket[5m]))

# Authentication success rate
rate(authly_oauth_token_requests_total{status="success"}[5m]) / rate(authly_oauth_token_requests_total[5m])

# Database connection utilization
authly_database_connections_active / (authly_database_connections_active + authly_database_connections_idle)

This comprehensive deployment guide provides everything needed to deploy Authly from development through enterprise production environments, with proper security, monitoring, and operational procedures.