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.

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.