Local Development with Compose
pilum compose writes a docker-compose.yml for the services Pilum discovers. It can also add infrastructure such as Postgres or Redis and wire the connection strings into the services that need them. The services you deploy are the services you run locally, with no second hand-maintained compose file.
Quick start
# 1. Build the images locally (host build + docker build, no push)pilum build
# 2. Generate docker-compose.yml for every servicepilum compose --all
# 3. Run the stackdocker compose up -dpilum compose only writes the file. It doesn’t build or start anything, and the generated services reference images by name; there are no build: sections. Build the images first with pilum build (or pull them from your registry). The command’s summary suggests docker compose up --build, but --build has nothing to build.
A multi-region service appears once in the file, since local runs have no regions.
Choosing services
You must pass --all or name services. Without either, the command fails with specify --all to include all services or pass service names as arguments.
pilum compose --all # every servicepilum compose api worker # these twopilum compose 'api-*' # globpilum compose --all --provider gcp # only GCP servicespilum compose --all --env staging # apply environments.staging firstpilum compose --all --no-deps # skip infrastructure dependenciesServices are discovered from the project root, even if you run the command from a subdirectory. The output file is written relative to where you run it.
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--all | false | Include every discovered service | |
--registry | -R | Image prefix. Images become <registry>/<name>:<tag> | |
--tag | -t | latest | Image tag |
--output | -o | docker-compose.yml | Output path |
--env | -e | Apply an environments override before generating | |
--provider | Only services of this provider | ||
--no-deps | false | Leave out compose.dependencies | |
--env-file | Also write a .env template with service URLs (e.g. apps/web/.env.example) |
compose is also available as docker-compose and dc.
What gets generated
For each service:
| Compose key | Value |
|---|---|
image | --registry given: <registry>/<name>:<tag>. GCP service with project and region: <region>-docker.pkg.dev/<project>/<registry_name or project>/<name>:<tag>, the same image pilum build produces. Otherwise <registry_name>/<name>:<tag>, or <name>:<tag> |
container_name | The service name |
restart | unless-stopped (no for a migration service, see below) |
ports | <host port>:8080. Host ports are assigned from 8080 upward in alphabetical order of service name, unless the service sets a top-level port: |
environment | Every env_vars entry as written. Every secrets key as NAME=${NAME}, so Docker Compose fills it from your shell or a .env file. Secrets that a dependency provides get its connection string instead |
networks | app-network (a bridge network shared by all services) |
depends_on | Dependencies whose secrets the service uses, with condition: service_healthy |
The container port is always 8080. Your service has to listen on 8080 inside the container, which is also the Cloud Run default.
The image name uses the service name, not image_name. If a service sets image_name, pass --registry or edit the generated file.
A few more keys in pilum.yaml are read only by compose:
| Key | Effect |
|---|---|
port (top level) | Host port for the service instead of the alphabetical assignment |
health_check.path, health_check.initial_delay | Adds a healthcheck curl -f http://localhost:8080<path> (path default /health, start period default 5s) |
runtime.memory, runtime.cpu | Become mem_limit (512Mi is written as 512m) and cpus |
Two secret names get fixed development values instead of ${NAME}: JWT_SECRET becomes dev-jwt-secret-do-not-use-in-production and ADMIN_KEY becomes dev-admin-key.
Migration services
If a service’s name contains migration, it is treated as a one-shot job: its restart is no, and every other service gets depends_on: <migration>: condition: service_completed_successfully. Migrations finish before the services start. If several names contain migration, only the first alphabetically is used for ordering.
Infrastructure dependencies
Databases and caches are declared once for the project in .pilum.yml, under compose.dependencies. Pilum reads .pilum.yml from the directory you run it in.
compose: dependencies: - type: postgres database: app secrets: [DATABASE_URL] # services with this secret get the connection string
- type: redis secrets: [REDIS_URL]
- type: custom name: mailpit image: axllent/mailpit:latest port: 8025 healthcheck: wget -q --spider http://localhost:8025| Field | Required | Description |
|---|---|---|
type | Yes | postgres, mysql, mariadb, redis, mongo or custom |
name | No | Compose service name and hostname. Defaults to type |
image | For custom | Overrides the preset image |
port | No | Published as <port>:<port>. Defaults to the preset’s port |
user, password, database | No | Credentials. Presets default to appuser / apppassword / appdb |
healthcheck | No | Shell command for the healthcheck. Presets have one built in |
environment | No | Extra environment variables (map) |
secrets | No | Service secret names this dependency satisfies |
Presets
| Type | Image | Port | Connection string injected |
|---|---|---|---|
postgres | postgres:15-alpine | 5432 | postgresql://<user>:<password>@<name>:<port>/<database>?sslmode=disable |
mysql | mysql:8.0 | 3306 | mysql://<user>:<password>@<name>:<port>/<database> |
mariadb | mariadb:11 | 3306 | mysql://<user>:<password>@<name>:<port>/<database> |
redis | redis:7-alpine | 6379 | redis://<name>:<port> |
mongo | mongo:7 | 27017 | mongodb://<user>:<password>@<name>:<port>/<database> |
custom | your image | your port | none |
Each preset also gets its environment variables (POSTGRES_USER, MYSQL_DATABASE and so on), a healthcheck (pg_isready, mysqladmin ping, redis-cli ping, …) checked every 10s with 5 retries, and a named volume <name>_data for its data. Postgres also mounts ./init-scripts into /docker-entrypoint-initdb.d, so SQL files there run on first start.
How secrets are wired
A service whose pilum.yaml lists a secret named in a dependency’s secrets gets two things:
- The secret’s environment variable is set to the dependency’s connection string instead of
${NAME}. - A
depends_onentry on the dependency withcondition: service_healthy, so the service starts only after the database passes its healthcheck.
name: apitype: gcp-cloud-runproject: my-projectregion: us-central1registry_name: servicestemplate: gosecrets: DATABASE_URL: prod-db-url:latest # Secret Manager in production REDIS_URL: prod-redis-url:latestThe generated compose entry for api, with the .pilum.yml above, contains:
api: image: us-central1-docker.pkg.dev/my-project/services/api:latest container_name: api environment: - DATABASE_URL=postgresql://appuser:apppassword@postgres:5432/app?sslmode=disable - REDIS_URL=redis://redis:6379 ports: - 8080:8080 depends_on: postgres: condition: service_healthy redis: condition: service_healthyFrontend env template
--env-file writes a .env template with one URL per service, for a frontend that calls your APIs:
pilum compose --all --env-file apps/web/.env.exampleVITE_API_API_URL=http://localhost:8080/api/v1VITE_AUTHENTICATION_API_URL=http://localhost:8081/api/v1The variable name is the service name in capitals, with - replaced by _ and a trailing _SERVICE removed (authentication-service becomes VITE_AUTHENTICATION_API_URL). Ports follow the same alphabetical assignment, so regenerate the file when services are added or removed.
Workflow
# After changing code: rebuild only what changed, then restart itpilum build apidocker compose up -d api
# After adding or removing a service, or changing secrets/env_varspilum compose --alldocker compose up -d
# Tear down (add -v to delete database volumes)docker compose downTips:
- Regenerate rather than hand-editing. The file is derived from
pilum.yamland.pilum.yml, and the nextpilum composeoverwrites it. Commit it if your team runs it without Pilum, or add it to.gitignoreif not. - Put values for
${NAME}secrets in a.envfile next todocker-compose.yml. Docker Compose reads it automatically. pilum buildtags images with your--tagand also with:latestlocally, so the default--tag latestincomposepicks up the latest local build.- With
--json,composeprints{file, services, ports}, which scripts can use to find each service’s port.
Next Steps
- Project Configuration —
.pilum.ymlreference, includingcompose.dependencies - Service Configuration —
secrets,env_varsandenvironments - Build Configuration — what
pilum buildruns - Environments & CI — environment overrides