AI Automation

Advanced Bash Scripting: Automate Linux Server Management

You have spent the last hour SSH-ing into the same VPS, running the same three commands to check disk space, restart a stuck service, and rotate a log file that grew out of control. Every sysadmin has been there. Bash scripting turns that hour into one command, or better, into something that runs itself while you sleep. This post shows you how to write practical, battle-tested Bash scripts for Linux server management on Ubuntu 24.04, and how to schedule them with systemd timers so they actually run on time.

  • Key takeaways:
  • Bash scripts automate repetitive tasks: backups, log rotation, resource monitoring, and service health checks.
  • Systemd timers are more reliable than cron for scheduling scripts on modern Linux, with built-in logging and failure handling.
  • Every script needs a VERIFY step: check exit codes, log output, and test the restore path before trusting automation.

Prerequisites

  • A Linux VPS running Ubuntu 24.04 LTS (the commands also work on Debian 12 with the same package names).
  • Root access or a user with sudo privileges.
  • Basic familiarity with the command line: navigating directories, editing files with nano or vim.
  • A backup destination: another directory on the VPS, an external drive, or an S3-compatible bucket (optional but recommended).

Why Bash Still Matters for Server Automation

Config management tools like Ansible and SaltStack have their place, but they add layers of complexity that a single-purpose VPS does not need. A 2GB RAM VPS running a small web application does not need a Puppet master just to run a nightly backup. Bash ships with every Linux distribution, has zero dependencies, and its syntax has not changed in decades. That stability is a feature.

The real power of Bash appears when you combine it with the tools that already live on your server. rsync for file sync, tar for compression, find for locating old files, journalctl for reading logs, and systemctl for service control. A Bash script ties these together into a repeatable workflow. When something breaks, you read the script, you see exactly what it does, and you fix it. Try that with a GUI automation tool.

Step 1 - Structuring a Reliable Bash Script

A good automation script is not a pile of commands. It has a structure that makes failures visible. Start every script with set -euo pipefail. The flag -e exits on the first error, -u treats unset variables as errors, and pipefail makes a pipeline return the failure of the last command that failed. These three flags catch the silent failures that would otherwise let a backup run partially and exit with code 0.

#!/bin/bash
set -euo pipefail

# Configuration
BACKUP_DIR="/var/backups/site"
SOURCE_DIR="/var/www/html"
LOG_FILE="/var/log/backup-script.log"

# Logging function
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

# Main logic
log "Starting backup"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/site-$(date '+%Y%m%d').tar.gz" -C / "$SOURCE_DIR"
log "Backup completed: $BACKUP_DIR/site-$(date '+%Y%m%d').tar.gz"

Notice the log() function. Every step writes a timestamped entry to a log file. When the script runs at 3 AM and something fails at 3:02 AM, you want to know exactly where it stopped. The log gives you that. Without it, you are guessing.

Verify: run the script manually and check the output file exists.

chmod +x /usr/local/bin/backup-site.sh
sudo /usr/local/bin/backup-site.sh
ls -lh /var/backups/site/
# Expected output: site-20261215.tar.gz with a real file size, not 0 bytes
cat /var/log/backup-script.log

Step 2 - Cleaning Up Old Backups Automatically

Backups are useless if they fill the disk. A VPS with a 40GB NVMe drive will choke if your script runs daily and never deletes anything. Add a retention policy directly into the script. The find command handles this cleanly: it deletes backup files older than 14 days.

#!/bin/bash
set -euo pipefail

BACKUP_DIR="/var/backups/site"
RETENTION_DAYS=14

# Delete backups older than RETENTION_DAYS
find "$BACKUP_DIR" -name "site-*.tar.gz" -type f -mtime +"$RETENTION_DAYS" -delete

# Log how much space is now used
du -sh "$BACKUP_DIR"

-mtime +14 matches files whose modification time is more than 14 days in the past. The -delete flag removes them silently. Run this after the backup, either in the same script or as a separate scheduled job. The du -sh line logs the directory size so you can track growth over time.

This is the kind of task that rent Linux VPS users run on their own boxes, because it protects them from the classic failure mode: a disk filling up at 3 AM and taking the whole site down with it.

