PostgREST turns a Postgres schema into a REST API with no glue code - you point it at a database and it serves the tables. I wanted the smallest local setup that gets it running, so I followed the docs on Containerized PostgREST with Docker Compose, added a Swagger UI service to browse the generated API, and lined up the environment variables with Tutorial 0 - Get it Running. The one extra piece is a seed.sql mounted into the db, so the roles PostgREST needs exist the moment the container starts.

Docker Compose

Three services. server is PostgREST itself, talking to Postgres as the authenticator role and exposing the api schema; anonymous requests fall back to the web_anon role. db is plain Postgres with the data volume and seed script mounted in. swagger gives you an interactive UI on 8080 pointed at the API.

docker-compose.yml
services:
server:
image: postgrest/postgrest
ports:
- "3000:3000"
environment:
PGRST_DB_URI: postgres://authenticator:mysecretpassword@db:5432/app_db
PGRST_DB_SCHEMAS: api
PGRST_DB_ANON_ROLE: web_anon
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
depends_on:
- db
db:
image: postgres
ports:
- "5432:5432"
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: password
volumes:
- ./pgdata:/var/lib/postgresql/data
- ./seed.sql:/docker-entrypoint-initdb.d/seed.sql
swagger:
image: swaggerapi/swagger-ui
ports:
- "8080:8080"
expose:
- "8080"
environment:
API_URL: http://localhost:3000/

The seed script

Postgres runs any .sql file in /docker-entrypoint-initdb.d/ once, on first startup. That is where the schema and roles go. The api schema is what PostgREST exposes; web_anon is the read-only role anonymous callers get; and authenticator is the login role PostgREST connects as, which can switch into web_anon because it is granted to it. Match mysecretpassword to the PGRST_DB_URI above or the API will not connect.

seed.sql
create schema api;
create table api.todos (
id int primary key generated by default as identity,
done boolean not null default false,
task text not null,
due timestamptz
);
insert into api.todos (task) values
('finish tutorial 0'), ('pat self on back');
create role web_anon nologin;
grant usage on schema api to web_anon;
grant select on api.todos to web_anon;
create role authenticator noinherit login password 'mysecretpassword';
grant web_anon to authenticator;

Run it

Bring it up with docker compose up -d, then hit http://localhost:3000/todos for the live API and http://localhost:8080 for the Swagger UI. No controllers, no serializers, no route definitions - the schema is the API.