Dev compose
Walkthrough of the committed docker-compose.yml at the repo root. This stack is what contributors run locally when they work on the backend or webapp — it boots Postgres, Redis, MinIO, Mailpit, and (optionally) Keycloak in one command.
For the production counterpart, jump to compose-prod.yml diff. For hardening before you expose anything to the internet, read the hardening checklist first.
Quick reference
docker compose up -d # core services (Postgres, Redis, MinIO, Mailpit)
docker compose --profile keycloak up -d # add Keycloak for OIDC testing
docker compose ps # list services + health
docker compose logs -f <service> # tail logs
docker compose down # stop; named volumes preserved
docker compose down -v # stop AND wipe all data volumes
| Service | URL | Credentials | Purpose |
|---|---|---|---|
| Postgres | postgres://translately:translately@localhost:5432/translately |
user translately / pass translately |
Primary datastore |
| Redis | redis://localhost:6379 |
none (dev) | Cache, rate limits, sliding-window counters |
| MinIO API | http://localhost:9000 |
translately / translately-dev |
S3-compatible object storage |
| MinIO console | http://localhost:9001 |
translately / translately-dev |
Web UI for inspecting buckets |
| Mailpit | http://localhost:8025 (web), localhost:1025 (SMTP) |
no auth | Dev mail sink — catches every outbound email |
| Keycloak | http://localhost:8180 |
admin / admin |
Optional OIDC IdP (profile: keycloak) |
The stack uses Compose project name translately, so container names are deterministic (translately-postgres, translately-redis, etc.) — handy for docker exec.
Service-by-service
postgres — PostgreSQL 16
Image: postgres:16-alpine. Container: translately-postgres. Port 5432 bound to the host.
Environment:
| Variable | Dev value | Production analogue |
|---|---|---|
POSTGRES_USER |
translately |
POSTGRES_USER (env-file supplied) |
POSTGRES_PASSWORD |
translately |
POSTGRES_PASSWORD (required, no default) |
POSTGRES_DB |
translately |
POSTGRES_DB |
Volumes:
postgres-data:/var/lib/postgresql/data— named volume; survivesdocker compose down, wiped bydocker compose down -v../infra/postgres/init:/docker-entrypoint-initdb.d:ro— run-once init SQL. Ships01-keycloak-db.sql, which idempotently creates the auxiliarykeycloakdatabase when the Keycloak profile is enabled.
Healthcheck: pg_isready -U translately -d translately, polled every 5s.
Production changes:
- Use a managed Postgres (AWS RDS, Cloud SQL, Crunchy) or at least TLS +
scram-sha-256auth. The dev image ships with trust auth forlocalhost. - Swap the dev password for a 32-byte random secret (
openssl rand -hex 20). - Scheduled backups + periodic restore drills (quarterly minimum).
redis — Redis 7
Image: redis:7-alpine. Container: translately-redis. Port 6379 bound to the host.
Command overrides (applied via Compose command:):
--appendonly yes— enables AOF persistence so cache state survives restart.--maxmemory 256mb— dev cap; production is512mbincompose-prod.yml.--maxmemory-policy allkeys-lru— evict least-recently-used keys when full.
Volumes: redis-data:/data — persists the AOF log.
Healthcheck: redis-cli ping.
Production changes:
- Increase
--maxmemoryto match real load (start at 512mb; tune from metrics). - Add
--requirepassand a TLS listener if Redis is network-reachable beyond the Compose network. - Consider Redis Sentinel or a managed Redis for HA.
minio + minio-init — S3-compatible object storage
minio (image: minio/minio:latest, container: translately-minio) runs the server on ports 9000 (API) and 9001 (console).
Environment:
| Variable | Dev value | Production analogue |
|---|---|---|
MINIO_ROOT_USER |
translately |
MINIO_ROOT_USER (required) |
MINIO_ROOT_PASSWORD |
translately-dev |
MINIO_ROOT_PASSWORD (required) |
Volume: minio-data:/data.
Healthcheck: HTTP GET /minio/health/live.
minio-init (image: minio/mc:latest, container: translately-minio-init) is a one-shot sidecar. It waits for MinIO to become healthy, then uses mc to create the buckets the backend expects and marks translately-cdn anonymous-readable:
mc mb --ignore-existing local/translately-screenshots
mc mb --ignore-existing local/translately-cdn
mc mb --ignore-existing local/translately-imports
mc anonymous set download local/translately-cdn
Bucket purposes:
translately-screenshots— reviewer screenshot uploads for translation context.translately-cdn— published content bundles (public-read so browsers can fetch without signed URLs).translately-imports— staged source-file uploads before they’re parsed into the TM.
The sidecar has restart: "no" and exits 0 after it finishes — docker compose ps will show it as Exited (0), which is expected.
Production changes:
- Swap MinIO for the real thing (AWS S3, GCS via S3 interop, Cloudflare R2, Backblaze B2). The backend speaks the S3 API — set
TRANSLATELY_STORAGE_S3_ENDPOINT,_ACCESS_KEY,_SECRET_KEY,_REGION. - Apply lifecycle rules (expire screenshot uploads after N days).
- If staying on MinIO, enable TLS, swap the root password, and put it behind a reverse proxy.
- Only the CDN bucket should be anonymous-readable. Keep
-screenshotsand-importsprivate.
mailpit — Dev SMTP sink
Image: axllent/mailpit:latest. Container: translately-mailpit. Ports 1025 (SMTP) and 8025 (web UI).
Environment:
| Variable | Value | Why |
|---|---|---|
MP_SMTP_AUTH_ACCEPT_ANY |
true |
Accept any SMTP credentials — tests don’t need real auth. |
MP_SMTP_AUTH_ALLOW_INSECURE |
true |
Allow plaintext auth; dev only. |
MP_MAX_MESSAGES |
500 |
Ring-buffer cap on stored messages. |
Volume: mailpit-data:/data.
Healthcheck: GET /livez.
Every outbound email sent by the backend in dev lands in Mailpit — open http://localhost:8025 to see signup verifications, password resets, webhook failure alerts, etc.
Production changes: do not ship Mailpit. Point TRANSLATELY_MAIL_* at a real SMTP provider (Amazon SES, Postmark, SendGrid, Mailgun) with auth + TLS + a dedicated sender domain (SPF, DKIM, DMARC).
keycloak — Optional OIDC IdP
Image: quay.io/keycloak/keycloak:25.0. Container: translately-keycloak. Port 8180 bound to the host (maps to Keycloak’s internal 8080).
Activated only with docker compose --profile keycloak up -d. The core stack omits it so laptops aren’t burning the extra RAM.
Environment:
| Variable | Value | Notes |
|---|---|---|
KEYCLOAK_ADMIN / _PASSWORD |
admin / admin |
Master realm admin creds (dev only). |
KC_DB |
postgres |
Reuse the stack’s Postgres. |
KC_DB_URL |
jdbc:postgresql://postgres:5432/keycloak |
The keycloak DB is created by infra/postgres/init/01-keycloak-db.sql. |
KC_DB_USERNAME / _PASSWORD |
translately / translately |
Shares the app Postgres role. |
KC_HOSTNAME_STRICT |
false |
Disable strict hostname checks for localhost. |
KC_HTTP_ENABLED |
true |
HTTP for dev; TLS required in prod. |
Volumes: ./infra/keycloak/realms:/opt/keycloak/data/import:ro — seed realms/clients for testing (currently a placeholder).
Depends on Postgres being healthy (Keycloak needs its DB up).
Production changes: Keycloak gets its own deployment (not hobbled into a single Compose file). See the Phase 7 Helm chart.
First-run smoke checks
After docker compose up -d, wait ~15 seconds for healthchecks, then:
# Everything should be Up (healthy)
docker compose ps
# Postgres
docker exec translately-postgres psql -U translately -d translately -c 'SELECT version();'
# Redis
docker exec translately-redis redis-cli ping # -> PONG
# MinIO (from the host)
curl -fsS http://localhost:9000/minio/health/live && echo OK
# Buckets were created by minio-init
docker exec translately-minio mc ls local/ # (or open http://localhost:9001)
# Mailpit
curl -fsS http://localhost:8025/livez && echo OK
# Keycloak (only if --profile keycloak)
curl -fsS http://localhost:8180/realms/master/.well-known/openid-configuration | head -1
If the backend is also running, confirm it connects:
./gradlew :backend:app:quarkusDev # in another terminal
curl -fsS http://localhost:8080/q/health/ready # -> {"status":"UP", ...}
docker compose logs -f postgres redis minio mailpit is your friend when a healthcheck never flips to healthy.
compose-prod.yml diff for production
infra/compose-prod.yml is the single-host production variant. Same service set plus backend and webapp, but meaningfully tighter. Key differences:
| Aspect | Dev (docker-compose.yml) |
Prod (infra/compose-prod.yml) |
|---|---|---|
| Backend + webapp | Run on the host (./gradlew quarkusDev, pnpm dev) |
Containerised from ghcr.io/pratiyush/translately-{backend,webapp}:${TRANSLATELY_VERSION} |
| Postgres creds | Hardcoded translately / translately |
${POSTGRES_PASSWORD:?...} — fails fast if the env-file is missing |
| MinIO creds | Hardcoded translately / translately-dev |
${MINIO_ROOT_USER:?...} / ${MINIO_ROOT_PASSWORD:?...} |
| Mailpit | Present | Absent — point TRANSLATELY_MAIL_* at real SMTP |
| Keycloak | Behind --profile keycloak |
Absent — deploy separately |
| MinIO init sidecar | Creates buckets automatically | Absent — create buckets once on your real object store |
| Redis max memory | 256mb |
512mb |
| Backend port binding | n/a (runs on host) | 127.0.0.1:8080:8080 — bind-localhost, reverse-proxy in front |
| Webapp port binding | n/a (Vite on :5173) |
127.0.0.1:8081:8080 — bind-localhost, reverse-proxy in front |
| Required secrets | None (dev defaults) | JWT_SECRET, CRYPTO_MASTER_KEY, POSTGRES_PASSWORD, MINIO_ROOT_PASSWORD, SMTP_* — all :?required |
| Healthchecks | Services only | Backend adds /q/health/ready probe |
| Volumes | Named dev volumes | Same layout; you own the backup policy |
Bring-up:
cp infra/.env.prod.example .env.prod
# edit secrets with `openssl rand -base64 32`
docker compose -f infra/compose-prod.yml --env-file .env.prod up -d
Read the hardening checklist before you expose the stack to the internet, and follow the runtime profiles page for the %prod env vars the backend itself needs.
For multi-node / HA / Kubernetes deployments, wait for the Phase 7 Helm chart or roll your own orchestration.
Teardown and data wipe
# Stop services; keep data (DB, object store, AOF, Mailpit buffer)
docker compose down
# Stop AND wipe named volumes — guaranteed fresh start next `up`
docker compose down -v
# Remove everything the stack owns (images too)
docker compose down -v --rmi local
# Just reset Postgres without touching MinIO / Redis
docker compose rm -sfv postgres
docker volume rm translately_postgres-data
docker compose up -d postgres
docker compose down -v is the hammer for “my local DB is in a weird state” — it wipes postgres-data, redis-data, minio-data, and mailpit-data. The MinIO init sidecar reruns on the next up and recreates the buckets.