Verify: create a dummy old file and confirm it gets removed.

touch -d "30 days ago" /var/backups/site/site-old.tar.gz
sudo /usr/local/bin/cleanup-backups.sh
ls /var/backups/site/
# Expected output: the old file is gone, only recent backups remain

Step 3 - Monitoring Disk, RAM, and Service Health

A proactive script checks the metrics that kill servers: disk usage crossing 90 percent, RAM running out, a service that died. The script below checks all three and logs a warning when something looks wrong. In a more advanced setup you would connect it to a notification channel, but logging is the correct first step.

#!/bin/bash
set -euo pipefail

THRESHOLD_DISK=90
THRESHOLD_RAM=90
LOG_FILE="/var/log/health-check.log"

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

# Check disk usage
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt "$THRESHOLD_DISK" ]; then
    log "WARNING: Disk usage at ${DISK_USAGE}%"
else
    log "OK: Disk usage at ${DISK_USAGE}%"
fi

# Check RAM usage
RAM_USAGE=$(free | awk '/^Mem:/ {printf "%.0f", $3/$2 * 100.0}')
if [ "$RAM_USAGE" -gt "$THRESHOLD_RAM" ]; then
    log "WARNING: RAM usage at ${RAM_USAGE}%"
else
    log "OK: RAM usage at ${RAM_USAGE}%"
fi

# Check if nginx is running
if systemctl is-active --quiet nginx; then
    log "OK: nginx is running"
else
    log "CRITICAL: nginx is not running"
    systemctl restart nginx
    log "Action taken: restarted nginx"
fi

The systemctl is-active --quiet nginx pattern is the cleanest way to test a service in a script. It returns exit code 0 when the service is active, non-zero otherwise. The --quiet flag suppresses output so you only see the log lines. The script takes action by restarting nginx and logging what it did. That is the difference between a script that reports problems and a script that fixes them.

When you run this on a cheap VPS priced at the entry level with 2GB of RAM, the RAM check matters most. A single runaway PHP-FPM worker can exhaust memory and trigger the OOM killer. Knowing the trend before it happens beats reading the alert afterwards.

Verify: run it and confirm the log shows the current state of your server.

sudo /usr/local/bin/health-check.sh
cat /var/log/health-check.log
# Expected output: three lines showing disk, RAM, and nginx status

Step 4 - Scheduling Scripts with Systemd Timers

Cron has been the default scheduler for decades and it still works. But systemd timers are the modern replacement on Ubuntu 24.04. They integrate with journalctl for logging, they can run missed jobs after a reboot, and they handle failures more gracefully. If your server uses systemd, use systemd timers.

A timer needs two files. The service unit defines what runs, and the timer unit defines when. Create them in /etc/systemd/system/.

# /etc/systemd/system/backup-site.service
[Unit]
Description=Run website backup script

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-site.sh
# /etc/systemd/system/backup-site.timer
[Unit]
Description=Run website backup daily at 2 AM

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target

Type=oneshot is required for scripts that run once and exit, as opposed to long-running daemons. OnCalendar takes the familiar cron-style syntax but with a more readable format. The Persistent=true line is the killer feature: if the VPS was off at 2 AM, the timer runs the job immediately at the next boot. Cron simply skips it.

Enable and start the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-site.timer
sudo systemctl list-timers

Verify: systemctl list-timers shows your timer in the NEXT column, and the LAST column shows when it last fired. The output looks like this:

NEXT                        LEFT       LAST                        PASSED    UNIT
Tue 2026-12-16 02:00:00 UTC 10h left   Mon 2026-12-15 02:00:01 UTC 13h ago   backup-site.timer

Once the timer fires, check the service result:

sudo systemctl status backup-site.service
# Expected output: "Active: inactive (dead)" with "Process: exec ... = code=exited, status=0/SUCCESS"

This pattern applies to any script. Create a pair of files for each automation job. On a Linux VPS hosting setup with multiple sites, you will quickly accumulate a small collection of timers, one per task, each independently logged and monitored.

Step 5 - Automating Log Rotation with a Script

