Dockerfile services
A service whose repo ships a Dockerfile (or its own docker-compose.yml)
can run in a container with nothing more than cloneFrom:
services:
api:
cloneFrom: git@example.com:org/api.git # repo has a Dockerfile
port: 3084
corgi run clones the repo, sees there are no start: scripts, finds the
Dockerfile, builds the image and runs the container — and says so:
✨ api: no start scripts — running from Dockerfile
The complexity ladder
Pick the rung that fits each service; they mix freely in one file.
cloneFromonly, repo hasdocker-compose.yml— corgi drives the repo's own compose file (docker-compose.yml/.yaml,compose.yml/.yaml), passing corgi's generated env via--env-fileso${VAR}references resolve. Zero-config only: any declaredrunner:build field (or plainrunner: docker) pins the Dockerfile instead — a repo's compose file never silently overrides your config.cloneFromonly, repo hasDockerfile— corgi generates a compose wrapper (ports, env, restart policy) and runs it.runner:fields tune the build — custom dockerfile path, target, build args, volumes, container port, command.beforeStart/startscripts — native mode, exactly as before.- Scripts and a Dockerfile — scripts run by default;
corgi run --dockerflips every docker-capable service to containers.
When does a service run in docker?
In priority order:
runner: dockerdeclared → always.corgi run --docker→ every service with a Dockerfile or compose file.- No
start:commands and the repo has a Dockerfile or compose file → automatically. - Otherwise → native scripts.
beforeStart (host-side: certs, env generation, migrations) still runs in
docker mode; the container replaces only start:. afterStart runs on stop
in both modes.
Check what a run would do without side effects:
corgi run --dry-run # mode=native | docker (Dockerfile) | docker (repo compose)
corgi run --dry-run --docker
Runner options
services:
web:
cloneFrom: git@example.com:org/web.git
port: 3100
runner:
name: docker
dockerfile: Dockerfile.dev # default: Dockerfile
context: . # build context, default: service dir
target: dev # multi-stage target
args:
NODE_VERSION: "22"
volumes:
- ./src:/app/src # host paths relative to the service dir
containerPort: 3000 # default: first EXPOSE, else port
command: npm run dev # override CMD
Scalar shorthand: runner: docker.
Two more runner tricks:
services:
pdf:
port: 3005
runner:
image: gotenberg/gotenberg:8 # registry image, no repo/build at all
containerPort: 3000
api:
cloneFrom: git@example.com:org/api.git
port: 3084
runner:
name: docker
watch: true # rebuild + restart the container on file changes
image needs no name: docker (implied) and no repo — perfect for a backing
service your team never edits. watch uses docker compose up --watch;
foreground runs only (detached runs skip it and say so).
Pre-building images
corgi build builds every docker-capable service's image in parallel without
starting anything — warm the cache before a demo, or in CI before
corgi run --wait. Respects --services; exit 1 if any build fails.
To use a specific compose file the repo ships instead of generating one:
runner:
name: docker
composeFile: ./docker-compose.dev.yml
composeFile and the build fields are mutually exclusive.
Ports
port: is the host port. Inside the container corgi maps it to
containerPort, which defaults to the Dockerfile's first EXPOSE, then to
port. With EXPOSE present you can omit port: entirely — corgi reads it
from the Dockerfile.
For repo-compose services the repo's own ports: mapping applies; set
port: in corgi-compose so readiness probes and corgi ps know where to
look.
Corgi's generated env (DB credentials, cross-service URLs — with
localhost rewritten to host.docker.internal) reaches repo-compose
containers two ways: ${VAR} interpolation inside the compose file, and an
auto-generated override (corgi.env.override.yml) that adds corgi's env
file to every service in it. Values the repo's compose sets explicitly win
over the injected ones.
Env, readiness, logs, lifecycle
- The service's corgi-generated
.env(dependencies, db credentials, cross-service URLs) feeds the container, withlocalhostrewritten tohost.docker.internalso containers reach host-side services and databases. Linux getshost.docker.internalviahost-gateway. - Readiness is unchanged: port probe or
healthCheckpath, pluswarmup. corgi logs <service>works — container logs stream into the same log files (detached mode).corgi psverifies the actual container state,corgi stopbrings containers down (volumes survive;corgi cleanremoves them).- Builds run concurrently — one goroutine per service, same dependency/database gating as native services.
Migrating from the old runner: docker
Earlier corgi versions generated a fixed wrapper: build context three levels
up (the compose root), Dockerfile at the service root, and an automatic
whole-workspace /app volume mount. Now the context defaults to the service
dir, no volumes are mounted unless you declare runner.volumes, and up
rebuilds on context changes (--build). If your Dockerfile relied on the old
root context, set runner.context explicitly; if you relied on the implicit
mounts, declare them under runner.volumes.
Container names are the docker-safe service name — two workspaces using the same service name share one docker namespace. Opt out of collisions with:
name: my-stack
scopeContainers: true # containers become my-stack-api, postgres-my-stack-db, …
Applies to services and databases alike. Off by default — existing names stay exactly as they are. Turning it on with old containers still running gets a warning at boot listing what to remove.
Troubleshooting
- Base image needs auth (private registry): run
docker loginfor that registry first; corgi surfaces docker's own error. - No EXPOSE and no port — corgi skips the service and says why; add
port:or anEXPOSEline. - Docker daemon down — corgi starts it when
useDocker: true(Docker Desktop, OrbStack, Colima supported).