Skip to content

Local Docker Compose Stack

Local development uses Docker only for infrastructure dependencies. The backend (Go) and frontend (Expo) run on the host so that hot reload and IDE tooling work without container overhead.

The stack is defined in docker-compose.dev.yml at the repository root.

Services

Service Image Port(s) Purpose Profile
postgres tomoda-postgres:dev (built from docker/postgres) 5432 Primary database (Postgres 17 + PostGIS 3.5 + PGroonga + pgvector) default
redis redis:7-alpine 6379 Cache, queues, presence, pub/sub default
minio minio/minio:latest 9000, 9001 S3-compatible object storage (9001 is the web console) default
photon rtuszik/photon-docker:2.2.0 2322 Self-hosted geocoder default
pgadmin dpage/pgadmin4:latest 5050 Postgres web UI — login admin@local.dev / admin tools
redis-commander rediscommander/redis-commander:latest 8081 Redis web UI tools
semantic ollama/ollama:0.5.7 11434 Self-hosted small-LLM server for the semantic resolver semantic
embeddings text-embeddings-inference:cpu-1.6 8085 TEI server hosting bge-m3 for semantic location search (task embeddings:up; first run downloads the model) semantic

Default credentials for the local stack (intentionally weak — they never leave your machine):

  • Postgres: tomoda / tomoda123, database tomoda
  • MinIO: root user tomoda, root password tomoda123

Redis Commander port clash

redis-commander exposes port 8081, which is the same port the Expo dev server uses for the web frontend. If you run both at once you'll see a bind conflict. Either skip the tools profile or remap one of the ports.

The custom Postgres image

postgres is built locally from docker/postgres/Dockerfile rather than pulled ready-made, because no published image carries all three of PostGIS, PGroonga, and pgvector. The Dockerfile extends the PostGIS 3.5 / PG17 base with:

  • PGroonga — multilingual full-text search, including CJK tokenization. The location search path uses it for lexical matching across localized names.
  • pgvector — vector similarity (halfvec + HNSW) for semantic retrieval.

Production builds the same extensions at the same pinned versions onto the CloudNativePG base (in the devops repo), so local and cluster Postgres expose an identical extension surface. docker compose build postgres rebuilds it after a Dockerfile change.

The database runs Postgres 17. Its data volume is postgres_tomoda_data_pg17; a volume from an older Postgres major is incompatible, so switching to this image needs a fresh volume (task docker:clean then re-migrate + re-seed).

Starting the stack

The repo ships two entry points:

This is what you run day to day. It:

  1. Brings up only postgres, redis, minio, and photon from the dev compose file (no tools profile, no application containers).
  2. Waits a few seconds, then runs database migrations (task db:migrate).
  3. Regenerates Swagger docs (task gen:docs).
  4. Attempts to pull secrets from GCP Secret Manager (see Secrets). If gcloud is not authenticated, falls back to hard-coded local defaults.
  5. Starts the backend with Air for hot reload (backend && air) on :8080.

You then start the frontend in a second terminal:

task dev:frontend

Just the infra services

If you want to run the backend yourself (e.g. attach a debugger) but still need Postgres/Redis/etc, you can bring up just the dependencies:

docker-compose -f docker-compose.dev.yml up -d postgres redis minio photon

This is what task dev does internally before invoking Air.

Including the GUI tools

docker-compose -f docker-compose.dev.yml --profile tools up -d

This brings up pgAdmin (http://localhost:5050) and Redis Commander (http://localhost:8081).

Volumes & persistence

Each stateful service has a named volume that survives docker-compose down:

  • postgres_tomoda_data_pg17/var/lib/postgresql/data
  • redis_tomoda_data/var/lib/redis
  • minio_tomoda_data/data
  • photon_data/photon/data (Photon index — the first download takes several minutes)
  • semantic_models/root/.ollama (pulled model weights; semantic profile only)

To wipe everything and start fresh — most commonly to reset a corrupted database or test migrations from scratch:

task docker:clean    # docker-compose down -v --remove-orphans
task docker:up       # bring everything back up
task db:migrate      # re-run migrations on the empty database
task db:test:setup   # (optional) re-seed test users / events

Or in one shot:

task db:reset

Network

All services share the tomoda-network bridge network so they can talk to each other by service name. The host reaches them through the published ports listed above (localhost:5432, localhost:6379, etc.).

Semantic model server (optional)

The semantic service runs Ollama for the semantic resolver and is opt-in via its own profile (it is not started by task dev). Bring it up and pull the default model once:

task semantic:up      # docker-compose --profile semantic up -d semantic
task semantic:pull    # pulls qwen2.5:3b-instruct into the container

Then enable the layer for the backend with SEMANTIC_ENABLED=true (base URL defaults to http://localhost:11434). With the profile down, the backend simply runs the deterministic enrichment path.

Photon first-run

The Photon container downloads its geocoding index on first start. The default REGION=canada (smallest index — overridable via the PHOTON_REGION env var) takes a few minutes. The healthcheck has a 300-second start_period to account for this. See Photon for how to switch to the multilingual GCS-hosted index for testing Japanese/Chinese queries.

Health checks

Every service defines a Compose-level healthcheck. The most useful one in practice:

docker-compose -f docker-compose.dev.yml ps

A service stuck on (health: starting) for more than a few minutes (other than photon on first launch) usually means something is wrong — check docker-compose logs <service>.