Back to Blog
Tutorial7 min read2026-07-11

How to Deploy a Hasura Instance with PandaStack

Run your own Hasura GraphQL Engine: migrations and metadata as code, a production Dockerfile, the env vars that matter, and go-live via git push.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Hasura GraphQL Engine gives you an instant GraphQL API over PostgreSQL — point it at a database, track your tables, and you have queries, mutations, and subscriptions with a permission system on top. Hasura Cloud exists, but running your own instance is a legitimate choice: you keep the database and the engine in one place, one bill, one dashboard, and there's no per-request metering to think about.

Hasura ships as a Docker image, which makes it an unusual deploy: there's no application code to build. Your "repo" is a Dockerfile plus your migrations and metadata. Done right, that repo becomes the single source of truth for your entire API — every table, relationship, and permission rule versioned in Git. Here's the full setup.

Project structure: migrations and metadata as code

Don't click your schema together in the console and hope for the best. Use the Hasura CLI so every change lands in Git:

# Install the CLI
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash

# Scaffold a project (config v3)
hasura init hasura-app
cd hasura-app

This gives you:

hasura-app/
├── config.yaml
├── migrations/     # SQL, versioned, forward + down
├── metadata/       # tracked tables, relationships, permissions
└── seeds/

During development, run a local Hasura + Postgres and open the console through the CLI — not the one served by the engine directly:

hasura console --endpoint http://localhost:8080 --admin-secret myadminsecret

The difference matters: the CLI console writes every change you make to migrations/ and metadata/ on disk. The built-in console at :8080 applies changes to the running instance only, and they'll silently drift from your repo. This is the single most common self-hosted Hasura mistake.

When you change the schema, migrations get created for you; you can also write them by hand:

hasura migrate create add_orders_table --database-name default
hasura migrate apply --database-name default
hasura metadata apply

The Dockerfile

Hasura publishes a cli-migrations image variant that applies your migrations and metadata automatically at startup, before the engine begins serving. That's exactly what you want on a platform deploy — no separate release step to orchestrate:

FROM hasura/graphql-engine:v2.36.1.cli-migrations-v3

COPY migrations /hasura-migrations
COPY metadata /hasura-metadata

# Production defaults — override per-environment as needed
ENV HASURA_GRAPHQL_ENABLE_CONSOLE=false
ENV HASURA_GRAPHQL_DEV_MODE=false
ENV HASURA_GRAPHQL_ENABLED_LOG_TYPES="startup, http-log, webhook-log, websocket-log"

EXPOSE 8080

Notes on the details:

  • Pin the version tag. latest on a schema-owning service is asking for a surprise upgrade mid-rollout.
  • The image looks for migrations at /hasura-migrations and metadata at /hasura-metadata — those paths are the contract, keep them exact.
  • Hasura listens on port 8080 by default. Most platforms detect the exposed port; if you need to change it, HASURA_GRAPHQL_SERVER_PORT controls it.
  • Startup ordering is handled for you: migrations run first, then metadata is applied, then the engine serves traffic. If a migration fails, the container exits instead of serving a half-migrated schema — which is the behavior you want.

Environment variables that actually matter

VariablePurpose
HASURA_GRAPHQL_DATABASE_URLPostgres connection string for your data
HASURA_GRAPHQL_METADATA_DATABASE_URLWhere Hasura stores its own metadata (can be the same DB)
HASURA_GRAPHQL_ADMIN_SECRETNon-negotiable in production
HASURA_GRAPHQL_JWT_SECRETJWT config if your app authenticates users
HASURA_GRAPHQL_UNAUTHORIZED_ROLERole for requests with no auth (omit to reject them)
HASURA_GRAPHQL_CORS_DOMAINLock this to your frontend's origin
HASURA_GRAPHQL_PG_CONNECTIONSConnection pool size (default 50)

