Configuration Reference

Tow uses a single tow.yaml file to define your entire deployment topology. This page covers every configuration option.

File Location

Tow looks for configuration in this order:

  1. Path specified by --config flag
  2. tow.yaml in the current directory
  3. tow.local.yaml is merged on top (for local overrides)

Tip: Add tow.local.yaml to .gitignore. Use it for developer-specific SSH key paths or custom settings that shouldn’t be shared.

Full Schema

project

Top-level project metadata.

project:
  name: my-project        # Required. Project name
  version: 1.0.0          # Project version
  base_dir: /app           # Base directory on remote servers (default: /app)

defaults

Global defaults applied to all environments and modules.

defaults:
  ssh_user: ec2-user          # Default SSH username (default: ec2-user)
  ssh_port: 22                # Default SSH port (default: 22)
  ssh_key_path: ~/.ssh/id_rsa # Path to SSH private key
  deploy_dir: deploy          # Deploy directory name (default: deploy)
  deploy_path: "{module}"     # Deploy path template (default: "{module}")
  log_dir: log                # Log subdirectory name (default: log)
  log_file: std.log           # Default log filename (default: std.log)
  health_check:
    type: tcp                  # tcp | http | log | command
    timeout: 300               # Total timeout in seconds (default: 300)
    interval: 5                # Check interval in seconds (default: 5)
    retries: 60                # Max retry count (default: 60)

environments

Define target environments with their servers.

