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)
  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/

modules

Define deployable services/applications.

modules:
  api-server:
    type: springboot                   # Module type (see below)
    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/

    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"
      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

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.

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]