Log rotation is the quiet killer of small servers. Nginx, PHP-FPM, MySQL, and your own scripts all write to logs that grow without bound. The logrotate utility handles this system-wide, but writing your own rotation script teaches you the mechanics and gives you control when logrotate misses a file.

#!/bin/bash
set -euo pipefail

LOG_DIR="/var/log/myapp"
RETENTION_DAYS=7

# Rotate the current log file
if [ -f "$LOG_DIR/app.log" ]; then
    mv "$LOG_DIR/app.log" "$LOG_DIR/app-$(date '+%Y%m%d').log"
    touch "$LOG_DIR/app.log"
    log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_DIR/rotation.log"; }
    log "Rotated app.log"
fi

# Delete rotated logs older than retention period
find "$LOG_DIR" -name "app-*.log" -type f -mtime +"$RETENTION_DAYS" -delete

The script renames the current log with a date stamp, creates a fresh empty file, then cleans up anything older than seven days. The application keeps writing to the same path because the new app.log exists. This is exactly what logrotate does internally, minus the configuration file.

Why write your own when logrotate exists? Because your application might have specific needs: keeping logs for exactly 14 days for compliance, compressing them with a specific tool, or shipping them elsewhere. A script gives you that flexibility. For most cases though, the built-in logrotate configuration in /etc/logrotate.d/ is the right answer.

自动化脚本能显著减少服务器管理的手动操作,但务必测试恢复流程。

Automation scripts greatly reduce manual server administration work, but always test the restore path.

Troubleshooting Common Script Failures

Three failures hit everyone who writes server scripts. The first is the script passing with set -e but a command inside a conditional behaves differently than expected. When you write if systemctl is-active --quiet nginx; then, the -e flag does not exit because the command is part of a conditional. That is correct Bash behavior, but it surprises people who expect -e to catch everything.

The second failure is the script working manually but failing in the timer. The cron and systemd environment has a minimal PATH that often lacks /usr/local/bin. Your script calls a binary that exists in your interactive shell but not in the timer environment. Fix it by defining the full path to every binary or setting PATH explicitly at the top of the script.

#!/bin/bash
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

The third failure is disk space. Your script writes a large backup, runs its cleanup, and reports success, but the cleanup ran before the backup or the retention threshold is too generous. Diagnose it with journalctl:

sudo journalctl -u backup-site.service -n 50
df -h /

journalctl -u reads the logs for that specific service unit, and df -h shows current disk usage. If the VPS is running low on disk, consider upgrading to a larger plan or moving backups off-box with rclone to S3-compatible storage.

FAQ

Is Bash still relevant when Ansible and Puppet exist?

Yes, for single-server setups. Bash has zero dependencies, works on every Linux install, and scripts are easier to read and debug than a playbook for a one-server job. Ansible shines when you manage dozens of servers. For one VPS, a well-written Bash script is simpler and faster to maintain.

Should I use cron or systemd timers in 2026?

Use systemd timers on Ubuntu 24.04 or Debian 12. Timers integrate with journalctl, support Persistent=true to catch missed runs, and handle dependencies cleanly. Cron still works and is fine for throwaway jobs, but timers are the better default on any server running systemd.

What is the safest way to test a new automation script?

Run it manually first and watch the output. Then create a test file or a test condition that triggers the failure path and confirm the script handles it correctly. Finally, schedule the timer and check the logs the next morning. Never deploy a script to production without testing the restore or recovery path.

How do I know if my backup is actually restorable?

You restore it. Extract the tar file to a temporary directory and compare a few files with the live ones. Time this restore. If it takes longer than your acceptable downtime, your backup strategy needs rethinking. A backup that has never been restored is a guess, not a backup.

Related articles

Bash 脚本自动管理 Linux 服务器

Bash 脚本仍然是管理 Linux VPS 最直接的工具。用 set -euo pipefail 确保脚本出错时立即停止,用 rsync 或 tar 做备份,用 systemd timer 替代 cron 定时执行。每个脚本都要记日志并测试恢复流程,自动化才能真正节省时间。服务器资源检查(磁盘、内存、服务状态)建议写成定时任务自动运行。

Note: This guide is for general reference. Every system and infrastructure has its own specifics, so test each step in a safe environment and consult a qualified engineer before applying it in production.