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