BLOG // [DEVOPS] 10 Simple Bash Scripts to Automate Your DevOps Workflow
10 Simple Bash Scripts to Automate Your DevOps Workflow

10 Simple Bash Scripts to Automate Your DevOps Workflow

A collection of 10 small yet useful Bash scripts for DevOps: backup, tmp cleanup, disk monitoring, service restart, batch rename, log rotation, archive compression, SSL checks, bulk ping, and folder sync.

DEVOPS BASH

Why collect small scripts?

In the Bash series, we went from variables, conditionals, and loops to error handling, cron, and best practices. But the real value of Bash lies in the small scripts that run every day: nightly backups, cleaning full disks, restarting a crashed service at 2 AM.

This post collects 10 lightweight scripts with a true DevOps flavor. Each script is under 30 lines, follows set -euo pipefail, quotes variables properly, and can be plugged into cron right away. The outline is inspired by community ideas and rewritten from scratch to match this series’ standards.


Dated folder backup

The most basic need: back up a config or data folder every night.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
set -euo pipefail

SOURCE_DIR="${1:-/etc/myapp}"
BACKUP_ROOT="${2:-/var/backups/myapp}"
KEEP_DAYS="${KEEP_DAYS:-7}"

DATE_TAG="$(date +%Y-%m-%d)"
DEST_DIR="${BACKUP_ROOT}/${DATE_TAG}"

mkdir -p "$DEST_DIR"
tar -czf "${DEST_DIR}/backup.tar.gz" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

echo "Backup completed: ${DEST_DIR}/backup.tar.gz"

# Delete backups older than KEEP_DAYS
find "$BACKUP_ROOT" -maxdepth 1 -type d -mtime "+${KEEP_DAYS}" -exec rm -rf {} +

Explanation:

  • tar -czf packs a whole folder into a single file that is easy to copy elsewhere.
  • -C combined with dirname / basename avoids storing absolute paths inside the archive.
  • find -mtime cleans up old backups automatically — the classic mistake is backing up without ever cleaning up until the disk fills.
  • Taking SOURCE_DIR as a parameter instead of hardcoding makes the script reusable for many apps.

Clean temp files and old caches

A bloated /tmp folder or cache directory is a common cause of full disks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env bash
set -euo pipefail

TARGET_DIR="${1:-/tmp}"
OLDER_THAN_DAYS="${2:-7}"

if [[ ! -d "$TARGET_DIR" ]]; then
  echo "ERROR: directory not found: $TARGET_DIR" >&2
  exit 1
fi

echo "Cleaning files older than ${OLDER_THAN_DAYS} days in ${TARGET_DIR}..."
find "$TARGET_DIR" -type f -mtime "+${OLDER_THAN_DAYS}" -print -delete

echo "Cleanup done."
du -sh "$TARGET_DIR"

Explanation:

  • find -mtime +7 -delete only removes old files and leaves recently used ones alone.
  • -print before -delete logs what was removed for easy auditing.
  • Checking -d before deleting guards against typos pointing at the wrong folder.
  • Dry-run with -print instead of -delete first when testing on production.

Disk usage monitoring

A compact version of the monitoring script from the monitoring post: check usage and alert past a threshold.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env bash
set -euo pipefail

THRESHOLD="${1:-80}"
MOUNT_POINT="${2:-/}"

USAGE=$(df --output=pcent "$MOUNT_POINT" | tail -1 | tr -dc '0-9')

if (( USAGE > THRESHOLD )); then
  echo "WARNING: disk usage on ${MOUNT_POINT} is ${USAGE}% (threshold: ${THRESHOLD}%)" >&2
  exit 1
else
  echo "OK: disk usage on ${MOUNT_POINT} is ${USAGE}%"
fi

Explanation:

  • df --output=pcent selects exactly the percentage column, more stable than parsing default df output.
  • tr -dc '0-9' strips the number out of a string like 82%.
  • Exit code 1 past the threshold lets cron or a pipeline detect the failure.
  • Run it via cron every 30 minutes, or trigger a webhook when the exit code is non-zero.

Auto-restart a crashed service

The classic on-call scenario: a service stops and nobody notices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

SERVICE="${1:-myapp.service}"

if systemctl is-active --quiet "$SERVICE"; then
  echo "OK: ${SERVICE} is running."
else
  echo "ALERT: ${SERVICE} is down. Restarting..."
  sudo systemctl restart "$SERVICE"

  sleep 5
  if systemctl is-active --quiet "$SERVICE"; then
    echo "RECOVERED: ${SERVICE} restarted successfully."
  else
    echo "FAILED: ${SERVICE} still down after restart." >&2
    exit 1
  fi
fi

