GraphQL
    Docker

    Deploy Hasura GraphQL Engine on a VPS

    Instant, real-time GraphQL and REST APIs over PostgreSQL with built-in authorization, event triggers, and remote schemas — no boilerplate CRUD code required.

    At a Glance

    ProjectHasura GraphQL Engine v2.48.x
    LicenseApache 2.0
    Recommended PlanRamNode Cloud VPS 2GB or higher (4GB for heavier workloads)
    OSUbuntu 22.04 / 24.04 LTS
    StackDocker Compose (Hasura, PostgreSQL 16, Caddy)
    Default Port8080 (proxied via Caddy on 443)
    Estimated Setup Time15–20 minutes

    Prerequisites

    • A RamNode VPS with at least 2 GB RAM, 1 vCPU, 25 GB SSD
    • A domain or subdomain pointed to your VPS IP (e.g., hasura.yourdomain.com)
    • SSH access with a non-root sudo user
    • Basic familiarity with Docker and the Linux command line
    1

    Initial Server Setup

    Update and install essentials
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y curl wget git ufw apt-transport-https ca-certificates gnupg lsb-release

    Configure the Firewall

    UFW rules
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow 22/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable

    Port 8080 stays closed — all traffic routes through Caddy on port 443.

    Set Hostname and Timezone

    Hostname and timezone
    sudo hostnamectl set-hostname hasura-vps
    sudo timedatectl set-timezone America/Chicago
    2

    Install Docker and Docker Compose

    Install Docker
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
    
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
    Add user to docker group
    sudo usermod -aG docker $USER
    newgrp docker
    Verify
    docker --version
    docker compose version
    3

    Create the Environment File

    Create project directory
    mkdir -p ~/hasura && cd ~/hasura
    Generate secrets
    openssl rand -base64 32

    Run that command twice for two different values, then create the .env file:

    .env
    POSTGRES_USER=hasura
    POSTGRES_PASSWORD=REPLACE_WITH_GENERATED_SECRET_1
    POSTGRES_DB=hasura
    
    HASURA_GRAPHQL_ADMIN_SECRET=REPLACE_WITH_GENERATED_SECRET_2
    HASURA_DOMAIN=hasura.yourdomain.com
    Lock permissions
    chmod 600 .env
    4

    Create the Docker Compose File

    docker-compose.yml
    services:
      postgres:
        image: postgres:16
        restart: unless-stopped
        volumes:
          - pg_data:/var/lib/postgresql/data
        environment:
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: ${POSTGRES_DB}
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
          interval: 10s
          timeout: 5s
          retries: 5
        networks:
          - hasura-net
    
      graphql-engine:
        image: hasura/graphql-engine:v2.48.12
        restart: unless-stopped
        depends_on:
          postgres:
            condition: service_healthy
        environment:
          HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
          PG_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
          HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
          HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
          HASURA_GRAPHQL_DEV_MODE: "false"
          HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
          HASURA_GRAPHQL_LOG_LEVEL: warn
          HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
          HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
          HASURA_GRAPHQL_CORS_DOMAIN: "https://${HASURA_DOMAIN}"
        networks:
          - hasura-net
    
      caddy:
        image: caddy:2
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
          - "443:443/udp"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - caddy_data:/data
          - caddy_config:/config
        depends_on:
          - graphql-engine
        networks:
          - hasura-net
    
    volumes:
      pg_data:
      caddy_data:
      caddy_config:
    
    networks:
      hasura-net:
        driver: bridge

    Key decisions: Dev mode is false for production. Admin secret is required. Telemetry is disabled. Caddy handles automatic HTTPS via Let's Encrypt.

    5

    Configure the Caddy Reverse Proxy

    Caddyfile
    {$HASURA_DOMAIN} {
        reverse_proxy graphql-engine:8080
    
        header {
            -Server
            Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
            X-Content-Type-Options "nosniff"
            X-Frame-Options "DENY"
            Referrer-Policy "strict-origin-when-cross-origin"
        }
    
        log {
            output file /data/access.log {
                roll_size 10mb
                roll_keep 5
            }
        }
    }

    Caddy automatically provisions a Let's Encrypt certificate, strips the Server header, and adds security headers to all responses.

    6

    Deploy the Stack

    Start all services
    cd ~/hasura
    docker compose up -d
    Watch logs
    docker compose logs -f
    Verify containers
    docker compose ps

    You should see postgres, graphql-engine, and caddy all running.

    7

    Access the Hasura Console

    Navigate to https://hasura.yourdomain.com/console and enter the admin secret from your .env file.

    Quick Verification

    Create a test table from the Data tab, then run this mutation from the API tab:

    Test mutation
    mutation {
      insert_test_items(objects: [
        { name: "First item" },
        { name: "Second item" }
      ]) {
        returning {
          id
          name
        }
      }
    }
    8

    PostgreSQL Tuning for Small VPS

    postgresql.conf
    # Memory
    shared_buffers = 512MB
    effective_cache_size = 1GB
    work_mem = 8MB
    maintenance_work_mem = 128MB
    
    # WAL
    wal_buffers = 16MB
    checkpoint_completion_target = 0.9
    max_wal_size = 1GB
    
    # Connections
    max_connections = 100
    
    # Query Planner
    random_page_cost = 1.1
    effective_io_concurrency = 200

    Mount the config in your postgres service:

    docker-compose.yml postgres update
      postgres:
        image: postgres:16
        restart: unless-stopped
        volumes:
          - pg_data:/var/lib/postgresql/data
          - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
        command: postgres -c config_file=/etc/postgresql/postgresql.conf
    Apply changes
    docker compose down
    docker compose up -d
    9

    Automated Backups

    Create backup script
    mkdir -p ~/hasura/backups
    
    cat > ~/hasura/backup.sh << 'SCRIPT'
    #!/bin/bash
    set -euo pipefail
    
    BACKUP_DIR="$HOME/hasura/backups"
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    BACKUP_FILE="${BACKUP_DIR}/hasura_${TIMESTAMP}.sql.gz"
    RETENTION_DAYS=14
    
    source "$HOME/hasura/.env"
    
    docker exec hasura-postgres-1 pg_dumpall -U "${POSTGRES_USER}" | gzip > "${BACKUP_FILE}"
    
    find "${BACKUP_DIR}" -name "hasura_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
    
    echo "[$(date)] Backup completed: ${BACKUP_FILE}"
    SCRIPT
    
    chmod +x ~/hasura/backup.sh
    Schedule daily backup at 3 AM
    (crontab -l 2>/dev/null; echo "0 3 * * * $HOME/hasura/backup.sh >> $HOME/hasura/backups/backup.log 2>&1") | crontab -
    10

    Metadata Export and Version Control

    Install Hasura CLI
    curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
    Initialize and export metadata
    cd ~/hasura
    hasura init hasura-project --endpoint https://hasura.yourdomain.com --admin-secret YOUR_ADMIN_SECRET
    cd hasura-project
    hasura metadata export

    Commit the metadata/ directory to Git. Restore with hasura metadata apply.

    11

    Resource Monitoring

    Install ctop
    sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-linux-amd64 -O /usr/local/bin/ctop
    sudo chmod +x /usr/local/bin/ctop

    Run ctop for a live view of container CPU, memory, and network usage.

    Health Endpoint

    Check health
    curl -s https://hasura.yourdomain.com/healthz

    A 200 OK confirms the engine is running. Point an uptime monitor at /healthz for alerting.

    12

    Updating Hasura

    Update the image tag in docker-compose.yml, then pull and restart:

    Update Hasura
    cd ~/hasura
    docker compose pull graphql-engine
    docker compose up -d graphql-engine

    Always take a database backup before upgrading.

    Security Checklist

    • Admin secret set to a strong, unique value
    • Dev mode set to false
    • Port 8080 not exposed in UFW
    • CORS restricted to your application's domain
    • TLS active with auto-renewing certificates
    • PostgreSQL credentials are randomly generated
    • Database backups running and verified
    • Metadata exported and stored in version control

    Troubleshooting

    • Cannot connect to PostgreSQL — Verify postgres is healthy with docker compose ps. Check .env credentials match.
    • 502 Bad Gateway from Caddy — The graphql-engine isn't ready yet. Check docker compose logs graphql-engine.
    • SSL certificate not issuing — Confirm DNS A record and ports 80/443 are open in UFW.
    • High memory usage — Reduce shared_buffers and work_mem. Monitor with ctop or docker stats.