The fastest way to a working Hasura is Docker Compose: the engine in one service, Postgres in another, both up with one command. This lesson gets that running, explains the parts of it that regularly go wrong, and ends with a real query.
The compose file
Two services. Postgres holds the data; Hasura reads it.
services:
postgres:
image: postgres:16-alpine
container_name: stayhub-postgres
restart: unless-stopped
environment:
POSTGRES_USER: stayhub
POSTGRES_PASSWORD: stayhub
POSTGRES_DB: stayhub
ports:
- "5433:5432"
volumes:
- stayhub-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U stayhub -d stayhub"]
interval: 5s
timeout: 5s
retries: 10
hasura:
image: hasura/graphql-engine:v2.42.0
container_name: stayhub-hasura
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
ports:
- "8081:8080"
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://stayhub:stayhub@postgres:5432/stayhub
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_DEV_MODE: "true"
...
HASURA_GRAPHQL_ADMIN_SECRET: stayhub-admin-secret
...
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
HASURA_GRAPHQL_EXPERIMENTAL_FEATURES: naming_convention
HASURA_GRAPHQL_DEFAULT_NAMING_CONVENTION: graphql-defaultLines shown as ... are elided — the real file also sets log types
and a JWT secret, which lessons 16 and 8 cover.
The port mistake everybody makes once
Look at those two ports carefully. Postgres is published on the host as 5433, because
5432 was already taken by a native install. But the connection string Hasura uses says
5432.
Both are right. "5433:5432" means host 5433 maps to container 5432. Hasura
is inside the compose network, so it reaches Postgres by service name on the container’s own
port. The 5433 shift only exists for tools on your machine. Putting 5433 in
HASURA_GRAPHQL_DATABASE_URL produces a connection refused that looks like a credentials
problem, and people lose an hour to it.
So: psql on your laptop connects to localhost:5433; Hasura connects to
postgres:5432.
The healthcheck is not decoration
Hasura crashes on boot if Postgres is not accepting connections yet, and Compose starts both at
once. depends_on: condition: service_healthy holds the engine back until
pg_isready succeeds.
Note the check uses a command the image actually ships. A curl-based healthcheck on an image without curl reports unhealthy forever while the service works perfectly — and Compose then blames the wrong container.
The admin secret
HASURA_GRAPHQL_ADMIN_SECRET is the single most important variable here, and it is
worth being blunt about what it is.
There is no username. That one string is the entire credential, and it grants the
built-in admin role, which bypasses every permission rule you will ever write. It
belongs in the console, in server-side scripts, and nowhere else. If it reaches a browser bundle you
have handed every visitor unrestricted read and write access to your database.
Without it set, an unprotected engine is open to anyone who can reach the port. Set it from the first run, not later.
Starting it
docker compose up -d
curl -s http://localhost:8081/healthz # OK
curl -s http://localhost:8081/v1/version # {"server_type":"ce","version":"v2.42.0"}server_type: ce is Community Edition. Worth checking early, because several features
people expect — response caching, allow-lists, rate limiting — are Enterprise and Cloud
only, and finding that out during a launch is unpleasant.
The console is at http://localhost:8081. It will ask for the admin secret.
Your first query
Tables are not exposed until you say so. In the console, open Data, find your tables and track them. Then, from API:
query {
properties(limit: 2) {
title
city
pricePerNight
}
}{"data":{"properties":[
{"title":"Modern Condo, Downtown Skyline","city":"Seattle","pricePerNight":198.00},
{"title":"Historic Adobe near the Plaza","city":"Santa Fe","pricePerNight":225.00}]}}The same thing over HTTP, which is all the console is doing:
curl -s -X POST http://localhost:8081/v1/graphql \
-H 'x-hasura-admin-secret: stayhub-admin-secret' \
-H 'Content-Type: application/json' \
-d '{"query":"query { properties(limit:2) { title city pricePerNight } }"}'Why the fields are camelCase
The database columns are price_per_night and first_name. The API returns
pricePerNight and firstName because of one setting:
HASURA_GRAPHQL_EXPERIMENTAL_FEATURES: naming_convention
HASURA_GRAPHQL_DEFAULT_NAMING_CONVENTION: graphql-defaultDecide this on day one. It changes every field name in the API, so turning it on later is a
breaking change for every client. It also changes arguments — with it on, sorting is
orderBy and not order_by, which is a surprise when you copy an example
from documentation written without it.
Installing v3 (DDN)
On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.
v3 installs a CLI rather than pulling an image. Docker Compose v2.20 or later is required.
curl -L https://graphql-engine-cdn.hasura.io/ddn/cli/v4/get.sh | bash
ddn doctorThen you scaffold a project instead of configuring a container:
ddn supergraph init mysupergraph
ddn connector init my_connector -i # choose hasura/postgres, give it the URL
ddn connector introspect my_connector
ddn model add my_connector '*'
ddn supergraph build local
ddn run docker-startThe difference worth noticing is where the configuration lives. In v2 it is environment variables
on a running container plus state inside the engine. In v3 it is files in a directory that you commit
— and ddn model add is the equivalent of clicking “track”.
One thing does not transfer: there is no admin secret in v3. The credential model is different, and every script you have that authenticates with one needs a different answer. Lesson 18 covers that in full.
Next
You have an engine serving a schema you never designed. Lesson 3 explains where that schema actually lives — because it is not in your database, and it is not in your code.