Explanation:

  • systemctl is-active --quiet checks status via exit code, no text parsing needed.
  • Always verify again after a few seconds instead of assuming the restart worked.
  • Schedule it in cron every 5 minutes for critical services, with logs to a file for tracing.
  • For many servers, wrap it in an SSH loop as shown in the SSH post.

Batch file rename

During deploys or log rotation, you often need to rename many files by pattern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

TARGET_DIR="${1:-.}"
PATTERN="${2:-*.log}"
SUFFIX="$(date +%Y%m%d)"

shopt -s nullglob
files=( "$TARGET_DIR"/$PATTERN )

if (( ${#files[@]} == 0 )); then
  echo "No files matched: ${TARGET_DIR}/${PATTERN}"
  exit 0
fi

for file in "${files[@]}"; do
  mv -- "$file" "${file}.${SUFFIX}"
  echo "Renamed: $file -> ${file}.${SUFFIX}"
done

Explanation:

  • nullglob makes an unmatched glob expand to nothing instead of the raw pattern string.
  • Quoting "${files[@]}" correctly handles filenames with spaces.
  • mv -- prevents filenames starting with - from being treated as options.
  • A date suffix makes sorting and searching easy.

Manual log rotation

Before relying on system logrotate, understanding the rotation mechanism in Bash makes debugging easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="${1:-/var/log/myapp/app.log}"
MAX_SIZE_MB="${2:-100}"
KEEP_FILES="${3:-5}"

if [[ ! -f "$LOG_FILE" ]]; then
  echo "Log file not found: $LOG_FILE" >&2
  exit 1
fi

size_mb=$(du -m "$LOG_FILE" | cut -f1)

if (( size_mb < MAX_SIZE_MB )); then
  echo "OK: ${LOG_FILE} is ${size_mb}MB, no rotation needed."
  exit 0
fi

# Rotate: app.log.4 -> app.log.5, ..., app.log -> app.log.1
for (( i=KEEP_FILES-1; i>=1; i-- )); do
  if [[ -f "${LOG_FILE}.${i}" ]]; then
    mv -- "${LOG_FILE}.${i}" "${LOG_FILE}.$((i+1))"
  fi
done

mv -- "$LOG_FILE" "${LOG_FILE}.1"
touch "$LOG_FILE"

echo "Rotated: ${LOG_FILE} (${size_mb}MB)"

Explanation:

  • du -m reports size in MB for threshold comparison.
  • The reverse loop keeps at most KEEP_FILES copies, the oldest one gets overwritten.
  • touch recreates the log file immediately so the app does not keep writing to the moved file.
  • For apps that log continuously, send a reload signal after rotation.

Compress old log archives

After rotating logs, compress the old copies to save disk space.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="${1:-/var/log/myapp}"
OLDER_THAN_DAYS="${2:-7}"

find "$LOG_DIR" -type f -name "*.log.*" -mtime "+${OLDER_THAN_DAYS}" ! -name "*.gz" -print0 |
  while IFS= read -r -d '' logfile; do
    gzip -9 "$logfile"
    echo "Compressed: ${logfile}.gz"
  done

du -sh "$LOG_DIR"

Explanation:

  • ! -name "*.gz" avoids compressing files twice.
  • -print0 with read -d '' safely handles filenames with spaces or special characters.
  • gzip -9 uses maximum compression, ideal for text logs.
  • Run weekly via cron, right after the rotation step.

Check SSL certificate expiry

An expired certificate you forgot to renew is one of the most avoidable incidents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash
set -euo pipefail

DOMAIN="${1:-<your-domain>}"
WARN_DAYS="${2:-14}"

expiry=$(echo | openssl s_client -servername "$DOMAIN" -connect "${DOMAIN}:443" 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)

expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

if (( days_left < 0 )); then
  echo "CRITICAL: certificate for ${DOMAIN} already expired!" >&2
  exit 2
elif (( days_left <= WARN_DAYS )); then
  echo "WARNING: certificate for ${DOMAIN} expires in ${days_left} days (${expiry})." >&2
  exit 1
else
  echo "OK: certificate for ${DOMAIN} valid for ${days_left} more days."
fi

Explanation:

  • openssl s_client -connect fetches the real certificate chain from the running server.
  • -servername (SNI) is required when one IP serves multiple domains.
  • Converting the expiry date to epoch time gives an exact days-remaining count.
  • Distinct exit codes for WARNING vs CRITICAL let the alerting system route correctly.

Bulk host ping check

When managing many servers, you need a quick way to see which hosts are alive.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash
set -euo pipefail

HOSTS_FILE="${1:-hosts.txt}"

if [[ ! -f "$HOSTS_FILE" ]]; then
  echo "ERROR: hosts file not found: $HOSTS_FILE" >&2
  exit 1
fi

while IFS= read -r host || [[ -n "$host" ]]; do
  # Skip empty lines and comments
  [[ -z "$host" || "$host" =~ ^# ]] && continue

  if ping -c 2 -W 2 "$host" &>/dev/null; then
    echo "UP:   $host"
  else
    echo "DOWN: $host" >&2
  fi
done < "$HOSTS_FILE"

Sample hosts.txt:

1
2
3
4
# Team server list
webserver-01
webserver-02
db-primary

Explanation:

  • ping -c 2 -W 2 sends 2 packets with a 2-second timeout each — fast enough for dozens of hosts.
  • Reading the file line by line with while read skips # comments so you can document inline.
  • || [[ -n "$host" ]] handles a final line missing its newline.
  • Next upgrade: run checks in parallel with & and wait as in the parallel post.

Sync folders with rsync

Copying folders between machines or doing incremental backups is much faster with rsync than cp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
set -euo pipefail

SOURCE_DIR="${1:-/srv/data/}"
DEST="${2:-backup-host:/srv/backup/data/}"
LOG_FILE="${LOG_FILE:-/var/log/sync-data.log}"

if [[ ! -d "${SOURCE_DIR%/}" && ! -e "$SOURCE_DIR" ]]; then
  echo "ERROR: source not found: $SOURCE_DIR" >&2
  exit 1
fi

rsync -avz --delete \
  --exclude='.cache/' \
  --exclude='*.tmp' \
  "$SOURCE_DIR" "$DEST" 2>&1 | tee -a "$LOG_FILE"

echo "Sync completed at $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$LOG_FILE"

Explanation:

  • -a preserves permissions, ownership, and timestamps; -z compresses on the wire; -v gives detailed logs.
  • --delete syncs deletions — files removed at the source are removed at the destination, keeping both sides identical.
  • --exclude skips caches and temp files, cutting volume significantly.
  • The trailing / on SOURCE_DIR matters in rsync: with / it copies the contents, without it copies the folder itself.

How to run and schedule

Make executable and test

1
2
3
4
5
6
7
8
9
chmod +x backup.sh cleanup.sh
./backup.sh /etc/myapp /var/backups/myapp

# Debug when something fails
bash -x backup.sh /etc/myapp /var/backups/myapp

# Syntax-check before pushing to a server
bash -n *.sh
shellcheck *.sh

Plug into cron

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Open crontab
crontab -e

# Backup every night at 2 AM
0 2 * * * /opt/scripts/backup.sh /etc/myapp /var/backups/myapp >> /var/log/backup.log 2>&1

# Check service every 5 minutes
*/5 * * * * /opt/scripts/restart-service.sh myapp.service >> /var/log/service-check.log 2>&1

# Check SSL every morning
0 8 * * * /opt/scripts/check-ssl.sh example.com 14 >> /var/log/ssl-check.log 2>&1

The cron notes analyzed in the cron post still apply: always use absolute paths, declare PATH and environment variables explicitly, and redirect both stdout and stderr to a log.


Deployment notes

  • Never hardcode secrets: Every script above takes parameters or environment variables. Do not embed passwords, tokens, or webhook URLs in files. Use a chmod 600 .env file and source it when needed.
  • Always quote variables: "$SOURCE_DIR", "${files[@]}" — the rule from the best practices post. Small scripts invite overconfidence, and unquoted variables break on filenames with spaces.
  • Fail fast, report clearly: Use set -euo pipefail, validate input at the top, and return meaningful exit codes so cron and pipelines can detect failures. A script that finishes silently did not necessarily run correctly.
  • Dry-run first on production: For destructive scripts (cleanup, find -delete) and sync (rsync --delete), first run with -print or --dry-run, confirm the file list, then allow real deletion.
  • One script, one job: Each file above does exactly one task. When you need a multi-step chain, write an orchestrator that calls each child script instead of stuffing everything into one long, untestable file.
  • Lint before committing: Run shellcheck and bash -n on all 10 scripts. Add them to pre-commit or CI so the team keeps one standard.

Conclusion

None of the ten scripts above are fancy — backup, tmp cleanup, disk check, service restart, rename, log rotation, archive compression, SSL check, host ping, folder sync. Together, though, they save hours of manual work every week and directly reduce overnight incidents.

Start with a single script: pick your team’s biggest pain point today, deploy that script via cron, observe it for a week, then add the next one. Once the small scripts are stable, you have a solid foundation to move toward Bash in CI/CD and centralized monitoring.

Do not leave automation on paper — run your first script today.

RESPONSES & DISCUSSION