Last active
April 23, 2026 06:23
-
-
Save CRCinAU/917949155eb29194405352efa3848a1d to your computer and use it in GitHub Desktop.
Alpine Linux automatic updates - place in /etc/periodic/daily/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -o pipefail | |
| # Alpine Linux Automatic Update Script | |
| # ==================================== | |
| # Configuration | |
| LOG_FILE="/var/log/alpine-auto-update.log" # Path to the update log file | |
| LOCK_FILE="/var/run/alpine-auto-update.lock" # Lock file to prevent concurrent runs | |
| BACKUP_DIR="/var/backups/pre-update" # Directory for pre-update backups | |
| ADMIN_EMAIL="admin@example.com" # Email address for update notifications | |
| SMTP_SERVER="smtp://mail.example.com" # SMTP server for sending notifications | |
| REBOOT_AFTER_KERNEL_UPDATE=1 # Set to 0 to skip rebooting after kernel updates | |
| BACKUP_RETENTION_DAYS=7 # Number of days to keep old backups | |
| MAX_START_DELAY=1800 # Maximum random delay in seconds before starting (0 to disable) | |
| # Internal state | |
| HOSTNAME=$(hostname) | |
| # Check if any ancestor process is crond (works with busybox on Alpine) | |
| is_cron_child() { | |
| local pid=$$ | |
| while [ "$pid" -gt 1 ]; do | |
| pid=$(awk '{print $4}' /proc/$pid/stat 2>/dev/null) || return 1 | |
| [ -r "/proc/$pid/comm" ] || return 1 | |
| [ "$(cat /proc/$pid/comm 2>/dev/null)" = "crond" ] && return 0 | |
| done | |
| return 1 | |
| } | |
| UPDATED_PACKAGES="" | |
| REBOOT_REQUIRED=0 | |
| # Logging function | |
| log_message() { | |
| echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" | |
| } | |
| # Create backup before update | |
| create_backup() { | |
| local today | |
| today=$(date +%Y%m%d) | |
| log_message "Creating system backup before update" | |
| mkdir -p "$BACKUP_DIR/$today" | |
| # Backup critical configurations | |
| local tar_output | |
| if ! tar_output=$(tar -czf "$BACKUP_DIR/$today/etc-backup.tar.gz" /etc/ 2>&1); then | |
| log_message "ERROR: Failed to backup /etc: $tar_output" | |
| return 1 | |
| fi | |
| # Backup package list | |
| if ! apk info -v > "$BACKUP_DIR/$today/installed-packages.txt"; then | |
| log_message "ERROR: Failed to backup package list" | |
| return 1 | |
| fi | |
| log_message "Backup completed" | |
| } | |
| # Update repository lists | |
| update_repositories() { | |
| log_message "Updating repository lists" | |
| if apk update 2>&1 | tee -a "$LOG_FILE"; then | |
| log_message "Repository update successful" | |
| return 0 | |
| else | |
| log_message "ERROR: Repository update failed" | |
| return 1 | |
| fi | |
| } | |
| # Check for available updates | |
| check_updates() { | |
| log_message "Checking for available updates" | |
| UPDATED_PACKAGES=$(apk list -u 2>/dev/null) | |
| local update_count | |
| update_count=$(echo "$UPDATED_PACKAGES" | grep -c "upgradable" || true) | |
| log_message "Found $update_count available updates" | |
| if [ "$update_count" -eq 0 ]; then | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # Install updates | |
| install_updates() { | |
| log_message "Installing available updates" | |
| echo "$UPDATED_PACKAGES" | tee -a "$LOG_FILE" | |
| if apk upgrade 2>&1 | tee -a "$LOG_FILE"; then | |
| log_message "Updates installed successfully" | |
| # Check if reboot is required (kernel update) | |
| if echo "$UPDATED_PACKAGES" | grep -qE "linux-(virt|lts|edge|hardened)"; then | |
| log_message "Kernel update detected - reboot required" | |
| REBOOT_REQUIRED=1 | |
| fi | |
| return 0 | |
| else | |
| log_message "ERROR: Update installation failed" | |
| return 1 | |
| fi | |
| } | |
| # Clean up old packages | |
| cleanup_cache() { | |
| log_message "Cleaning up package cache" | |
| if ! apk cache clean 2>&1 | tee -a "$LOG_FILE"; then | |
| log_message "WARNING: Cache cleanup encountered errors" | |
| fi | |
| log_message "Cache cleanup completed" | |
| } | |
| # Remove old backups beyond retention period | |
| cleanup_backups() { | |
| local removed | |
| removed=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +"$BACKUP_RETENTION_DAYS" -print -exec rm -rf {} \;) | |
| if [ -n "$removed" ]; then | |
| log_message "Removed backups older than $BACKUP_RETENTION_DAYS days" | |
| fi | |
| } | |
| # Send notification email (requires s-nail, no local MTA needed) | |
| send_notification() { | |
| local status="$1" | |
| local message="$2" | |
| if [ -n "$ADMIN_EMAIL" ]; then | |
| if ! apk info -e s-nail >/dev/null 2>&1; then | |
| log_message "s-nail not found, installing" | |
| apk add s-nail | |
| fi | |
| if apk info -e s-nail >/dev/null 2>&1; then | |
| if echo "$message" | mail -S v15-compat \ | |
| -S mta="$SMTP_SERVER" -S smtp-auth=none \ | |
| -s "Alpine Auto-Update $HOSTNAME: $status" "$ADMIN_EMAIL"; then | |
| log_message "Notification email sent to $ADMIN_EMAIL" | |
| else | |
| log_message "ERROR: Failed to send notification email to $ADMIN_EMAIL" | |
| fi | |
| else | |
| log_message "ERROR: Failed to install s-nail, cannot send notification" | |
| fi | |
| fi | |
| } | |
| # Main update process | |
| main() { | |
| # Random delay to stagger updates across multiple systems (only when run from cron) | |
| if [ "$MAX_START_DELAY" -gt 0 ] && is_cron_child; then | |
| local delay=$((RANDOM % MAX_START_DELAY)) | |
| log_message "Delaying start by ${delay} seconds (max ${MAX_START_DELAY}s)" | |
| sleep "$delay" | |
| fi | |
| log_message "=== Starting automatic update process ===" | |
| # Create backup | |
| if ! create_backup; then | |
| log_message "ERROR: Backup failed, aborting update" | |
| send_notification "FAILED" "Host: $HOSTNAME | |
| Pre-update backup failed, update aborted" | |
| exit 1 | |
| fi | |
| # Clean up old backups | |
| cleanup_backups | |
| # Update repositories (also serves as connectivity check) | |
| if ! update_repositories; then | |
| send_notification "FAILED" "Host: $HOSTNAME | |
| Repository update failed (check network connectivity)" | |
| exit 1 | |
| fi | |
| # Check for updates | |
| if ! check_updates; then | |
| log_message "=== Automatic update process completed ===" | |
| exit 0 | |
| fi | |
| # Install updates | |
| if install_updates; then | |
| cleanup_cache | |
| local email_body="Host: $HOSTNAME | |
| Packages updated: | |
| $UPDATED_PACKAGES | |
| Updates installed successfully." | |
| # Reboot if kernel was updated and rebooting is enabled | |
| if [ "$REBOOT_REQUIRED" -eq 1 ]; then | |
| if [ "$REBOOT_AFTER_KERNEL_UPDATE" -eq 1 ]; then | |
| log_message "Rebooting system due to kernel update" | |
| email_body="$email_body | |
| NOTE: System is rebooting due to kernel update." | |
| else | |
| log_message "Kernel update detected but REBOOT_AFTER_KERNEL_UPDATE is disabled, skipping reboot" | |
| email_body="$email_body | |
| NOTE: Kernel was updated but automatic reboot is disabled. A manual reboot is required." | |
| fi | |
| fi | |
| send_notification "SUCCESS" "$email_body" | |
| log_message "=== Automatic update process completed ===" | |
| if [ "$REBOOT_REQUIRED" -eq 1 ] && [ "$REBOOT_AFTER_KERNEL_UPDATE" -eq 1 ]; then | |
| sleep 30 | |
| reboot | |
| fi | |
| else | |
| send_notification "FAILED" "Host: $HOSTNAME | |
| Packages pending: | |
| $UPDATED_PACKAGES | |
| Update installation failed." | |
| log_message "=== Automatic update process completed ===" | |
| exit 1 | |
| fi | |
| } | |
| # Acquire lock and run main function | |
| exec 9>"$LOCK_FILE" | |
| if ! flock -n 9; then | |
| echo "Another instance is already running, exiting" | tee -a "$LOG_FILE" | |
| exit 0 | |
| fi | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment