Skip to content

SSH

Execute commands on remote servers via SSH.

DAG-Level Configuration

You can configure SSH settings at the DAG level to avoid repetition:

yaml
# DAG-level SSH configuration
ssh:
  user: deploy
  host: production.example.com
  port: 22
  key: ~/.ssh/deploy_key
  password: ${SSH_PASSWORD}  # Optional; prefer keys
  strict_host_key: true  # Default: true for security
  known_host_file: ~/.ssh/known_hosts  # Default: ~/.ssh/known_hosts
  shell: "/bin/bash -e"  # Optional: string or array syntax for shell + args (DAG-level only)
  timeout: 60s  # Connection timeout (default: 30s)

steps:
  # All SSH steps inherit DAG-level configuration
  - id: health_check
    run: curl -f http://localhost:8080/health
  - id: restart_app
    run: systemctl restart myapp
    depends: health_check

Step-Level Configuration

yaml
steps:
  - action: ssh.run
    with:
      user: ubuntu
      host: 192.168.1.100
      key: /home/user/.ssh/id_rsa
      shell: "/bin/bash -o pipefail"  # Step-level `with.shell` accepts string form
      command: echo "Hello from remote server"

Configuration

DAG-Level Fields

FieldRequiredDefaultDescription
userYes-SSH username
hostYes-Hostname or IP address
portNo22SSH port
keyNoAuto-detectPrivate key path (see below)
passwordNo-Password (not recommended)
strict_host_keyNotrueEnable host key verification
known_host_fileNo~/.ssh/known_hostsKnown hosts file path
shellNo-Shell for remote command execution (string or array, e.g., "/bin/bash -e" or ["/bin/bash","-e"])
timeoutNo30sConnection timeout (e.g., 30s, 1m)
bastionNo-Bastion/jump host configuration (see below)

Step-Level Fields

FieldRequiredDefaultDescription
userYes-SSH username
hostYes-Hostname or IP address
portNo22SSH port
keyNoAuto-detectPrivate key path (see below)
passwordNo-Password (not recommended)
strict_host_keyNotrueEnable host key verification
known_host_fileNo~/.ssh/known_hostsKnown hosts file path
shellNo-Shell for remote command execution (string form only; e.g., "/bin/bash -e")
timeoutNo30sConnection timeout
bastionNo-Bastion/jump host configuration

Note: Password authentication is supported at both DAG and step level, but key-based authentication is strongly recommended.

Shell Configuration

When shell is specified, commands are wrapped and executed through the shell on the remote server:

yaml
ssh:
  user: deploy
  host: app.example.com
  shell: ["/bin/bash", "-e"]  # Commands wrapped as: /bin/bash -e -c 'command' (DAG-level example)

steps:
  - run: echo $HOME && ls -la  # Shell features like pipes, variables work

Without shell, commands are executed directly without shell interpretation. Use shell when you need:

  • Shell variable expansion ($HOME, $PATH)
  • Command chaining (&&, ||, ;)
  • Pipes (|) and redirections (>, <)
  • Glob patterns (*.txt)

Shell Priority:

  1. Step-level SSH with.shell (string only)
  2. DAG-level SSH config shell (string or array)
  3. Step-level shell field on the step (string or array, acts as fallback for UX)

Specifying Shell Arguments

  • DAG-level ssh.shell or the step-level shell field accept either string or array syntax, which is parsed into the executable plus argument list.
  • Step-level SSH with.shell currently accepts string form only because the configuration map is decoded into a string. Use quoted strings such as "/bin/bash -eo pipefail" there.

Variable Expansion Behavior

Dagu expands only DAG-scoped variables (env, params, secrets, step outputs) before sending commands to the remote host. OS-only variables (e.g., $HOME, $USER, $PATH) are not expanded locally — they pass through unchanged, letting the remote shell resolve them. This applies regardless of whether shell is configured.

yaml
ssh:
  user: deploy
  host: app.example.com

env:
  - DEPLOY_BRANCH: main

steps:
  - run: |
      cd $HOME/app              # $HOME NOT expanded — remote shell resolves it
      git checkout ${DEPLOY_BRANCH}  # Expanded by Dagu — defined in DAG env

This allows you to write shell scripts that use remote variables without Dagu replacing them:

yaml
steps:
  - action: ssh.run
    with:
      user: deploy
      host: app.example.com
      command: |
        for FILE in *.log; do
          echo "Processing ${FILE}"  # ${FILE} preserved for remote shell
        done

To emit a literal $ in SSH commands or with fields, escape it as \$. When shell is configured, the remote shell handles the escape; without shell, Dagu unescapes it before sending.

To use a local OS value in SSH commands, explicitly import it via the DAG-level env: block:

yaml
env:
  - LOCAL_HOME: ${HOME}  # Import local $HOME into DAG scope

steps:
  - action: ssh.run
    with:
      user: deploy
      host: app.example.com
      command: echo "Local home was ${LOCAL_HOME}, remote home is $HOME"

The same rule applies to SSH with fields (user, host, key, password, etc.). A reference like key: $HOME/.ssh/deploy_key will not expand $HOME because it is not DAG-scoped. Import it first:

yaml
env:
  - HOME_DIR: ${HOME}

ssh:
  user: deploy
  host: app.example.com
  key: ${HOME_DIR}/.ssh/deploy_key  # Expanded — HOME_DIR is DAG-scoped

The shell field controls whether POSIX shell expansion features (default values, parameter substitution like ${VAR:-default}) are available — it does not affect whether OS variables are expanded.

SSH Key Auto-Detection

If no key is specified, Dagu automatically tries these default SSH keys in order:

  1. ~/.ssh/id_rsa
  2. ~/.ssh/id_ecdsa
  3. ~/.ssh/id_ed25519
  4. ~/.ssh/id_dsa

Bastion Host

Connect through a jump host:

yaml
ssh:
  user: deploy
  host: private-server.internal
  bastion:
    host: bastion.example.com
    user: jump-user
    key: ~/.ssh/bastion_key

steps:
  - run: hostname

Step-level bastion:

yaml
steps:
  - action: ssh.run
    with:
      user: deploy
      host: private-server.internal
      bastion:
        host: bastion.example.com
        user: jump-user
        key: ~/.ssh/bastion_key
      command: hostname

Bastion Fields

FieldRequiredDefaultDescription
hostYes-Bastion hostname
portNo22Bastion SSH port
userYes-Bastion username
keyNoAuto-detectBastion private key path
passwordNo-Bastion password

Multiple Commands

Multiple commands share the same step configuration:

yaml
steps:
  - id: remote_checks
    action: ssh.run
    with:
      user: deploy
      host: production.example.com
      key: ~/.ssh/deploy_key
      command:
        - systemctl status nginx
        - systemctl status myapp
        - df -h /var/log
    preconditions:
      - condition: "${ENV}"
        expected: "production"

Instead of duplicating the SSH executor with block, preconditions, retry_policy, env, etc. across multiple steps, combine commands into one step.

Important: Each command runs in a new SSH session, so:

  • Working directory resets to the user's home directory for each command
  • Environment variables set in one command don't persist to the next
  • Use absolute paths or combine commands with && if you need shared context

Security Best Practices

  1. Host Key Verification: Always enabled by default (strict_host_key: true)

    • Prevents man-in-the-middle attacks
    • Uses ~/.ssh/known_hosts by default
    • Only disable for testing environments
  2. Key-Based Authentication: Strongly recommended

    • Prefer keys over passwords at all times
    • Use dedicated deployment keys with limited permissions
    • Rotate keys regularly
  3. Known Hosts Management:

    bash
    # Add host to known_hosts before running DAGs
    ssh-keyscan -H production.example.com >> ~/.ssh/known_hosts

Running Commands on Multiple Hosts

To execute the same command across multiple hosts in parallel, use parallel.items with a sub-DAG. Each host gets its own DAG run with separate logs, status, and retry tracking.

Define both DAGs in a single file using --- as a document separator. The second document becomes a local DAG, resolved by name when dag.run references it.

yaml
# deploy.yaml
steps:
  - name: run-on-servers
    parallel:
      items:
        - server1.example.com
        - server2.example.com
        - server3.example.com
      max_concurrent: 2
    action: dag.run
    with:
      dag: ssh-command
      params:
        HOST: ${ITEM}
---
name: ssh-command

params:
  - HOST: ""

steps:
  - name: run
    action: ssh.run
    with:
      host: ${HOST}
      user: deploy
      key: ~/.ssh/id_rsa
      command: uptime

For object items (e.g., different user per host), use ${ITEM.field} references:

yaml
steps:
  - name: run-on-servers
    parallel:
      items:
        - host: server1.example.com
          user: admin
        - host: server2.example.com
          user: deploy
      max_concurrent: 2
    action: dag.run
    with:
      dag: ssh-command
      params:
        HOST: ${ITEM.host}
        USER: ${ITEM.user}
---
name: ssh-command

params:
  - HOST: ""
  - USER: ""

steps:
  - name: run
    action: ssh.run
    with:
      host: ${HOST}
      user: ${USER}
      key: ~/.ssh/id_rsa
      command: uptime

See parallel.items for full fan-out options.

See Also

  • SFTP - File transfer via SFTP
  • Docker - Container execution
  • Shell - Local commands

Released under the MIT License.