Skip to content

[compose.*]

A [compose.<alias>] block imports the services of an existing docker-compose.yml. Each compose service becomes a RunWisp service with captured logs, restarts, notifications, and start/stop from the Web UI and API. The containers still run under Docker. For a walkthrough, see Migrating from docker-compose.

The smallest block imports every service from the compose file next to runwisp.toml:

runwisp.toml
[compose.myapp]

All block keys:

runwisp.toml
[compose.myapp]
file = "./docker-compose.yml" # default: auto-discovered next to runwisp.toml
services = ["api", "worker"] # keep only these; "-db" drops one instead; omit to import all
import = "services" # "services" (default) | "stack"
group = "myapp" # UI group; defaults to the alias
project_name = "myapp" # docker compose -p; defaults to the file's own project
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

type: string default: auto-discovered next to runwisp.toml

Path to the compose file. When unset, RunWisp looks in the runwisp.toml directory for compose.yaml, compose.yml, docker-compose.yaml, then docker-compose.yml (the same order docker compose uses).

runwisp.toml
[compose.myapp]
file = "./docker-compose.yml"

type: string[] default: all services

Which compose services to import.

  • Bare names (or "+name") keep only the listed services.
  • "-name" drops the listed services and keeps the rest.
  • You can’t mix the two styles in one list.
  • A name that doesn’t exist (for example, one hidden behind an inactive profile) is an error.
runwisp.toml
[compose.myapp]
services = ["api", "worker"]

type: enum default: "services"

How the compose file maps to RunWisp. See Import strategies.

runwisp.toml
[compose.myapp]
import = "stack"

Possible values:

  • services: one RunWisp service per compose service
  • stack: one RunWisp service that runs the whole project

type: string default: the alias

The Web UI and TUI sidebar group for every imported service.

runwisp.toml
[compose.myapp]
group = "myapp"

type: string default: the compose file’s own project

The compose project name (docker compose -p). Unset, it is the project docker compose up uses in the file’s directory: COMPOSE_PROJECT_NAME, the file’s top-level name:, or the directory name.

runwisp.toml
[compose.myapp]
project_name = "myapp"

type: string[]

Compose profiles to activate (docker compose --profile). A service with a profiles: tag in the compose file doesn’t exist for RunWisp until you activate its profile here.

The profiles are applied first, then the services filter picks from the result:

runwisp.toml
[compose.myapp]
profiles = ["workers"] # makes the profiled services visible
services = ["worker"] # then keep only this one
  • Keeping a profiled service without activating its profile is an error (runwisp validate reports an unknown service).
  • Activating a profile with no services filter imports every visible service, profiled ones included.

To see which services a profile exposes, ask Compose:

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

type: string[]

Env files for the compose command itself (docker compose --env-file).

runwisp.toml
[compose.myapp]
env_file = ["./.env.prod"]

type: string default: the directory of file

The directory the docker compose command runs in. It doesn’t change the container’s working directory.

runwisp.toml
[compose.myapp]
working_dir = "/opt/myapp"

type: boolean default: false

Whether starting a service also starts its compose depends_on services. With false, RunWisp passes --no-deps.

runwisp.toml
[compose.myapp]
with_deps = true

type: enum default: "missing"

When to pull the image before a start.

runwisp.toml
[compose.myapp]
pull = "always"

Possible values: missing, always, never.

type: string default: "{alias}.{service}"

The RunWisp name of each imported service. With import = "services" it must contain {service}, otherwise all names would be the same.

runwisp.toml
[compose.myapp]
name_format = "{alias}-{service}"

[compose.<alias>.defaults] sets override keys once for every imported service. A per-service sub-table still wins.

runwisp.toml
[compose.stack]
services = ["worker-a", "worker-b", "worker-c"]
[compose.stack.defaults]
restart = "always"
restart_attempts = 5
[compose.stack.worker-a]
restart = "on_failure" # only worker-a changes
  • Accepts the same keys as a per-service override.
  • Order: imported default, then defaults, then the per-service sub-table.
  • A notify list here adds routes to every service, on top of each service’s own.
  • A compose service named defaults is rejected. Rename it.
  • Not allowed with import = "stack". Set keys on the block instead.

The imported restart default is on_failure, so a worker that exits with code 0 is not started again. For workers that used restart: unless-stopped in Compose, set restart = "always" here.

Possible restart values:

  • never: don’t restart; one exit is final
  • on_failure: restart only after an exit that failures counts as a failure
  • always: restart after any exit, like docker compose up

A [compose.<alias>.<service>] sub-table sets RunWisp keys for one compose service. The name must be a compose service that passed the services filter.

