Deployment guide

Self-Host Hasura on a VPS: Instant GraphQL API on PostgreSQL

Deploy on a VPS Cloud →

Tutorial

Self-Host Hasura on a VPS: Instant GraphQL API on PostgreSQL

Self-hosting6 min read4 steps

Writing the same CRUD code for every table, then the same access checks, then the same API documentation: that is the work Hasura GraphQL Engine removes. This open-source engine (Apache-2.0 licensed) connects to a PostgreSQL database and immediately exposes a complete GraphQL API, with relationships, filters, aggregates and real-time subscriptions, protected by permissions declared per role. This guide installs it with Docker Compose on a VPS, next to its own PostgreSQL database, publishes it over HTTPS behind nginx and sets the security rules you must not forget.

Contents· Why a generated API rather than a hand-written backend1/10
  1. 01Why a generated API rather than a hand-written backend
  2. 02What Hasura brings
  3. 03Hasura versus other ways to expose PostgreSQL
  4. 04Prerequisites
  5. 05Install Hasura with Docker Compose
  6. 06The admin secret never leaves the server
  7. 07Create a table and query it
  8. 08Authenticate your users and set permissions
  9. 09Troubleshooting
  10. 10One-click install from the ServOrbit Marketplace

Why a generated API rather than a hand-written backend

In most applications, a large part of the server code merely translates HTTP requests into SQL: list, filter, paginate, create, update, delete, checking each time that the user is allowed to. That code is repetitive, it needs tests, and every new column means touching it again.

Hasura starts from the database schema. A tracked table becomes a GraphQL type, a foreign key becomes a nested field, and a permission rule written once applies to every query. Your own business logic — a payment, an email, a calculation — stays in your code, which Hasura calls by webhook. You keep the PostgreSQL database: nothing is locked into a proprietary format.

What Hasura brings

  • Instant GraphQL API — queries, mutations, filters, sorting, pagination and aggregates on every tracked table.
  • Relationships — foreign keys become nested fields, queryable in a single request.
  • Real-time subscriptions — a client subscribes to a query and receives changes over WebSocket.
  • Role-based permissions — row- and column-level rules, driven by the claims of a JWT.
  • Actions and triggers — webhooks called by a mutation, by a data change or on a cron schedule.
  • Console and REST endpoints — web administration, GraphiQL explorer, and turning a query into a REST route.

Hasura versus other ways to expose PostgreSQL

Scroll the table

HasuraHand-written APIPostgREST
API generated from the schemaYes, GraphQL and RESTNoYes, REST
Nested relationships in one requestYesDepends on the codeYes, via embedding
Real timeGraphQL subscriptionsTo be builtNo
PermissionsPer role, row and column, in the consoleIn the codePostgreSQL roles and RLS
Business logicActions and triggers as webhooksFreeSQL functions
Admin interfaceWeb consoleNoNo

Prerequisites

An Ubuntu 24.04 VPS with 1 GB of RAM is enough to start: at rest, the Hasura engine and PostgreSQL use about 200 MB together. Plan for 2 GB as soon as the database grows or subscriptions multiply. You need Docker with the Compose plugin, and a domain name pointing to the VPS, with nginx and an HTTPS certificate: your API will be called by browsers and mobile apps, so it must answer on a stable, encrypted address.

