Skip to content

[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.

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.

[compose.myapp]
file = "./docker-compose.yml" # default: auto-discovered next to runwisp.toml
include = ["api", "worker"] # subset; omit to import all
exclude = ["db"] # mutually exclusive with include
mode = "services" # "services" (default) | "stack"
group = "myapp" # UI group; defaults to the alias
project_name = "myapp" # docker compose -p; defaults to the alias
profiles = ["production"] # docker compose --profile
env_file = ["./.env.prod"] # docker compose --env-file
working_dir = "/opt/myapp" # cwd for compose invocation; defaults to dir of `file`
with_deps = false # default: --no-deps; true starts compose deps too
pull = "missing" # "missing" (default) | "always" | "never"
name_format = "{alias}.{service}" # RunWisp task naming; default shown

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 = 100
env = { LOG_LEVEL = "info" } # layered on top of compose env at runtime
[compose.myapp.worker]
restart = "on_failure"
instances = 3 # three identical worker instances

Override 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, and user — are deliberately not accepted here: they configure a host shell run, while a compose service runs inside a container the runtime owns. (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.

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:

  1. profiles = [...] decides which services exist at all.
  2. include / exclude then 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 exposes

Two 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 the include reference doesn’t resolve — runwisp validate flags it as an unknown service. Add the profile to profiles.
  • Activating a profile but forgetting it widens the default set. With profiles set and no include, RunWisp imports every now-visible service, profiled ones included. Use include (or exclude) 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:

Terminal window
docker compose --profile workers config --services

That prints exactly the service set RunWisp will choose from.

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.

One RunWisp service per [compose.<alias>], named <alias>:

docker compose -f <file> -p <project> up --abort-on-container-exit --no-log-prefix

The 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.

Imported tasks get smarter defaults than the generic [services.*] baseline because compose users carry strong existing expectations:

KnobCompose-import defaultReason
kindserviceCompose services are long-running.
restarton_failureMatches the unspoken expectation set by docker compose up.
groupthe compose aliasGrouped together in the Web UI sidebar by default.
pullmissingBehaviour of docker compose up.
graceful_stopcompose stop_grace_period when set, else daemon defaultHonours 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 = 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.

[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"

run and compose_file are mutually exclusive on the same table — RunWisp rejects the config at load if both are set.

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.