Hasura – Installation and Your First Query

June 27, 20264 min readUpdated 8/21/2026

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-default

Lines 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-default

Decide 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 doctor

Then 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-start

The 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.