Skip to content

Container Field ​

Run workflow steps in Docker containers for consistent, isolated execution environments.

Docker-compatible runtime required

The Dagu process must reach a Docker-compatible daemon. If Dagu runs in Docker, mount the host Docker socket.

The container field supports two modes:

  • Image mode: Create a new container from an image
  • Exec mode: Execute commands in an existing running container

Entrypoint Not Used for Steps

When using DAG-level container, steps run via docker exec in a long-lived container. Your image's ENTRYPOINT/CMD is not invoked for step commands. See Execution Model for details.

Basic Usage ​

Image Mode (Create New Container) ​

yaml
container:
  image: python:3.11

steps:
  - id: install
    run: pip install pandas numpy  # Install dependencies
  - id: process
    run: python process.py          # Process data
    depends: install

All steps run in the same container instance, sharing the filesystem and installed packages.

Exec Mode (Use Existing Container) ​

Execute commands in a container that's already running (e.g., started by Docker Compose or another process):

yaml
# Simple string form - exec with container's default settings
container: my-running-container

steps:
  - id: migrate
    run: php artisan migrate
  - id: clear_cache
    run: php artisan cache:clear
    depends: migrate

Or with overrides for user, working directory, and environment:

yaml
# Object form with exec field
container:
  exec: my-running-container
  user: root
  working_dir: /app
  env:
    - DEBUG=true

steps:
  - run: chown -R app:app /data

Exec mode is ideal for:

  • Running commands in application containers started by Docker Compose
  • Interacting with long-running service containers
  • Development workflows where containers are already running

With Volume Mounts ​

yaml
container:
  image: node:24
  volumes:
    - ./src:/app
    - ./data:/data
  working_dir: /app

steps:
  - id: install
    run: npm install    # Install dependencies

  - id: build
    run: npm run build  # Build the application
    depends: install

  - id: test
    run: npm test       # Run tests
    depends: build

Volume specs use source:target[:ro|rw].

  • source may be an absolute host path, ~, ., ./..., another relative path, or a Docker named volume.
  • Dagu expands $VAR and ${VAR} only on the source side, using the Dagu process environment after DAG expression interpolation.
  • target is the container path and is not expanded from host environment variables.
  • ro and rw are the only valid modes; the mode is not expanded.
  • Missing source-side environment variables fail before Dagu calls Docker.

Path-like sources become bind mounts. Sources that are not path-like remain Docker named volumes, so ${CACHE_VOLUME}:/cache is a named volume when CACHE_VOLUME=dagu-cache, while ${HOME}/.codex:/codex-home is a bind mount.

With Environment Variables ​

yaml
container:
  image: postgres:16
  env:
    - POSTGRES_PASSWORD=secret
    - POSTGRES_DB=myapp

steps:
  - id: wait_for_postgres
    run: pg_isready -U postgres
    retry_policy:
      limit: 10
      interval_sec: 3
      
  - id: load_schema
    run: psql -U postgres myapp -f schema.sql
    depends: wait_for_postgres

Environment Files ​

env_file loads variables from one or more dotenv files into the container:

yaml
container:
  image: postgres:16
  env_file:
    - .env.postgres
    - .env.local
  env:
    - POSTGRES_DB=myapp

steps:
  - run: pg_isready -U postgres
  • env_file accepts one path or a list. Files are read in order, and a later file overrides an earlier one.
  • env entries override file variables with the same name. Step env: values sit below both.
  • Relative paths are searched in the working directory, then in the DAG file's directory. Absolute and ~ paths also work. References such as ${env.ENV_NAME} in a path resolve before the file is read.
  • Files use the same dotenv syntax as top-level dotenv, not Docker's --env-file format. Surrounding quotes are removed, and $VAR or ${VAR} in unquoted or double-quoted values expands from variables defined earlier in the same file. Single-quoted values stay literal. Dagu does not resolve ${env.*}, ${params.*}, or other Dagu references in file contents.
  • File variables are also available to Dagu references in the step, such as ${NAME} in run, as env entries are.
  • Unlike dotenv, a missing or unreadable file fails the step. For a DAG-level container, it fails the run.
  • Files are read on the host that runs the step. In distributed mode, the file must exist on the worker.

Top-level dotenv loads variables into the DAG environment. env_file loads them only into the selected container.

Private Registry Authentication ​

yaml
# For private images
registry_auths:
  ghcr.io:
    username: ${env.GITHUB_USER}
    password: ${env.GITHUB_TOKEN}

container:
  image: ghcr.io/myorg/private-app:latest

