How to Create a Bash Script for Automated Deployment

Learn how to write a Bash script for automated deployment with step-by-step instructions and examples. Perfect for Linux and DevOps beginners wanting reliable, repeatable app releases.

How to Create a Bash Script for Automated Deployment

Short introduction

Automating deployment with a Bash script saves time, reduces human error, and creates reproducible workflows. This tutorial walks through planning, writing, testing, and running a safe deployment script with clear examples and explanations for beginners.

Planning Your Deployment Script

Before you start typing a script, decide what steps your deployment needs — e.g., pulling code, building artifacts, running tests, uploading files, restarting services, and rollback behavior. Keep the script simple and idempotent (repeating it should not cause harm) and define configurable variables at the top.

Example checklist and minimal variables:

# config.sh (sourced by main script)
REPO_DIR="/home/deploy/myapp"
REMOTE_USER="deploy"
REMOTE_HOST="example.com"
REMOTE_DIR="/var/www/myapp"
BUILD_CMD="npm run build"
TEST_CMD="npm test"

Commands table (quick reference)

Command Description Example
git pull Update source from repository git -C /home/deploy/myapp pull origin main
rsync Efficiently sync files to remote server rsync -avz --delete build/ deploy@example.com:/var/www/myapp
ssh Run remote commands via SSH ssh deploy@example.com "sudo systemctl restart myapp"
docker build Build a Docker image docker build -t myapp:latest .
systemctl restart Restart a systemd service sudo systemctl restart myapp.service

Why a table? It gives a compact summary of commands you'll commonly use in the script and helps a beginner understand which tool to pick for each action.

Writing the Script: Structure and Best Practices

Start with a robust header and defensive flags. Use functions for tasks, parse arguments, and add logging. Using set -euo pipefail prevents subtle failures.

Example script skeleton:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# Load config (optional)
source "$(dirname "$0")/config.sh"

log() { printf '%s\n' "$*"; }

usage() {
  cat <<EOF
Usage: $(basename "$0") [--dry-run] [deploy|rollback]
EOF
}

dry_run=false
if [[ "${1:-}" == "--dry-run" ]]; then
  dry_run=true
  shift
fi

command="${1:-deploy}"

run() {
  if $dry_run; then
    log "[DRY-RUN] $*"
  else
    log "[EXEC] $*"
    eval "$@"
  fi
}

Explanation:

  • Shebang: ensures script runs with an appropriate shell.
  • set -euo pipefail: exit on error, treat unset vars as error, and make pipelines fail when any command fails.
  • IFS set: avoid word-splitting bugs.
  • run() wrapper: centralizes dry-run handling and logging.

Add functions for each logical step (update_repo, build, test, deploy, rollback) to keep the flow readable.

Common Operations: Build, Test, Deploy

Here are common operations with examples. Keep secrets out of the script (use environment variables or an external secrets manager).

Pulling latest code and building:

update_repo() {
  log "Updating repository in ${REPO_DIR}"
  run git -C "$REPO_DIR" fetch --all
  run git -C "$REPO_DIR" checkout main
  run git -C "$REPO_DIR" reset --hard origin/main
}

build() {
  log "Running build command"
  run bash -c "cd '$REPO_DIR' && $BUILD_CMD"
}

Running tests:

test_suite() {
  log "Running tests"
  if $dry_run; then
    log "[DRY-RUN] would run: cd '$REPO_DIR' && $TEST_CMD"
  else
    cd "$REPO_DIR"
    if ! $TEST_CMD; then
      log "Tests failed — aborting"
      exit 1
    fi
  fi
}

Deploying artifacts (example using rsync + SSH):

deploy_files() {
  log "Syncing build to remote host"
  run rsync -avz --delete "$REPO_DIR/build/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
  run ssh "${REMOTE_USER}@${REMOTE_HOST}" "sudo systemctl restart myapp.service"
}

Notes:

  • Use rsync for file sync efficiency and a --delete flag to avoid stale files.
  • Use SSH keys and restrict them appropriately; never store plaintext passwords.

Testing and Running Safely

Test locally and on a staging server. Use dry-run mode and syntax checks before executing on production. Add traps for cleanup and consider performing a healthcheck after deploy.

Syntax check and dry-run:

# check script syntax
bash -n deploy.sh

# run dry-run to see actions without modifying anything
./deploy.sh --dry-run deploy

Example trap and healthcheck:

trap 'log "Interrupted — performing cleanup"; cleanup_function' INT TERM EXIT

deploy_and_check() {
  update_repo
  build
  test_suite
  deploy_files

  # simple healthcheck
  log "Waiting for service to come up..."
  sleep 5
  status=$(ssh "${REMOTE_USER}@${REMOTE_HOST}" "curl -s -o /dev/null -w '%{http_code}' http://localhost/health")
  if [[ "$status" != "200" ]]; then
    log "Healthcheck failed (status: $status). Consider rolling back."
  else
    log "Deployment successful and healthy."
  fi
}

Explanation:

  • bash -n checks for syntax errors without executing.
  • Traps ensure cleanup if the script is killed.
  • Healthchecks help detect post-deploy regressions quickly so you can rollback.

Rollback Strategy

Always plan a rollback. The simplest rollback is to keep the last known good build/artifact and re-deploy it.

Simple rollback sketch:

# assume builds are stored with timestamps or tags
rollback() {
  log "Rolling back to previous release"
  previous_release="/var/releases/myapp-2025-10-20"
  run rsync -avz --delete "${REMOTE_USER}@${REMOTE_HOST}:${previous_release}/" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
  run ssh "${REMOTE_USER}@${REMOTE_HOST}" "sudo systemctl restart myapp.service"
}

Tips:

  • Retain multiple past releases and a symlink current -> release_n for easy swap.
  • Record the version-deployed (e.g., git SHA) in a RELEASE file to know what to roll back to.

Common Pitfalls

  • Overwriting production without backup: Always keep a copy of the current production state or keep versioned releases to revert reliably.
  • Storing secrets in plaintext: Use environment variables, vaults, or deploy keys; do not commit credentials to source control.
  • Assuming ephemeral operations succeed: Network hiccups, permission errors, and race conditions happen — add checks and retries.

Next Steps

  • Add CI integration: Run this script from your CI/CD pipeline to automate deploys on merge or tag.
  • Implement atomic deploys: Use symlink swaps or container-based deployments to avoid downtime.
  • Add observability: Integrate healthchecks, logging, and alerting (metrics, logs, PagerDuty) post-deploy.

This guide gives you a practical, beginner-friendly template and practices to build safe, repeatable Bash deployment scripts. Adapt the examples to your stack, keep secrets secure, and iterate with staging tests before moving to production.

👉 Explore more IT books and guides at dargslan.com.