Automating System Updates with Bash Scripts
Learn how to automate system updates with simple Bash scripts to keep Linux servers secure and up-to-date. Step-by-step guide for Linux and DevOps beginners to create, test, and schedule updates.
Short introduction
A simple, reliable update process reduces security risk and maintenance overhead. Automating system updates with a Bash script gives you control, visibility, and reproducibility while still being lightweight and transparent.
Why automate system updates?
Manual updates are fine for occasional maintenance, but they’re easy to forget and slow when you manage more than one system. Automation ensures updates run on a schedule, captures logs for auditing, and avoids repetitive manual steps.
Example: check what would update right now (Debian/Ubuntu and RHEL/Fedora):
# Debian/Ubuntu: list upgradable packages
apt list --upgradable
# RHEL/CentOS/Fedora:
yum check-update
# or for dnf:
dnf check-update
Why you might prefer a script versus unattended-upgrades:
- Scripts give you full control over when to reboot, what to exclude, and how to log.
- unattended-upgrades is great for hands-off security patches, but scripts can implement policy and hooks (pre/post) easily.
Writing a basic update Bash script
Here is a beginner-friendly script that:
- Detects apt vs dnf
- Runs updates non-interactively
- Logs output to a timestamped log file
- Exits with a non-zero status on failure
Save as /usr/local/bin/auto-update.sh and make executable (chmod +x).
#!/usr/bin/env bash
set -eo pipefail
LOG_DIR="/var/log/auto-update"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/update-$(date +%F_%H%M%S).log"
echo "Starting update at $(date)" | tee -a "$LOG_FILE"
# Detect package manager
if command -v apt >/dev/null 2>&1; then
PM="apt"
elif command -v dnf >/dev/null 2>&1; then
PM="dnf"
elif command -v yum >/dev/null 2>&1; then
PM="yum"
else
echo "No supported package manager found" | tee -a "$LOG_FILE" >&2
exit 2
fi
if [[ "$PM" == "apt" ]]; then
apt-get update 2>&1 | tee -a "$LOG_FILE"
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade 2>&1 | tee -a "$LOG_FILE"
apt-get -y autoremove 2>&1 | tee -a "$LOG_FILE"
else
# dnf and yum behavior
$PM -y upgrade 2>&1 | tee -a "$LOG_FILE"
$PM -y autoremove 2>&1 | tee -a "$LOG_FILE" || true
fi
echo "Update finished at $(date)" | tee -a "$LOG_FILE"
Commands table
| Command | Purpose |
|---|---|
| apt-get update | Refresh package index (Debian/Ubuntu) |
| apt-get -y upgrade | Install available upgrades non-interactively |
| apt-get -y autoremove | Remove orphaned packages |
| dnf -y upgrade | Upgrade packages (Fedora/RHEL newer) |
| yum -y update | Upgrade packages (older RHEL/CentOS) |
| systemctl reboot | Reboot the machine if needed |
| crontab -e | Edit user/system cron jobs |
Notes:
- For Debian/Ubuntu, using DEBIAN_FRONTEND=noninteractive prevents prompts.
- Always test scripts in a non-production environment first.
- Consider excluding kernel updates or handling them specially: kernels often require reboots that must be planned.
Scheduling updates with cron
Cron is the simplest scheduler for periodic checks. Use root’s crontab (sudo crontab -e) to ensure permissions for package operations.
Example: run the script every Sunday at 3 AM and capture environment differences:
# Edit root crontab: run Sundays at 03:00
0 3 * * 0 /usr/local/bin/auto-update.sh >> /var/log/auto-update/cron-run.log 2>&1
Common gotchas with cron:
Cron emails output to the owner. To suppress, set MAILTO="":
MAILTO=""
0 3 * * 0 /usr/local/bin/auto-update.sh >> /var/log/auto-update/cron-run.log 2>&1
Cron jobs run with a limited PATH. Either use absolute paths in your script (as the example does with command -v) or set PATH at the top of the crontab:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * 0 /usr/local/bin/auto-update.sh >> /var/log/auto-update/cron-run.log 2>&1
If you prefer systemd timers (more robust, better logging), create a .service and .timer unit. Example systemd timer is beyond this beginner tutorial but is a recommended next step for production systems.
Safety, logging, and best practices
When automating updates you must balance availability and security. Start small and build confidence:
- Dry-run first: many package managers support a simulation mode.
- Log everything: store logs with timestamps and rotate them.
- Decide reboot policy: automatic reboots can upset users; consider alerting instead.
Dry-run examples:
# Debian/Ubuntu: simulate upgrade without installing
apt-get -s upgrade
# dnf simulate:
dnf --assumeno upgrade
Simple log rotation with logrotate (example /etc/logrotate.d/auto-update):
/var/log/auto-update/*.log {
weekly
rotate 8
compress
missingok
notifempty
create 0640 root adm
}
Example: send a short failure email if updates fail (requires mailutils configured):
if ! /usr/local/bin/auto-update.sh >> /var/log/auto-update/cron-run.log 2>&1; then
echo "Auto-update failed on $(hostname) at $(date)" | mail -s "Update failure" admin@example.com
fi
Tips:
- Use set -euo pipefail at the top of scripts during development. For cron, consider catching errors and handling them gracefully so unrelated cron jobs are not impacted.
- Keep scripts idempotent: running them multiple times should not have harmful side effects.
- If updates are highly sensitive, prefer a staged rollout: update a test host group, verify, then update production.
Common Pitfalls
- Reboot surprises: applying kernel updates automatically without coordination may cause downtime. Make reboot decisions explicit in scripts; consider requiring manual confirmation for reboots or schedule them during maintenance windows.
Missing PATH or environment in cron: cron’s environment is minimal, causing commands not found.
# Ensure PATH at top of crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * 0 /usr/local/bin/auto-update.sh >> /var/log/auto-update/cron-run.log 2>&1
Running updates as a non-root user: package managers need root. Use sudo or run scripts as root.
# Wrong (non-root)
apt-get update
# Right (run as root or sudo)
sudo apt-get update
Next Steps
- Test the script in a staging VM or container before enabling it in production.
- Replace cron with systemd timers for better logging and dependency control.
- Add health checks and remote notification (Slack/email) to report update status.
👉 Explore more IT books and guides at dargslan.com.