[compose.*]
[compose.<alias>] blocks point RunWisp at an existing
docker-compose.yml. Every service in the compose file becomes an
observable RunWisp service:
per-container logs are streamed and indexed,
restart policies are enforced, failures route through your
notification rules, and you can trigger or
stop containers from the Web UI and
REST API — without rewriting anything.
The containers themselves keep running under Docker exactly as before; RunWisp is the supervisor and observability layer on top. For the worked walkthrough, see Migrating from docker-compose — this page is the key-by-key reference.
Minimum example
Section titled “Minimum example”Drop one line next to your docker-compose.yml:
[compose.myapp]That’s it. RunWisp searches the directory of runwisp.toml for
compose.yaml, compose.yml, docker-compose.yaml,
docker-compose.yml in that order — the same fallback docker compose
itself uses — and imports every service in the first file it finds.
Full surface
Section titled “Full surface”[compose.myapp]file = "./docker-compose.yml" # default: auto-discovered next to runwisp.tomlinclude = ["api", "worker"] # subset; omit to import allexclude = ["db"] # mutually exclusive with includemode = "services" # "services" (default) | "stack"group = "myapp" # UI group; defaults to the aliasproject_name = "myapp" # docker compose -p; defaults to the aliasprofiles = ["production"] # docker compose --profileenv_file = ["./.env.prod"] # docker compose --env-fileworking_dir = "/opt/myapp" # cwd for compose invocation; defaults to dir of `file`with_deps = false # default: --no-deps; true starts compose deps toopull = "missing" # "missing" (default) | "always" | "never"name_format = "{alias}.{service}" # RunWisp task naming; default shownBlock defaults
Section titled “Block defaults”Got a stack of near-identical workers that all want the same policy? Instead of
repeating a sub-table per service, set them once in [compose.<alias>.defaults].
Every imported service inherits those keys, and any per-service sub-table still
wins:
[compose.stack]include = ["worker-a", "worker-b", "worker-c"]
[compose.stack.defaults]restart = "always" # applies to all three workersstart_retries = 5
[compose.stack.worker-a]restart = "on_failure" # worker-a opts out; the other two stay "always"This is worth knowing about because the compose-import default is
restart = "on_failure" — a worker that exits 0 (batch-then-quit,
memory-guard restart, idle timeout) counts as “done” and won’t be relaunched.
If you’re migrating workers that carried restart: unless-stopped under plain
Compose, a one-line [compose.<alias>.defaults] restart = "always" restores that
behavior for the whole block.
defaults accepts the same keys as a per-service override (below). Its
notify_on_failure / notify_on_success lists add routes to every service, on
top of any a service declares for itself. Because defaults is a reserved
sub-table name, a compose service literally named defaults is rejected with a
rename hint. Block defaults aren’t available in mode = "stack" (there’s only one
task there — set its knobs on the block).
Per-service overrides
Section titled “Per-service overrides”Per-service sub-tables apply RunWisp knobs to a single compose service.
The sub-table key has to match a compose service name — and one that
survived your include / exclude filters:
[compose.myapp]file = "./docker-compose.yml"
[compose.myapp.api]restart = "always"notify_on_failure = ["slack-prod"]graceful_stop = "30s"keep_runs = 100env = { LOG_LEVEL = "info" } # layered on top of compose env at runtime
[compose.myapp.worker]restart = "on_failure"instances = 3 # three identical worker instancesOverride sub-tables accept any subset of the
[services.*] keys:
restart, env, env_file, secrets, secrets_file,
notify_on_failure, notify_on_success, graceful_stop, stop_signal,
exit_codes, keep_runs, keep_for, instances, timeout,
description, log_max_size, log_on_full, api_trigger, on_overlap,
restart_delay, restart_backoff, healthy_after, start_retries,
priority, autostart.
The host-process knobs — shell, umask, env_base, and user — are
deliberately not accepted here: they configure a host shell run, while a
compose service runs inside a container the runtime owns. (env_base in
particular has nothing to choose between — a container never inherits the
daemon’s environment either way.) (working_dir comes from
the [compose.<alias>] block, not the per-service sub-table.)
One sharp edge: a compose service that happens to share a name with one
of the scalar keys above (file, include, exclude, mode, group,
project_name, profiles, env_file, working_dir, with_deps,
pull, name_format) can’t get a sub-table — the loader rejects the
config with an error asking you to rename the compose service.
Profiles
Section titled “Profiles”If your docker-compose.yml puts services behind
Compose profiles, Compose
hides them by default — a service tagged profiles: ["workers"] simply
doesn’t exist as far as docker compose is concerned until that profile
is turned on. RunWisp is no different: it only sees the services Compose
would, so a profile-gated service is invisible until you activate its
profile.
You activate profiles with the profiles key, and that has to happen
before include/exclude make sense. Think of it as two steps:
profiles = [...]decides which services exist at all.include/excludethen pick from that activated set.
So this imports the worker service that lives behind the workers
profile, and nothing else:
[compose.myapp]file = "./docker-compose.yml"profiles = ["workers"] # turn the profile on first…include = ["worker"] # …then select from what it exposesTwo failure modes worth naming, because both look like “RunWisp ignored my service”:
include-ing a profiled service without activating its profile. The service isn’t in the activated set, so theincludereference doesn’t resolve —runwisp validateflags it as an unknown service. Add the profile toprofiles.- Activating a profile but forgetting it widens the default set.
With
profilesset and noinclude, RunWisp imports every now-visible service, profiled ones included. Useinclude(orexclude) to narrow it back down if you only wanted a subset.
A quick way to see what a given profiles value exposes before wiring
it into RunWisp is to ask Compose directly:
docker compose --profile workers config --servicesThat prints exactly the service set RunWisp will choose from.
mode = "services" (default)
Section titled “mode = "services" (default)”Each compose service becomes its own RunWisp service, named per
name_format (default <alias>.<svc>). Here’s the exact command each
instance runs under the hood:
docker compose -f <file> -p <project> [--profile ...] [--env-file ...] run --rm --no-deps --service-ports --use-aliases --name <project>_<svc>_<instance_index> [--pull always|missing|never] -e RUNWISP_INSTANCE_INDEX=<i> -e KEY=VAL ... # your env + secrets overrides <svc>Why run --rm and not up: it keeps RunWisp’s “one process per
instance slot” supervisor invariant uniform with every other backend.
--service-ports and --use-aliases make the container behave exactly
like its compose-declared self; --rm cleans the container on exit so
every restart starts fresh.
mode = "stack"
Section titled “mode = "stack"”One RunWisp service per [compose.<alias>], named <alias>:
docker compose -f <file> -p <project> up --abort-on-container-exit --no-log-prefixThe whole stack runs together; logs from all containers stream into a
single RunWisp run; the run exits when any container exits. Per-service
sub-tables, include, exclude, and instances are all rejected in
stack mode — use per-service mode if you want per-service knobs.
Identity
Section titled “Identity”Imported tasks get smarter defaults than the generic [services.*]
baseline because compose users carry strong existing expectations:
| Knob | Compose-import default | Reason |
|---|---|---|
kind |
service |
Compose services are long-running. |
restart |
on_failure |
Matches the unspoken expectation set by docker compose up. |
group |
the compose alias | Grouped together in the Web UI sidebar by default. |
pull |
missing |
Behaviour of docker compose up. |
graceful_stop |
compose stop_grace_period when set, else daemon default |
Honours the compose-declared grace window. |
| Container name | {project}_{service}_{index} |
Matches docker compose ps output; docker logs keeps working. |
| Task name | {alias}.{service} |
Override with name_format; must contain {service} (else collide). |
User overrides win over every default via [compose.<alias>.<svc>].
Instances (identical only)
Section titled “Instances (identical only)”instances = N produces N parallel docker compose run invocations.
Each instance receives a stable RUNWISP_INSTANCE_INDEX=<i> env var
(0..N-1) and a container name suffix matching the index so
docker compose ps shows each container separately. Use the env var to
self-shard workloads.
Parameterised instances (different env per slot) are deliberately out of scope — for that, declare one compose service per slot.
One-off / cron uses
Section titled “One-off / cron uses”[services.X] or [tasks.X] can also point at a compose service
directly, without bringing in the bulk [compose.<alias>] block:
# Long-running single service backed by compose:[services.api]compose_file = "./docker-compose.yml"compose_service = "api" # defaults to the table key
# Cron task running a compose-defined image once:[tasks.nightly-backup]cron = "0 3 * * *"compose_file = "./docker-compose.yml"compose_service = "backup"Running a command in a service: compose_mode
Section titled “Running a command in a service: compose_mode”Set run alongside compose_service and RunWisp runs your command
inside the service’s already-running container — docker compose exec,
not a new container:
[tasks.laravel-schedule]cron = "* * * * *"compose_file = "/srv/myapp/compose.yaml"compose_service = "app"run = "php artisan schedule:run"That’s what most people want from a cron task attached to a running app: it executes in the live container, with that container’s env, code version, and credentials, and nothing new gets started.
compose_mode names the choice explicitly when you need the other one:
| Value | What happens |
|---|---|
exec |
docker compose exec into the service’s running container. Requires run. |
run |
docker compose run --rm — a fresh container. With run set, it runs your command; without, the service’s own. |
The default is whichever one your config can actually mean: exec when
run is set, run when it isn’t. So the snippet above needs no
compose_mode, and neither does a plain compose_service task — while
compose_mode = "run" gets you a fresh container running your command
instead of the live one.
Two things to know before you lean on exec:
- The container has to be up. If it isn’t, the run fails with
container … is not running— visibly, as a failed run with that in the log, not a silent skip. timeoutcan’t reach inside. Docker offers no way to cancel an exec, so when a run times out RunWisp kills the local client and the process inside the container carries on. Bound long work on the inside too —run = "timeout 3600 php artisan long-job". RunWisp warns at startup if you put a long-running service inexecmode, because there the leftover process compounds on every restart.
Your command runs under the container’s /bin/sh with fail-fast armed,
the same as run on the host — so a
multi-line script stops at the first failing line. The shell key isn’t
accepted here: it configures a host process, and the host’s shell path
says nothing about what exists in someone else’s image.
Supervision boundary
Section titled “Supervision boundary”RunWisp owns supervision: when a container exits, the RunWisp service
supervisor decides whether to restart based on the task’s restart
policy and backoff curve. The compose-file restart: directive is
not consulted — RunWisp is your supervisor now.
When the docker CLI is missing or the Docker daemon is unreachable, imported tasks remain visible in the UI but their runs fail with a clear system-log message pointing at install docs.