Skip to main content

Deployment

The included Docker Compose stack provides the infrastructure used by stac-catalog: PostgreSQL/pgSTAC, STAC FastAPI, TiTiler, and STAC Browser.

For the CLI workflow, see CLI reference. For input fields and filename conventions, see Metadata.

Architecture

stac-catalog

┌───────────────┴────────────────┐
│ │
Local development Production
│ │
│ direct host access │ --prod
│ ▼
│ generator container
│ │
└───────────────┬────────────────┘

private Docker network

┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
PostgreSQL / pgSTAC TiTiler STAC FastAPI
│ │
│ ▼
│ STAC Browser

local: 127.0.0.1:5432
prod: no published port

Locally, the host stac-catalog CLI connects to pgSTAC through a loopback-only PostgreSQL port. In production, stac-catalog load --prod and stac-catalog sync --prod run the database-dependent operation inside the generator container, so PostgreSQL stays private on the Docker network.

Services

ServicePurposeLocal development address
dbPostgreSQL with pgSTAC127.0.0.1:5432
apiSTAC FastAPI backed by pgSTAChttp://localhost:8081
titilerRaster rendering and preview APIhttp://localhost:8000
browserSTAC Browserhttp://localhost:8082
generatorContainerized stac-catalog CLI used by --prodno persistent port

The generator is an on-demand tool (it is in the tools compose profile), so it does not run with a normal docker compose up.

Environment

cp .env.example .env
POSTGRES_DB=stac
POSTGRES_USER=postgres
POSTGRES_PASSWORD=stac

Do not commit .env. Inside the Docker network, pgSTAC is reached as:

POSTGRES_HOST=db
POSTGRES_PORT=5432

Local development

For local development, the host CLI needs access to PostgreSQL and TiTiler.

Bind PostgreSQL only to the loopback interface (already the default in the compose file):

ports:
- "127.0.0.1:5432:5432"

Start and check the stack:

docker compose config # validate
docker compose up -d # starts db, api, titiler, browser
docker compose ps # check status

A typical local session is: prepare input/, run stac-catalog sync --source csv --input ./input --output ./output, then browse at http://localhost:8082. See Getting started.

Production

Production should keep PostgreSQL private inside the Docker network — only the services that need to be public are routed through the reverse proxy.

Private PostgreSQL

Remove the ports block from the db service in production:

db:
image: ghcr.io/stac-utils/pgstac:v0.9.8

environment:
POSTGRES_DB: ${POSTGRES_DB:-stac}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

networks:
- stac

There should be no ports: block (no 5432:5432 or 127.0.0.1:5432:5432) on the production db service. The database is then reachable only as db:5432 from containers on the private Compose network.

Production CLI

Use the same CLI with --prod (see CLI reference → --prod):

# Load previously generated output
stac-catalog load --prod

# Generate + load CSV metadata
stac-catalog sync --prod --source csv

# Generate + load from a Google Sheet
stac-catalog sync --prod --source gsheet --config stac-config.yaml

--prod handles the container execution needed to reach private pgSTAC, so you do not need to run docker compose run --rm manually. It uses the standard mounted paths (./input/data/input, ./output/data/output), and for Google Sheets it also mounts the config and referenced credentials. Do not pass --input, --output, or --dsn with --prod.

Service exposure

A typical deployment exposes three public services, each behind HTTPS:

https://stac.example.org → STAC Browser
https://stac-api.example.org → STAC FastAPI
https://tiles.example.org → TiTiler
PostgreSQL → private Docker network only

STAC FastAPI can remain a read-only public API. The production loading workflow does not require exposing PostgreSQL or enabling public STAC Transactions write endpoints.

Public URLs

Two settings should change for a public deployment.

STAC Browser — point SB_catalogUrl at the public STAC API:

SB_catalogUrl: "https://stac-api.example.org"

TiTiler — keep the internal address, but write the public URL into STAC metadata:

TITILER_URL: http://titiler:8000 # internal only
TITILER_PUBLIC_URL: https://tiles.example.org # written into STAC assets

http://titiler:8000 works only inside the Compose network and should not be written into public STAC assets.

Lifecycle

Logs

docker compose logs -f # all long-running services
docker compose logs -f api db titiler browser # specific services
docker compose logs --tail=100 db api titiler browser # recent lines only

Restart

docker compose restart api # one service
docker compose restart api browser # several
docker compose up -d --force-recreate # recreate after a compose change
docker compose build generator # rebuild after Python/Dockerfile changes

Stop

docker compose stop # stop without removing containers
docker compose start # start them again
docker compose down # remove containers + network, keep the DB volume

Database persistence

pgSTAC data is stored in the named volume stac_db. docker compose down preserves this volume. Inspect volumes with docker volume ls.

:::danger docker compose down -v deletes the database The -v flag deletes Compose volumes — including the pgSTAC data. Only use it when you intend a full reset. :::

To reset the local database intentionally:

docker compose down -v
docker compose up -d
stac-catalog sync --source csv --input ./input --output ./output

Verify the stack

Local:

curl http://localhost:8081/ # STAC API landing
curl http://localhost:8081/collections # collections
curl -I http://localhost:8000/docs # TiTiler docs
# STAC Browser: http://localhost:8082

Production: use the configured public domains, and confirm with docker compose ps that the db service shows no host mapping such as 0.0.0.0:5432->5432/tcp or 127.0.0.1:5432->5432/tcp.

Production checklist

For a public deployment:

  • set a strong PostgreSQL password;
  • do not publish the PostgreSQL port;
  • put STAC FastAPI, STAC Browser, and TiTiler behind HTTPS/reverse proxy;
  • update SB_catalogUrl to the public STAC API URL;
  • update TITILER_PUBLIC_URL to the public TiTiler URL;
  • keep TITILER_URL=http://titiler:8000 for internal container communication;
  • use stac-catalog load --prod or stac-catalog sync --prod for private database access;
  • restrict network and firewall exposure;
  • back up the PostgreSQL/pgSTAC data volume (stac_db);
  • consider pinning stac-fastapi-pgstac, TiTiler, and STAC Browser image versions instead of using latest.