runwisp.toml
[compose.myapp]
file = "./docker-compose.yml"
[compose.myapp.api]
restart = "always"
notify = ["slack-prod"]
graceful_stop = "30s"
keep_runs = 100
env = { LOG_LEVEL = "info" } # added on top of the compose env
[compose.myapp.worker]
restart = "on_failure"
instances = 3

Accepted keys:

Rejected:

  • run, compose_file, compose_service: the block decides what runs.
  • on_overlap: a task-only key.
  • shell, umask, env_base, user: these configure a host process, and a compose service runs in a container.
  • working_dir: set it on the block.

A compose service with the same name as a block key (file, services, import, group, project_name, profiles, env_file, working_dir, with_deps, pull, name_format) can’t have a sub-table. The config is rejected; rename the compose service.

Each compose service becomes one RunWisp service, named by name_format. Each instance runs:

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
-e KEY ... # your env + secrets, values read from RunWisp's own environment
<svc>

--service-ports and --use-aliases give the container the same ports and network aliases as in the compose file. --rm removes the container on exit, so every restart starts from a fresh container.

Since 1.8, an instance whose container is already running from docker compose up is not started twice. RunWisp attaches to that container instead: the run shows its output from then on (through docker logs -f) and ends with its exit code, and the run log says it attached. A stop or restart, including stopping the daemon, stops the up container, so the next start runs RunWisp’s own. The Nth instance attaches to the Nth up container by name. Tasks always start their own container.

One RunWisp service named <alias> runs the whole project:

docker compose -f <file> -p <project> up --abort-on-container-exit --no-log-prefix
  • Logs from all containers go into one run.
  • The run ends when any container exits.
  • Per-service sub-tables, defaults, the services filter, and instances are rejected.

Imported services use these defaults instead of the [services.*] ones. A per-service sub-table overrides any of them.

Key Default for imported services
kind service
restart on_failure
group the alias
pull missing
graceful_stop the compose stop_grace_period if set, else the daemon default
Container name {project}_{service}_{index} (matches docker compose ps; docker logs works). An overlapping run adds _{run id}.
RunWisp name {alias}.{service} (see name_format)

instances = N in a per-service sub-table starts N parallel docker compose run containers.

  • Each gets RUNWISP_INSTANCE_INDEX (0 to N-1); use it to split the work.
  • The container name ends with the index, so docker compose ps lists each one.
  • All instances get the same env. For different env per instance, define one compose service per instance.

A [services.*] or [tasks.*] table can run one compose service without a [compose.*] block:

runwisp.toml
# Long-running service from a compose file:
[services.api]
compose_file = "./docker-compose.yml"
compose_service = "api" # defaults to the table key
# Cron task that runs a compose service once:
[tasks.nightly-backup]
cron = "0 3 * * *"
compose_file = "./docker-compose.yml"
compose_service = "backup"

Both run in the compose file’s own project, which is also the default for a [compose.*] block’s project_name; see compose_file.

Running a command in a service: compose_mode

Section titled “Running a command in a service: compose_mode”

With run set next to compose_service, RunWisp runs your command inside the service’s running container (docker compose exec). No new container is started, and the command gets that container’s env, code, and credentials.

runwisp.toml
[tasks.laravel-schedule]
cron = "* * * * *"
compose_file = "/srv/myapp/compose.yaml"
compose_service = "app"
run = "php artisan schedule:run"

compose_mode picks the mode:

Value What happens
exec docker compose exec in the running container. Requires run. Default when run is set.
run docker compose run --rm in a new container, without the service’s ports or network aliases. Runs your run command if set, else the service’s own command. Default when run is unset.

since: 1.7

When RunWisp runs as a service of the same compose project, an exec task can leave out compose_file. RunWisp reads the project from its own container’s labels, so it needs only the Docker socket, not the project directory.

runwisp.toml
[tasks.laravel-schedule]
cron = "* * * * *"
compose_service = "app"
run = "php artisan schedule:run"

compose_mode = "run" still needs compose_file. runwisp validate warns when RunWisp doesn’t run in a container, since every run would fail.

Limits of exec:

  • The container must be running. If it isn’t, the run fails, and its log names the compose project RunWisp searched (see If RunWisp runs in a container).
  • timeout can’t stop the process inside the container. Docker can’t cancel an exec, so RunWisp stops only the local client. Add a limit inside the command too: run = "timeout 3600 php artisan long-job". RunWisp warns at startup about a long-running service in exec mode, because each restart leaves another process behind.
  • The command runs under the container’s /bin/sh with fail-fast, like run on the host. shell is not accepted.
  • RunWisp decides restarts, using restart and the backoff keys. The compose file’s restart: setting is ignored.
  • If the docker CLI is missing or the Docker daemon is unreachable, the services stay visible, and their runs fail with a system log message.