environments:
  dev:
    ssh_user: ubuntu
    # ssh_key_path in tow.local.yaml (don't commit secrets)
    branch: develop
    variables:
      LOG_LEVEL: debug
    servers:
      # Named server — target with: tow deploy -s api-1
      - name: api-1
        host: 10.0.1.10
        modules: [api-server]
        labels:
          role: api

  prod:
    # ssh_key_path in tow.local.yaml
    branch_policy:
      allowed: [main, release/*, hotfix/*]
      commands: [deploy, auto, start, restart]
    servers:
      # Multiple servers with same config → hosts shorthand
      - name: api
        hosts: [52.78.100.1, 52.78.100.2]    # auto-expands to api-1, api-2
        modules: [api-server]

      # Servers with per-server config → individual entries
      - name: kafka-1
        host: 10.0.2.1
        modules: [kafka]
      - name: kafka-2
        host: 10.0.2.2
        modules: [kafka]

Server naming

Servers are identified by name (recommended) or number (legacy):

servers:
  # ✅ Recommended: use name
  - name: api-1
    host: 10.0.1.10
    modules: [api-server]

  # ✅ Shorthand: same config for multiple hosts
  - name: redis
    hosts: [10.0.3.1, 10.0.3.2, 10.0.3.3]  # → redis-1, redis-2, redis-3
    modules: [redis]

  # ⚠️ Legacy: number-based (still supported)
  - number: 1
    host: 10.0.1.10

Target a specific server by name:

tow deploy -e prod -m kafka -s kafka-2      # by name
tow logs -e prod -m api-server -s api-1     # by name
tow deploy -e prod -m api-server -s 1       # by number (legacy)

Per-server configuration

Use hierarchical config directories with server names:

config/
├── application.yml              ← shared defaults
├── prod/
│   └── application.yml          ← prod overrides
├── prod-kafka-1/
│   └── server.properties        ← kafka-1 specific (broker.id=1)
├── prod-kafka-2/
│   └── server.properties        ← kafka-2 specific (broker.id=2)
└── prod-kafka-3/
    └── server.properties        ← kafka-3 specific (broker.id=3)

Resolution order: config/{env}-{serverName}/ > config/{env}/ > config/

defaults.deploy_path

Controls the remote directory name for each module instance.

ValueResolves toExample
"{module}" (default)Module name only/app/api-server/
"{module}-{server}"Module + server number (legacy)/app/api-server-1/

If you are migrating from a system that used numbered directories, set deploy_path: "{module}-{server}" to preserve compatibility.

defaults.log_dir

Name of the log subdirectory inside each module directory. Defaults to "log". For example, with the default value, logs are stored at /app/api-server/log/.

defaults.log_file

Default log filename within log_dir. Defaults to "std.log". This is used by tow logs when no explicit log_path is set on the module.

modules

Define deployable services/applications.

modules:
  api-server:
    type: springboot                   # Module type (see below)
    version: 3.2.1                     # Version pin (required for plugin modules)
    port: 8080                         # Application port
    build_cmd: ./gradlew :api-server:bootJar
    artifact_path: api-server/build/libs/*.jar
    start_cmd: bin/server start        # Custom start command
    stop_cmd: bin/server stop          # Custom stop command
    status_cmd: bin/server status      # Custom status command
    log_path: log/std.log              # Log file path (relative to module dir)
    deploy_dir: deploy                 # Override deploy directory
    config_dir: config                 # Config directory for hierarchical resolution

    package_includes:                  # Files/dirs to include in package
      - lib/
      - bin/
      - conf/

    package_layout:                    # Customizable package directory structure
      "script/":               "bin/"
      "build/libs/*.jar":      "lib/"
      "config/${ENV}/":        "conf/"

    data_dirs:                         # Persistent directories (survive deploys)
      - data
      - cache

    variables:                         # Module-level variables
      JAVA_OPTS: -Xms512m -Xmx1024m

    health_check:
      type: http                       # tcp | http | log | command
      target: http://localhost:8080/actuator/health
      timeout: 120
      interval: 3

    hooks:
      pre_build: ./gradlew test
      post_build: echo "Build complete"
      pre_deploy: echo "Starting deploy"
      post_deploy: "curl -X POST https://hooks.slack.com/services/xxx"
      post_install: |                  # Runs after extract + symlink (before stop/start)
        cd current && python -m venv .venv
        cd current && .venv/bin/pip install -r requirements.txt
      pre_start: ""
      post_start: ""
      pre_stop: ""
      post_stop: ""

    ssh:                               # Per-module SSH override
      user: app-deploy
      port: 2222
      auth: key                        # key | password | agent
      key_path: ~/.ssh/special-key.pem

modules.{name}.version

Version pinning for plugin modules (e.g., Kafka, Redis, PostgreSQL). When set, Tow downloads and installs that exact version. This is required for plugin modules to prevent accidental upgrades.

modules:
  kafka:
    type: kafka
    version: 3.6.1    # Required — pinned version
    port: 9092

If version is omitted for a plugin module, Tow prints a warning:

⚠  WARNING: Module 'kafka' (type: kafka) has no version pin.
   Set 'version' in tow.yaml to prevent accidental upgrades.

For built-in application types (springboot, node, python, etc.), version is optional and purely informational.

Module Types

Built-in (Language/Framework)

TypeDescriptionDefault BuildDefault Health Check
springbootSpring Boot applications./gradlew :mod:bootJarHTTP /actuator/health
javaGeneric Java applications./gradlew :mod:buildTCP port
nodeNode.js applicationsnpm ci && npm run buildTCP port
pythonPython applicationspip install -r requirements.txtTCP port
goGo applicationsgo build -o bin/modTCP port
rustRust applicationscargo build --releaseTCP port
phpPHP applicationscomposer installTCP port
rubyRuby applicationsbundle installTCP port
dotnetC#/.NET applicationsdotnet publishTCP port
kotlinKotlin applications./gradlew buildTCP port
elixirElixir applicationsmix releaseTCP port
genericCustom applications(must specify)TCP port

YAML Plugins (Infrastructure — 35 services)

Infrastructure services are loaded from plugins/ directory. No Go code needed.

CategoryServices
DatabasesMySQL, PostgreSQL, MariaDB, MongoDB, ClickHouse, InfluxDB
Message BrokersKafka, RabbitMQ, ZooKeeper
Kafka EcosystemKafka Connect, Kafka Lag Exporter, KMinion, CMAK
Caching & StorageRedis, Memcached, MinIO, etcd
MonitoringPrometheus, Grafana, Node Exporter, Kibana
LoggingElasticsearch, Logstash, Fluentd, Loki, Promtail
Web & ProxyNginx, HAProxy
SecurityVault, Consul, Keycloak
CI/CDJenkins, SonarQube
Data & WorkflowAirflow, Superset

Usage example:

modules:
  my-db:
    type: postgresql     # Loaded from plugins/postgresql.yaml
    port: 5432

  my-cache:
    type: redis          # Loaded from plugins/redis.yaml
    port: 6379

All defaults can be overridden in tow.yaml. See plugins/README.md for full list and plugin authoring guide.

Package Layout

The package_layout option lets you customize how files are arranged inside the deployment package. Each key is a source glob pattern (relative to the project root) and the value is the destination directory inside the package.

modules:
  api-server:
    type: springboot
    package_layout:
      "script/":               "bin/"
      "build/libs/*.jar":      "lib/"
      "config/${ENV}/":        "conf/"

When package_layout is not set, Tow uses the default layout based on package_includes. When set, package_layout takes precedence and gives you full control over the mapping from source paths to package paths.

  • Source patterns support globs (*, **) and environment variable interpolation (${ENV})
  • Trailing / on the source copies the directory contents; without it, the file is copied directly
  • Destination paths are always relative to the package root

Hooks

Tow supports lifecycle hooks at various stages of the deployment pipeline:

HookWhen it runsExecution
pre_buildBefore build commandLocal
post_buildAfter build commandLocal
pre_deployBefore deploy pipeline startsLocal
post_deployAfter deploy pipeline completesLocal
post_installAfter extract + symlink, before stop/startRemote (on each server)
pre_startBefore start commandRemote
post_startAfter start commandRemote
pre_stopBefore stop commandRemote
post_stopAfter stop commandRemote

The post_install hook is particularly useful for environment setup that must happen after new code is in place but before the service restarts. Common uses include creating Python virtualenvs, running pip install, and executing database migrations.

Health Check Types

TCP

Checks if a port is accepting connections.

health_check:
  type: tcp
  target: ":8080"          # Port to check (default: module port)
  timeout: 60
  interval: 3

HTTP

Sends an HTTP GET and expects a 2xx response.

health_check:
  type: http
  target: http://localhost:8080/actuator/health
  timeout: 120
  interval: 5

Log

Watches a log file for a specific pattern.

health_check:
  type: log
  target: "Started Application in"   # Pattern to grep for
  timeout: 120
  interval: 3

Command

Runs a custom command; success (exit code 0) means healthy.

health_check:
  type: command
  target: "redis-cli ping | grep PONG"
  timeout: 30
  interval: 5

Environment Variable Interpolation

Use ${VAR_NAME} syntax anywhere in tow.yaml:

environments:
  prod:
    servers:
      - number: 1
        host: ${PROD_SERVER_1}    # Resolved from OS environment
      - number: 2
        host: ${PROD_SERVER_2}

modules:
  api-server:
    ssh:
      password: ${DEPLOY_PASSWORD}   # Secrets from environment

Security: Never hardcode secrets in tow.yaml. Use environment variables or tow.local.yaml (in .gitignore).

Retention Policy

Control how many old deployments are kept on remote servers.

retention:
  keep: 5              # Keep the 5 most recent deployments (default: 5)
  auto_cleanup: true   # Automatically clean up after deploy/auto (default: false)
  • The current active deployment is never removed
  • Manual cleanup: tow cleanup -e prod -m api-server --keep 3
  • Without auto_cleanup, old deployments accumulate until you run tow cleanup

Notifications

Send deployment event notifications to Slack, Discord, or any webhook.

notifications:
  - type: slack
    url: ${SLACK_WEBHOOK_URL}

  - type: discord
    url: ${DISCORD_WEBHOOK_URL}

  - type: webhook
    url: https://my-service.com/deploy-events

Events sent: deploy_start, deploy_success, deploy_failed, auto_rollback, rollback_success, rollback_failed.

Webhook payload format:

{
  "project": "my-platform",
  "environment": "prod",
  "module": "api-server",
  "event": "deploy_success",
  "message": "deployment completed",
  "timestamp": "2026-03-26T14:30:00Z"
}

Local Overrides (tow.local.yaml)

tow.local.yaml is deep-merged over tow.yaml. Perfect for:

  • Developer-specific SSH keys
  • Local port overrides
  • Custom build commands for development
# tow.local.yaml — in .gitignore
defaults:
  ssh_key_path: ~/.ssh/my-personal-key.pem

environments:
  dev:
    servers:
      - number: 1
        host: 192.168.1.100    # Local dev server

Merge rules:

  • Scalar values are replaced
  • Maps are deep-merged
  • Lists are replaced (not appended)

Hierarchical Config Directories

Tow supports hierarchical configuration resolution for your application config files:

config/
├── application.yml             ← base (all environments)
├── dev/
│   └── application.yml         ← dev environment override
├── prod/
│   └── application.yml         ← prod environment override
└── prod-1/
    └── application.yml         ← prod server-1 specific

Resolution order (highest priority first):

  1. config/{env}-{server_number}/
  2. config/{env}/
  3. config/

When packaging, Tow copies the resolved config files to the conf/ directory in the deployment archive.

Complete Example

project:
  name: my-saas
  version: 2.1.0
  base_dir: /app

defaults:
  ssh_user: ec2-user
  ssh_port: 22
  ssh_key_path: ~/.ssh/deploy.pem
  health_check:
    type: tcp
    timeout: 120
    interval: 5

environments:
  dev:
    ssh_key_path: ~/.ssh/dev.pem
    branch: develop
    variables:
      SPRING_PROFILES_ACTIVE: dev
    servers:
      - number: 1
        host: 10.0.1.10

  staging:
    ssh_key_path: ~/.ssh/staging.pem
    branch_policy:
      allowed: [develop, release/*]
    servers:
      - number: 1
        host: 10.0.2.10

  prod:
    ssh_key_path: ~/.ssh/prod.pem
    branch_policy:
      allowed: [main, hotfix/*]
      commands: [deploy, auto, start, restart, rollback]
    servers:
      - number: 1
        host: ${PROD_SERVER_1}
        modules: [api-server, batch-server]
      - number: 2
        host: ${PROD_SERVER_2}
        modules: [api-server]
      - number: 3
        host: ${PROD_SERVER_3}
        modules: [kafka, redis]

modules:
  api-server:
    type: springboot
    port: 8080
    build_cmd: ./gradlew :api-server:bootJar -x test
    artifact_path: api-server/build/libs/api-server-*.jar
    config_dir: config
    data_dirs: [uploads, cache]
    health_check:
      type: http
      target: http://localhost:8080/actuator/health
      timeout: 120
    hooks:
      pre_build: ./gradlew :api-server:test
      post_deploy: |
        curl -X POST ${SLACK_WEBHOOK} \
          -d '{"text":"api-server deployed to prod"}'

  batch-server:
    type: springboot
    port: 8081
    build_cmd: ./gradlew :batch-server:bootJar
    health_check:
      type: log
      target: "Started BatchApplication in"

  kafka:
    type: kafka
    port: 9092
    data_dirs: [kafka-logs]

  redis:
    type: redis
    port: 6379
    data_dirs: [redis-data]