One gotcha worth calling out now: Hasura does not read a plain DATABASE_URL — it wants the HASURA_GRAPHQL_-prefixed names. We'll deal with that in a minute.

Deploying on PandaStack

1. Provision the database

Create a managed PostgreSQL instance (14.x and 16.x are both available) from the dashboard. It's a real Postgres orchestrated by KubeBlocks on Kubernetes, with scheduled daily backups — 7-day retention on the free tier, 15 on Pro, 30 on Premium.

2. Connect the repo

Push the project — Dockerfile, migrations/, metadata/ — to GitHub and connect it as a container app. PandaStack sees the Dockerfile and builds it with rootless BuildKit in an ephemeral Kubernetes job; you can watch the build logs stream live while it runs. Attach the database you created to the app.

3. Wire the connection string

Attaching the database injects DATABASE_URL into the app automatically. Since Hasura wants its own variable names, add two environment variables in the app's settings, using that same connection string as the value:

HASURA_GRAPHQL_DATABASE_URL=<the injected DATABASE_URL value>
HASURA_GRAPHQL_METADATA_DATABASE_URL=<same value>

Using one database for both data and metadata is fine at this scale — Hasura keeps its metadata in a separate hdb_catalog schema, so it won't collide with your tables.

While you're in the env var screen, set the rest:

HASURA_GRAPHQL_ADMIN_SECRET=<long random string>
HASURA_GRAPHQL_CORS_DOMAIN=https://yourapp.example.com

Generate the admin secret properly — openssl rand -hex 32 — and treat it like a database password. Anyone holding it has full read/write on everything.

4. Push and verify

git push origin main

The build runs, migrations apply on boot, and the instance goes live. Verify it:

curl https://<your-app-url>/healthz
# OK

curl https://<your-app-url>/v1/version
# {"version":"v2.36.1"}

Then confirm the API is actually locked down — this should fail without the admin secret:

curl -X POST https://<your-app-url>/v1/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ __typename }"}'

If that returns data instead of an auth error, stop and check HASURA_GRAPHQL_ADMIN_SECRET before doing anything else.

Production gotchas

Connection pool vs. plan limits. Hasura's default pool is 50 connections — exactly the connection limit on PandaStack's free database tier. That leaves zero headroom for psql, a second Hasura replica, or anything else. Set HASURA_GRAPHQL_PG_CONNECTIONS=20 on the free tier; on Pro (300 connections) or Premium (1000) the default is fine.

Console off, CLI on. Keep HASURA_GRAPHQL_ENABLE_CONSOLE=false in production. When you need the console against prod, tunnel it through the CLI from your machine — changes get written to your repo, and the next git push redeploys them through the same migration path as everything else:

hasura console \
  --endpoint https://<your-app-url> \
  --admin-secret $HASURA_GRAPHQL_ADMIN_SECRET

Auth for real users. The admin secret is for you, not your frontend. Configure HASURA_GRAPHQL_JWT_SECRET to validate tokens from your auth provider, define roles with row-level permissions in metadata, and only set HASURA_GRAPHQL_UNAUTHORIZED_ROLE if you genuinely have public data — an anonymous role with carefully scoped select permissions, nothing more.

Subscriptions and scale-to-zero. GraphQL subscriptions hold WebSocket connections open. On PandaStack's free tier, idle apps scale to zero via KEDA and cold-start on the next request — fine for a staging instance or a hobby API, but if clients rely on long-lived subscriptions in production, run it on a paid compute tier where the instance stays warm.

Rolling back. Because your schema lives in migrations/ and your permissions in metadata/, a bad release is a git revert plus a push. The down migrations you wrote (you did write down migrations?) run through the same pipeline. This is the payoff for resisting the built-in console back in step one.

That's the whole system: one repo holding the engine version, the schema, and the permission rules, deployed by a push, with the database wired in next to it. If you want to see it run, [pandastack.io](https://pandastack.io) is the place to try it.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also