Install Hasura with Docker Compose

  1. Prepare the secrets

    Create the application directory and a .env file holding the database password and the admin secret, the API's master key. The file is readable by root only.

    mkdir -p /opt/hasura && cd /opt/hasura
    cat > .env <<EOF
    DB_PASSWORD=$(openssl rand -hex 16)
    ADMIN_SECRET=$(openssl rand -hex 24)
    EOF
    chmod 600 .env
  2. Create the Compose file

    This docker-compose.yml starts PostgreSQL on the stack's internal network, with no published port, then Hasura once the database is ready. Hasura's port is published on the loopback only; the console is served from the image, with no call to the vendor's CDN, and telemetry is off.

    services:
      postgres:
        image: postgres:17.11-alpine
        restart: unless-stopped
        environment:
          POSTGRES_DB: hasura
          POSTGRES_USER: hasura
          POSTGRES_PASSWORD: ${DB_PASSWORD}
        volumes:
          - hasura-pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U hasura -d hasura"]
          interval: 10s
          retries: 10
      hasura:
        image: hasura/graphql-engine:v2.51.3
        restart: unless-stopped
        depends_on:
          postgres:
            condition: service_healthy
        ports:
          - "127.0.0.1:8103:8080"
        env_file: .env
        environment:
          HASURA_GRAPHQL_DATABASE_URL: postgres://hasura:${DB_PASSWORD}@postgres:5432/hasura
          HASURA_GRAPHQL_ADMIN_SECRET: ${ADMIN_SECRET}
          HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
          HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
          HASURA_GRAPHQL_DEV_MODE: "false"
          HASURA_GRAPHQL_ENABLE_TELEMETRY: "false"
    volumes:
      hasura-pgdata: {}
  3. Start the stack

    On first start, Hasura creates its metadata tables in the hdb_catalog schema and connects the database as the default source. The /healthz endpoint answers OK when the engine is ready.

    docker compose up -d
    curl -fsS http://127.0.0.1:8103/healthz   # OK
  4. Publish over HTTPS behind nginx

    Add a server block for your domain, with a Let's Encrypt certificate. The Upgrade and Connection headers are required: subscriptions go through WebSocket.

    location / {
        proxy_pass http://127.0.0.1:8103;
        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-Forwarded-Proto $scheme;
    }

The admin secret never leaves the server

Without HASURA_GRAPHQL_ADMIN_SECRET, the console and the whole API are open to anyone who knows the address. With it, it grants full rights: read and write every table, run SQL, change the metadata. Keep it for the console and your admin scripts, and never put it in frontend or mobile app code.

Create a table and query it

Open https://api.example.com/console, enter the admin secret, then, in the Data tab, create a todos table with the columns id (auto-increment integer), title (text) and done (boolean). Click Track: the table immediately shows up in the explorer of the API tab. From an admin script, the same query is sent with the admin secret:

curl -s https://api.example.com/v1/graphql \
  -H "x-hasura-admin-secret: $ADMIN_SECRET" \
  -H 'content-type: application/json' \
  -d '{"query":"{ todos { id title done } }"}'

Authenticate your users and set permissions

Your users must not use the admin secret: they present a JWT issued by your identity provider (Logto, Authentik, Zitadel, Auth0…). Tell Hasura where to find the verification keys, and which role to give requests without a token, in the .env file:

HASURA_GRAPHQL_JWT_SECRET={"type":"RS256","jwk_url":"https://auth.example.com/oidc/jwks"}
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous

Restart the stack with docker compose up -d: the hasura service reads this file through env_file. The token must carry the x-hasura-default-role, x-hasura-allowed-roles and x-hasura-user-id claims. In each table's Permissions tab, then define what each role can read, insert or update — for example, the user role only sees rows where user_id equals X-Hasura-User-Id.

Troubleshooting

The console shows "invalid x-hasura-admin-secret": the secret you entered does not match the value in .env. Check it with grep ADMIN_SECRET /opt/hasura/.env, then reload the page.

Hasura keeps restarting: the database is unreachable. docker compose logs hasura shows the connection error; check that the password in the HASURA_GRAPHQL_DATABASE_URL URL is PostgreSQL's. If you changed it after the first start, PostgreSQL kept the old one: it only reads POSTGRES_PASSWORD when the volume is created.

Subscriptions drop: the reverse proxy does not pass the WebSocket upgrade. Add the Upgrade and Connection headers shown above, and raise proxy_read_timeout if your clients stay idle for long.

A table does not appear in the API: it exists in the database but is not tracked. Click Track in the Data tab, or track every table of the schema at once.

One-click install from the ServOrbit Marketplace

The ServOrbit Marketplace installs Hasura on your VPS without a command line: it deploys PostgreSQL and the Hasura engine, generates the admin secret and the database password, connects the database as the default source and publishes the API over HTTPS on a subdomain of your domain or on the VPS's free subdomain. The admin secret is shown in your client area; all that is left is to open the console and create your tables.

Your GraphQL API on PostgreSQL, on your own VPS

A ServOrbit Ubuntu 24.04 VPS with 1 GB of RAM is enough to get started with Hasura. One-click install from the Marketplace, PostgreSQL included, admin secret generated for you, HTTPS on your domain or on the VPS's free subdomain.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab