Part 6 of 6 — Final

    Production Hardening

    SSO with Authentik, zero-trust access via Cloudflare Tunnel, secrets management with Infisical, and a disaster recovery playbook.

    30 min read
    Security focused

    Your Dokploy setup works. Now make it bulletproof. This guide covers authentication with SSO, zero-trust access with Cloudflare Tunnel, secrets management, and a disaster recovery playbook you'll be grateful to have.

    SSO with Authentik

    Authentik is a self-hosted identity provider that adds single sign-on to all your applications — including Dokploy itself.

    Why SSO?

    • One login for Dokploy, Grafana, and all your apps
    • Centralized user management — disable one account, lose access everywhere
    • 2FA enforcement across all services
    • Audit logs for who accessed what, when

    Deploy Authentik

    Create a new Docker Compose service in Dokploy

    docker-compose.yml
    version: '3.8'
    
    services:
      authentik-db:
        image: postgres:16-alpine
        container_name: authentik-db
        environment:
          POSTGRES_DB: authentik
          POSTGRES_USER: authentik
          POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD}
        volumes:
          - authentik_db:/var/lib/postgresql/data
        restart: unless-stopped
    
      authentik-redis:
        image: redis:alpine
        container_name: authentik-redis
        command: --save 60 1 --loglevel warning
        volumes:
          - authentik_redis:/data
        restart: unless-stopped
    
      authentik-server:
        image: ghcr.io/goauthentik/server:latest
        container_name: authentik-server
        command: server
        environment:
          AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
          AUTHENTIK_REDIS__HOST: authentik-redis
          AUTHENTIK_POSTGRESQL__HOST: authentik-db
          AUTHENTIK_POSTGRESQL__USER: authentik
          AUTHENTIK_POSTGRESQL__NAME: authentik
          AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
        volumes:
          - authentik_media:/media
          - authentik_templates:/templates
        ports:
          - "9000:9000"
          - "9443:9443"
        depends_on:
          - authentik-db
          - authentik-redis
        restart: unless-stopped
    
      authentik-worker:
        image: ghcr.io/goauthentik/server:latest
        container_name: authentik-worker
        command: worker
        environment:
          AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
          AUTHENTIK_REDIS__HOST: authentik-redis
          AUTHENTIK_POSTGRESQL__HOST: authentik-db
          AUTHENTIK_POSTGRESQL__USER: authentik
          AUTHENTIK_POSTGRESQL__NAME: authentik
          AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
        volumes:
          - authentik_media:/media
          - authentik_templates:/templates
        depends_on:
          - authentik-db
          - authentik-redis
        restart: unless-stopped
    
    volumes:
      authentik_db:
      authentik_redis:
      authentik_media:
      authentik_templates:

    Environment Variables

    Set in Dokploy
    AUTHENTIK_SECRET_KEY=generate-a-long-random-string-here
    AUTHENTIK_DB_PASSWORD=another-secure-password

    Generate a secret key:

    openssl rand -base64 36

    Initial Setup

    1. Visit http://your-server-ip:9000/if/flow/initial-setup/
    2. Create your admin account
    3. Set up 2FA (strongly recommended)

    Protect Dokploy with Authentik

    In Authentik:

    1. Go to Applications → Providers → Create
    2. Select OAuth2/OpenID Provider
    3. Configure:
      • Name: Dokploy
      • Authorization flow: default-provider-authorization-implicit-consent
      • Client ID: (auto-generated, copy this)
      • Client Secret: (click to reveal, copy this)
      • Redirect URIs: https://dokploy.yourdomain.com/api/auth/callback/authentik
    4. Go to Applications → Applications → Create
    5. Configure: Name: Dokploy, Slug: dokploy, Provider: Select the provider you just created

    In Dokploy:

    1. Go to Settings → Authentication
    2. Add OAuth provider with:
      • Provider: Custom OAuth
      • Client ID: (from Authentik)
      • Client Secret: (from Authentik)
      • Authorization URL: https://auth.yourdomain.com/application/o/authorize/
      • Token URL: https://auth.yourdomain.com/application/o/token/
      • Userinfo URL: https://auth.yourdomain.com/application/o/userinfo/

    Protect Grafana with Authentik

    Create another OAuth2 provider and application for Grafana in Authentik, then set these environment variables:

    Grafana environment variables
    GF_AUTH_GENERIC_OAUTH_ENABLED=true
    GF_AUTH_GENERIC_OAUTH_NAME=Authentik
    GF_AUTH_GENERIC_OAUTH_CLIENT_ID=your-client-id
    GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=your-client-secret
    GF_AUTH_GENERIC_OAUTH_SCOPES=openid profile email
    GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.yourdomain.com/application/o/authorize/
    GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.yourdomain.com/application/o/token/
    GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.yourdomain.com/application/o/userinfo/
    GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups[*], 'Grafana Admins') && 'Admin' || 'Viewer'

    Forward Auth for Any Application

    For apps that don't support OAuth natively, use Authentik's forward auth with Traefik

    In your app's labels
    labels:
      - "traefik.http.routers.myapp.middlewares=authentik@docker"
      - "traefik.http.middlewares.authentik.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
      - "traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true"
      - "traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email"

    This puts Authentik login in front of any application — even static sites.

    Zero-Trust Access with Cloudflare Tunnel

    Cloudflare Tunnel connects your server to Cloudflare's network without opening any inbound ports. No public IP exposure, no firewall holes.

    Why Cloudflare Tunnel?

    • No open ports — Your server is invisible to port scanners
    • DDoS protection — Cloudflare absorbs attacks
    • Free SSL — Automatic certificates
    • Access policies — Require authentication before reaching your apps

    Install cloudflared

    On your Dokploy server
    curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
    echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
    sudo apt update && sudo apt install cloudflared

    Authenticate and Create Tunnel

    Authenticate
    cloudflared tunnel login

    This opens a browser to authenticate with Cloudflare. Select your domain.

    Create tunnel
    cloudflared tunnel create dokploy-tunnel

    Note the tunnel ID and credentials file path.

    Configure the Tunnel

    /etc/cloudflared/config.yml
    tunnel: your-tunnel-id
    credentials-file: /root/.cloudflared/your-tunnel-id.json
    
    ingress:
      # Dokploy dashboard
      - hostname: dokploy.yourdomain.com
        service: http://localhost:3000
      
      # Your applications
      - hostname: app.yourdomain.com
        service: http://localhost:80
      
      # Grafana
      - hostname: grafana.yourdomain.com
        service: http://localhost:3001
      
      # Catch-all (required)
      - service: http_status:404

    Create DNS Records and Run as Service

    Create DNS records
    cloudflared tunnel route dns dokploy-tunnel dokploy.yourdomain.com
    cloudflared tunnel route dns dokploy-tunnel app.yourdomain.com
    cloudflared tunnel route dns dokploy-tunnel grafana.yourdomain.com
    Run as a service
    sudo cloudflared service install
    sudo systemctl start cloudflared
    sudo systemctl enable cloudflared

    Lock Down the Firewall

    Now that traffic comes through Cloudflare, close the public ports:

    Remove public access
    # Remove public access
    sudo ufw delete allow 80/tcp
    sudo ufw delete allow 443/tcp
    sudo ufw delete allow 3000/tcp
    sudo ufw delete allow 3001/tcp
    
    # Keep SSH (or tunnel that too)
    sudo ufw status

    Your services are now only accessible through Cloudflare Tunnel.

    Cloudflare Access (Optional)

    Add another authentication layer through Cloudflare's dashboard:

    1. Go to Cloudflare Zero Trust → Access → Applications
    2. Add an application
    3. Set policies (e.g., require email ending in @yourcompany.com)
    4. Users must authenticate with Cloudflare before reaching your app

    This stacks with Authentik — Cloudflare Access for network-level auth, Authentik for application-level auth.

    Secrets Management with Infisical

    Hardcoded secrets in environment variables work, but don't scale. Infisical centralizes secrets with versioning, access control, and audit logs.

    Deploy Infisical

    docker-compose.yml
    version: '3.8'
    
    services:
      infisical-db:
        image: postgres:16-alpine
        container_name: infisical-db
        environment:
          POSTGRES_DB: infisical
          POSTGRES_USER: infisical
          POSTGRES_PASSWORD: ${INFISICAL_DB_PASSWORD}
        volumes:
          - infisical_db:/var/lib/postgresql/data
        restart: unless-stopped
    
      infisical-redis:
        image: redis:alpine
        container_name: infisical-redis
        volumes:
          - infisical_redis:/data
        restart: unless-stopped
    
      infisical:
        image: infisical/infisical:latest
        container_name: infisical
        environment:
          ENCRYPTION_KEY: ${INFISICAL_ENCRYPTION_KEY}
          JWT_SIGNUP_SECRET: ${INFISICAL_JWT_SIGNUP_SECRET}
          JWT_REFRESH_SECRET: ${INFISICAL_JWT_REFRESH_SECRET}
          JWT_AUTH_SECRET: ${INFISICAL_JWT_AUTH_SECRET}
          SITE_URL: https://secrets.yourdomain.com
          DB_CONNECTION_URI: postgresql://infisical:${INFISICAL_DB_PASSWORD}@infisical-db:5432/infisical
          REDIS_URL: redis://infisical-redis:6379
        ports:
          - "8070:8080"
        depends_on:
          - infisical-db
          - infisical-redis
        restart: unless-stopped
    
    volumes:
      infisical_db:
      infisical_redis:

    Generate Secrets

    Generate required secrets
    # Encryption key (32 bytes, hex encoded)
    openssl rand -hex 32
    
    # JWT secrets
    openssl rand -base64 32
    openssl rand -base64 32
    openssl rand -base64 32

    Initial Setup

    1. Visit http://your-server-ip:8070
    2. Create your organization and admin account
    3. Create a project for your application
    4. Add environment (e.g., production, staging)
    5. Add your secrets: DATABASE_URL, REDIS_URL, API_KEY, etc.

    Option 1: Infisical CLI in Dockerfile

    Dockerfile
    # Install Infisical CLI
    RUN curl -fsSL https://raw.githubusercontent.com/Infisical/infisical/main/scripts/install.sh | sh
    
    # Run with secrets injected
    CMD ["infisical", "run", "--", "node", "server.js"]

    Set these environment variables in Dokploy:

    INFISICAL_TOKEN=your-service-token
    INFISICAL_PROJECT_ID=your-project-id
    INFISICAL_ENVIRONMENT=production

    Access Control

    Create service tokens with minimal permissions:

    1. Go to Project Settings → Service Tokens
    2. Create token with:
      • Environment: production
      • Permissions: Read only
      • Expiry: 90 days

    Different tokens for different apps means compromising one doesn't expose everything.

    Disaster Recovery Playbook

    When things go wrong, you need a plan — not a panic.

    What Could Go Wrong

    ScenarioImpactRecovery Time
    Container crashSingle app downMinutes (auto-restart)
    Server rebootAll apps down briefly5-10 minutes
    Disk failureData lossHours (restore from backup)
    Server compromiseFull breachHours to days
    Datacenter outageEverything downHours (failover to new server)

    Prevention Checklist

    • Automated daily backups (Part 3)
    • Offsite backup sync (Restic to S3)
    • Monitoring and alerting (Part 5)
    • Tested restore procedure
    • Documented server configuration
    • Multi-factor authentication everywhere
    • Firewall configured (UFW)
    • Regular security updates

    Server Configuration as Code

    Document everything needed to rebuild your server from scratch

    infrastructure.md
    # Server Configuration
    
    ## Base Setup
    - Ubuntu 24.04 LTS
    - RamNode Standard VPS, 4GB RAM, NL datacenter
    - Domain: yourdomain.com
    
    ## Installed Software
    - Docker (via get.docker.com)
    - Dokploy (via install script)
    - cloudflared (Cloudflare Tunnel)
    - UFW firewall
    
    ## Firewall Rules
    - 22/tcp (SSH)
    - All other traffic via Cloudflare Tunnel
    
    ## DNS Records
    - dokploy.yourdomain.com → Cloudflare Tunnel
    - app.yourdomain.com → Cloudflare Tunnel
    - auth.yourdomain.com → Cloudflare Tunnel
    
    ## Secrets Location
    - Infisical: https://secrets.yourdomain.com
    - Backup encryption key: In password manager
    
    ## Backup Schedule
    - Daily at 3 AM UTC
    - Retained 14 days locally
    - Synced to S3: bucket-name
    - Retention: 7 daily, 4 weekly, 6 monthly

    Full Server Rebuild

    1. Provision new VPS — Same specs as documented, same datacenter if possible
    2. Base setup:
      apt update && apt upgrade -y
      apt install -y curl git ufw
      curl -fsSL https://get.docker.com | sh
    3. Install Dokploy:
      curl -sSL https://dokploy.com/install.sh | sh
    4. Restore data:
      # Install restic
      apt install restic
      
      # Restore from S3
      export AWS_ACCESS_KEY_ID="your-key"
      export AWS_SECRET_ACCESS_KEY="your-secret"
      restic -r s3:s3.region.amazonaws.com/your-bucket restore latest --target /
    5. Restore databases:
      gunzip -c backup.sql.gz | docker exec -i <db-container> psql -U user dbname
    6. Update DNS / Cloudflare Tunnel:
      cloudflared tunnel login
      cloudflared tunnel route dns <tunnel-name> <hostname>
    7. Verify everything works — Check all applications respond, verify database connections, test authentication, confirm monitoring is receiving data

    Incident Response Template

    When something breaks, document it
    ## Incident: [Brief description]
    
    **Date:** YYYY-MM-DD HH:MM UTC
    **Duration:** X hours Y minutes
    **Severity:** Critical / High / Medium / Low
    
    ### Timeline
    - HH:MM - Alert received
    - HH:MM - Investigation started
    - HH:MM - Root cause identified
    - HH:MM - Fix deployed
    - HH:MM - Service restored
    
    ### Root Cause
    [What actually broke and why]
    
    ### Resolution
    [What was done to fix it]
    
    ### Prevention
    [What we'll do to prevent this happening again]
    
    ### Action Items
    - [ ] Task 1
    - [ ] Task 2

    Security Hardening Checklist

    Server Level

    • SSH key authentication only
    • Non-root user for daily operations
    • UFW firewall enabled
    • Automatic security updates
    • Fail2ban installed

    Docker Level

    • Keep Docker updated
    • Use official/trusted images
    • Run as non-root users
    • Set resource limits
    • Scan images for vulnerabilities

    Application Level

    • All traffic over HTTPS
    • Strong passwords / SSO enforced
    • 2FA enabled for admin accounts
    • Secrets in Infisical, not env vars
    • Regular dependency updates

    Network Level

    • Cloudflare Tunnel (no exposed ports)
    • Cloudflare Access policies
    • Internal services not public
    • Database ports closed

    Essential Security Commands

    Server hardening commands
    # Disable password authentication
    sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
    sudo systemctl restart sshd
    
    # Enable automatic updates
    sudo apt install unattended-upgrades
    sudo dpkg-reconfigure -plow unattended-upgrades
    
    # Install fail2ban
    sudo apt install fail2ban
    sudo systemctl enable fail2ban
    
    # Scan images with Trivy
    docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image myapp:latest

    Maintenance Schedule

    Daily (Automated)

    • Backups run at 3 AM
    • Backup sync to offsite storage
    • Monitoring checks every minute

    Weekly

    • Review monitoring dashboards
    • Check backup completion logs
    • Review security alerts

    Monthly

    • Apply system updates
    • Rotate service tokens
    • Test backup restoration
    • Review access logs
    • Update documentation

    Quarterly

    • Full disaster recovery test
    • Security audit
    • Dependency updates
    • Performance review
    • Cost optimization review

    Series Complete!

    You've built a production-grade deployment platform:

    1. Part 1 — Dokploy installed, first app deployed
    2. Part 2 — Production Dockerfiles for real frameworks
    3. Part 3 — Databases with automated backups
    4. Part 4 — CI/CD pipelines with automatic deploys
    5. Part 5 — Full observability stack
    6. Part 6 — Hardened for production

    This setup rivals managed platforms like Vercel, Render, or Railway — at a fraction of the cost, with full control over your infrastructure.

    Related Guides