Docker Images
Pre-built images are published to the GitHub Container Registry on every push to main:
ghcr.io/roostorg/coop-server # API server
ghcr.io/roostorg/coop-worker # Background worker
ghcr.io/roostorg/coop-client # Frontend (nginx)
ghcr.io/roostorg/coop-migrations # One-shot database migrations runner
Images are tagged with latest, the git SHA, and semver tags on release. The
coop-migrations image is only rebuilt when something under db/ changes (e.g.
a new migration script) or on a release, so its tags track the schema rather
than every server/client change.
Quick start
Pull the published images and run the full stack locally with a single command:
docker compose -f docker-compose.images.yaml up -d
Warning
docker-compose.images.yamluses.env.docker, which ships working defaults for local evaluation but includes placeholder secrets. Review and replace secrets before any non-local deployment.
This starts:
- Coop server on port 8080
- Coop client (nginx) on port 3000
- Postgres (pgvector), Redis, ScyllaDB, ClickHouse
- Database migrations (run automatically)
- A seed service that creates an admin user with a randomly generated password
Get your login credentials
The seed service prints the generated credentials to its logs:
docker compose -f docker-compose.images.yaml logs seed
Look for the output at the bottom:
============================================
Login: admin@coop.local
Password: <randomly-generated>
============================================
Then open http://localhost:3000 and log in.
On subsequent startups (with the same volumes), the seed service detects the existing user and skips — your credentials remain the same. If you lose the password, tear down with -v to reset.
Create additional users
docker compose -f docker-compose.images.yaml exec server \
node bin/create-org-and-user.js \
--name "My Org" \
--email "you@example.com" \
--website "https://example.com" \
--firstName "Jane" \
--lastName "Doe" \
--password "your-password"
Tear down
# Stop containers, keep data volumes
docker compose -f docker-compose.images.yaml down
# Stop containers and wipe all data
docker compose -f docker-compose.images.yaml down -v
Image details
| Image | Dockerfile | Build target | Base |
|---|---|---|---|
coop-server | Dockerfile | build_server | node:24-bullseye-slim + dumb-init |
coop-worker | Dockerfile | build_worker_runner | node:24-bullseye-slim + dumb-init |
coop-client | client/Dockerfile | serve | nginx:1.27-bookworm |
coop-migrations | db/Dockerfile | (final stage) | node:24-bullseye-slim |
The client image serves the Vite-built SPA via nginx and proxies /api/ requests (including /api/v1/graphql) to a backend service named server on port 8080.
Running migrations
The coop-migrations image bundles the migration scripts under db/ together
with the migrator engine, so it can run as a one-shot task (an ECS RunTask, a
Kubernetes Job, or the migrations service in docker-compose.images.yaml).
It exposes the same npm run db:* commands used in local development, so the
invocation matches what you’d run from the repo root:
# Apply all pending migrations to an existing prod database
docker run --rm --env-file .env.docker ghcr.io/roostorg/coop-migrations:latest \
npm run db:update -- --db api-server-pg --env prod
Supported --db values are api-server-pg, scylla, and clickhouse.
Connection settings come from environment variables (see db/.env.example),
which must be present for any command since the database configs are read at
startup.
What --env controls
--env (staging or prod) only affects seed scripts, not migrations:
- Migration scripts run in every environment.
- A seed named
*.seed.<env>.sqlruns only when--envmatches it. Seed files are timestamp-prefixed; the repo currently shipsdb/src/scripts/api-server-pg/2025.12.01T00.00.01.initial-test-data.seed.staging.sql, which creates a sample org with default-password users — so use--env prodin production to skip it. Running--env stagingagainst a prod database would seed that test data. db:createignores--envfunctionally and is allowed in prod, so you can provision schemas for self-hosted Scylla/ClickHouse (which have no managed “create the database” step).db:cleananddb:dropare destructive and reject--env prodas a safety guard.