steps:
  - run: ./app

Or use DOCKER_AUTH_CONFIG environment variable (same format as ~/.docker/config.json).

Shell Wrapper ​

The shell field wraps step commands with a shell interpreter to enable shell operators like pipes, redirects, and command chaining. Configure the shell once at the container level instead of wrapping each command individually.

Why Use Shell Wrapper? ​

Without shell (repetitive wrapping):

yaml
container:
  image: alpine:latest

steps:
  - run: sh -c "cat data.csv | cut -d',' -f2 | sort | uniq > unique.txt"
  - run: sh -c "npm install && npm test"
  - run: sh -c "npm run build || exit 1"

With shell (configured once):

yaml
container:
  image: alpine:latest
  shell: ["/bin/sh", "-c"]  # Configure once

steps:
  - run: cat data.csv | cut -d',' -f2 | sort | uniq > unique.txt
  - run: npm install && npm test
  - run: npm run build || exit 1

Supported Shell Features ​

When shell is configured, your commands can use:

  • Pipes: | - Pass output from one command to another
  • Command chaining: &&, ||, ; - Execute multiple commands conditionally
  • Redirects: >, >>, < - Redirect input/output
  • Variable expansion: $VAR, ${VAR} - Use shell variables
  • Globbing: *.txt, **/*.js - File pattern matching
yaml
container:
  image: alpine:latest
  shell: ["/bin/sh", "-c"]

steps:
  # Pipes
  - run: echo "hello world" | tr a-z A-Z

  # Command chaining
  - run: mkdir -p build && cd build && cmake ..

  # Redirects
  - run: cat data.txt | grep ERROR > errors.log

  # Variable expansion
  - run: echo "User is $USER"

Format ​

Array format: [shell_path, ...flags, command_flag]

  • First element: Path to shell executable (/bin/sh, /bin/bash, etc.)
  • Middle elements (optional): Shell flags (-e, -o errexit, etc.)
  • Last element: Command flag is auto-added if not present (-c, -Command, /c, --run)

Examples:

yaml
# Simple - auto-adds -c
shell: ["/bin/sh"]

# With flags - auto-adds -c at the end
shell: ["/bin/bash", "-e", "-x"]

# Bash strict mode - -c already present
shell: ["/bin/bash", "-o", "errexit", "-o", "pipefail", "-c"]

# PowerShell - auto-adds -Command
shell: ["powershell"]

# Windows cmd - auto-adds /c
shell: ["cmd.exe"]

Cross-Platform Support ​

The shell field automatically detects and adds the correct command flag for different shells:

Shell TypeAuto-Added FlagExample
Unix shells (sh, bash, zsh)-c["/bin/bash"] → ["/bin/bash", "-c"]
PowerShell (powershell, pwsh)-Command["powershell"] → ["powershell", "-Command"]
Windows cmd/c["cmd.exe"] → ["cmd.exe", "/c"]
Nix shell--run["nix-shell"] → ["nix-shell", "--run"]

Exec Mode Example ​

Works in both image mode and exec mode:

yaml
container:
  exec: my-running-container
  shell: ["/bin/bash", "-c"]

steps:
  - run: composer install && php artisan migrate
  - run: npm run build || echo "Build failed"

Bash Strict Mode ​

Enable error handling and debugging flags for robust shell scripts:

yaml
container:
  image: ubuntu:22.04
  shell: ["/bin/bash", "-o", "errexit", "-o", "xtrace", "-o", "pipefail", "-c"]

steps:
  # Exit immediately if any command fails
  - run: npm install && npm run build && npm test

  # Print commands before execution (debug)
  - run: echo "Starting deployment" && deploy.sh

Flags explained:

  • -o errexit (or -e): Exit immediately if any command fails
  • -o xtrace (or -x): Print commands before executing (useful for debugging)
  • -o pipefail: Return exit code of the first failed command in a pipeline

Important Notes ​

  • Only affects step commands: The shell wrapper is applied to your workflow step commands, not to container startup commands
  • Without shell: Commands execute in Docker exec form without shell interpretation (operators like && won't work)
  • Command joining: Commands are joined with spaces, so shell operators work naturally
  • Quoting: If your shell command arguments contain spaces, quote them in YAML: run: echo "hello world"

Configuration Options ​

The container field accepts either a string (exec mode) or an object (exec or image mode).

String Form (Exec Mode) ​

yaml
container: my-running-container  # Exec into existing container with defaults

Object Form ​

yaml
# Image mode - create new container
container:
  name: my-workflow-container # Custom container name (optional)
  image: ubuntu:22.04        # Required for image mode
  pull_policy: missing       # always | missing | never | fallback
  volumes:                   # Volume mounts
    - /host:/container
  env:                       # Environment variables
    - KEY=value
  env_file: .env.container   # dotenv file(s) loaded before env
  working_dir: /app           # Working directory
  user: "1000:1000"          # User/group
  platform: linux/amd64      # Platform
  ports:                     # Port mappings
    - "8080:8080"
  network: host              # Network mode
  startup: keepalive         # keepalive | entrypoint | command
  command: ["sh", "-c", "my-daemon"] # when startup: command
  wait_for: running           # running | healthy
  log_pattern: "Ready"        # optional regex; wait for log pattern
  restart_policy: unless-stopped  # optional Docker restart policy (no|always|unless-stopped)
  keep_container: true        # Keep after workflow
  shell: ["/bin/bash", "-c"] # Shell wrapper for step commands
yaml
# Exec mode - use existing container
container:
  exec: my-running-container  # Required for exec mode
  user: root                  # Optional: override user
  working_dir: /app            # Optional: override working directory
  env:                        # Optional: additional environment variables
    - DEBUG=true
  env_file: .env.exec         # Optional: dotenv file(s) loaded before env
  shell: ["/bin/sh", "-c"]    # Shell wrapper for step commands

Field Availability by Mode ​

FieldString ModeExec ModeImage Mode
execN/A (implicit)RequiredNot allowed
imageNot allowedNot allowedRequired
userN/AOptionalOptional
working_dirN/AOptionalOptional
envN/AOptionalOptional
env_fileN/AOptionalOptional
shellN/AOptionalOptional
nameN/ANot allowedOptional
pull_policyN/ANot allowedOptional
volumesN/ANot allowedOptional
portsN/ANot allowedOptional
networkN/ANot allowedOptional
platformN/ANot allowedOptional
startupN/ANot allowedOptional
commandN/ANot allowedOptional
wait_forN/ANot allowedOptional
log_patternN/ANot allowedOptional
restart_policyN/ANot allowedOptional
keep_containerN/ANot allowedOptional

Pull Policy ​

pull_policy defaults to missing in image mode.

ValueBehavior
alwaysPull the image before starting the container.
missingPull only when a compatible local image is not available.
neverUse a compatible local image without contacting the registry.
fallbackTry to pull first, then use a compatible local image if the pull fails. The workflow fails if neither succeeds.

Validation and Errors ​

Common rules:

  • exec and image are mutually exclusive; specifying both causes an error.
  • Either exec or image must be specified (or use string form for exec).

Image mode:

  • image is required.
  • name is optional; if specified, must be unique. If a container with the same name already exists (running or stopped), the workflow fails.
  • volumes must use source:target[:ro|rw] format. Source-side $VAR and ${VAR} values are expanded from the Dagu process environment; relative bind paths are resolved from the DAG working_dir; invalid formats fail.
  • ports accept "80", "8080:80", "127.0.0.1:8080:80"; container port may have /tcp|udp|sctp (default tcp); invalid formats fail.
  • network accepts bridge, host, none, container:<name|id>, or a custom network name.
  • restart_policy supports no, always, or unless-stopped; other values fail.
  • startup must be one of keepalive (default), entrypoint, command; invalid values fail.
  • wait_for must be running (default) or healthy; if healthy is chosen but no healthcheck exists, Dagu falls back to running with a warning.
  • log_pattern must be a valid regex; readiness waits up to 120s (including log_pattern), then errors with the last known state.

Exec mode:

  • The container must exist and be running; Dagu waits up to 120 seconds for the container to be running.
  • Fields like volumes, ports, network, pull_policy, etc. cannot be used with exec (they're only valid when creating a new container).
  • Only user, working_dir, env, env_file, and shell can override the container's defaults.

Shell field:

  • Non-empty array: first element is shell path, last element is command flag (e.g., -c)
  • Available in both image and exec modes

Multiple Commands ​

Multiple commands share the same step configuration, including the container config:

yaml
steps:
  - id: build_and_test
    container:
      image: node:24
      volumes:
        - ./src:/app
      working_dir: /app
    run:
      - npm install
      - npm run build
      - npm test

Instead of duplicating the container, env, retry_policy, preconditions, etc. across multiple steps, combine commands into one step. All commands run in the same container instance, sharing the filesystem state (e.g., node_modules from npm install).

Key Benefits ​

  • Shared Environment: All steps share the same filesystem and installed dependencies
  • Performance: No container startup overhead between steps
  • Consistency: Guaranteed same environment for all steps
  • Simplicity: No need to configure Docker executor for each step

Execution Model and Entrypoint Behavior ​

  • How it runs: When you set a DAG-level container, Dagu starts one long-lived container for the workflow. By default (startup: keepalive), it runs a lightweight keepalive process (or sleep) so the container stays up. Each step then runs inside that container via docker exec.
  • Entrypoint/CMD not used for steps: Because steps are executed with docker exec, your image's ENTRYPOINT or CMD are not invoked for step commands. Steps run directly in the running container process context.
  • Implication: If your image's entrypoint is a dispatcher that expects a subcommand (for example, my-entrypoint sendConfirmationEmails which then calls npm run sendConfirmationEmails), the step command must invoke that dispatcher explicitly.

Startup Modes ​

Choose how the DAG-level container starts:

yaml
container:
  image: servercontainers/samba:latest
  startup: entrypoint   # keepalive | entrypoint | command
  wait_for: healthy      # running | healthy (default running)
yaml
container:
  image: alpine:latest
  startup: command
  command: ["sh", "-c", "my-daemon --flag"]
  restart_policy: unless-stopped   # optional
  • keepalive (default): uses an embedded keepalive binary or a shell sleep fallback.
  • entrypoint: honors the image's ENTRYPOINT/CMD with no overrides.
  • command: runs a user-provided command array instead of image defaults.

Readiness before steps run:

  • wait_for: running (default): continue once the container is running.
  • wait_for: healthy: if image defines a Docker healthcheck, wait for healthy; if not defined, Dagu falls back to running and logs a warning.
  • log_pattern: optional regex; when set, steps start only after this pattern appears in container logs (after the selected wait_for condition passes).

Readiness timeout and errors:

  • Dagu waits up to 120 seconds for readiness (running/healthy and any log_pattern). On timeout, it fails the run and reports the mode and last known state (for example, status=exited, exit_code=1).

Examples ​

Image entrypoint expects a job name as its first argument:

yaml
container:
  image: myorg/myimage:latest

steps:
  # This will NOT pass through the image ENTRYPOINT automatically.
  # Explicitly call the entrypoint script or the underlying command.
  - run: my-entrypoint sendConfirmationEmails
  # Or call the underlying command directly, if appropriate
  - run: npm run sendConfirmationEmails

If your step needs a shell to interpret operators (like &&, redirects, or environment expansion), wrap it explicitly:

yaml
steps:
  - run: sh -c "npm run prep && npm run sendConfirmationEmails"

Step-Level Container ​

If you want each step to run in its own container (as with a fresh docker run per step), use the step-level container field:

Step-level containers also apply to action: harness.run. If a DAG has a root-level container:, CLI-based harness.run steps inherit that shared container unless the step defines its own container:. See Harness Sandboxed Execution for running AI and coding-agent CLIs inside container sandboxes.

yaml
steps:
  - id: send_confirmation_emails
    container:
      image: myorg/myimage:latest
    # The container is automatically created and removed after execution
    run: sendConfirmationEmails

You can also use different containers for different steps:

yaml
steps:
  - id: build
    container:
      image: node:24
      volumes:
        - ./src:/app
      working_dir: /app
    run: npm run build

  - id: test
    container:
      image: node:24
      volumes:
        - ./src:/app
      working_dir: /app
    run: npm test
    depends: build

  - id: deploy
    container:
      image: python:3.11
      env:
        - AWS_DEFAULT_REGION=us-east-1
    run: python deploy.py
    depends: test

Step-Level Exec Mode ​

Steps can also exec into existing containers using the same syntax as DAG-level:

yaml
steps:
  # String form - exec with defaults
  - id: run_migration
    container: my-app-container
    run: php artisan migrate

  # Object form with overrides
  - id: clear_cache
    container:
      exec: my-app-container
      user: www-data
      working_dir: /var/www
    run: php artisan cache:clear
    depends: run_migration

  # Mix exec and image modes in the same workflow
  - id: run_tests
    container:
      image: node:24
    run: npm test
    depends: clear_cache

WARNING

When using step-level container, you cannot use executor or script fields on the same step.

Note: A supported step-level container: overrides the DAG-level container for that step. Without a step-level container, command steps and CLI-based harness.run steps inherit the shared DAG-level container when one is configured.

See Also ​

Dagu is open source under the GNU General Public License v3.0.