Automate Maintenance on a Debian LAMP Stack Using a Bash Script

Keeping a production server patched and clean requires running the same small set of commands on a regular schedule. When performed manually, these routine tasks are easy to overlook or postpone. A bash script added to the system crontab handles them automatically and logs the results for review.

This guide covers writing a maintenance script for the Debian 12 LAMP stack built in Install and Harden a Debian 12 LAMP Stack on Amazon Lightsail. The script takes a Lightsail snapshot before patching, prunes old snapshots to control storage costs, updates installed packages, conditionally restarts PHP-FPM, Apache, and MariaDB only when relevant packages changed, cleans up stale package files, removes old rotated log files, and writes a warning when disk usage exceeds a configurable threshold. A --dry-run flag allows every action to be previewed before committing to a live run.

Prerequisites

This guide assumes the AWS CLI is installed and configured with credentials that have permission to create and delete Lightsail snapshots. If the AWS CLI is not yet installed, run:

sudo apt install awscli -y

Then configure credentials:

aws configure

Provide an AWS access key ID and secret access key for an IAM user or role with at least the lightsail:CreateInstanceSnapshot, lightsail:GetInstanceSnapshots, and lightsail:DeleteInstanceSnapshot permissions. The credentials are stored in ~/.aws/credentials for the current user and reused automatically by subsequent AWS CLI commands.

To verify the configuration is working before proceeding:

aws lightsail get-instances --query 'instances[*].name'

The output should list the Lightsail instance names.

The script uses sudo for apt, systemctl, and rm commands. On a default Lightsail Debian instance, the default user has passwordless sudo configured, which allows these commands to run unattended in a cron job. If sudoers has been tightened to require a password, the cron job will fail silently on every sudo command. Verify that passwordless sudo is in place for the relevant commands before scheduling the script.

NOTE: The snapshot steps require the instance name as it appears in the Lightsail console. The script uses a variable for this. Set the variable to the actual instance name before running the script for the first time.

What the Maintenance Script Does

The script performs seven tasks on each run:

Create the Script

Create a scripts directory in the home directory and set permissions so only the owner can read, write, or execute it:

mkdir -m 700 ~/scripts

Create the script file and set the same permissions:

touch ~/scripts/maintenance.sh
chmod 700 ~/scripts/maintenance.sh

Open the file for editing:

nano ~/scripts/maintenance.sh

Copy the full script from the Full Source Code section at the end of this article into the file. Replace your-instance-name in the configuration section with the name of the Lightsail instance as it appears in the console, then save the file.

NOTE: If the PHP version is upgraded in the future, update the PHP_VERSION variable at the top of the script to match. See Upgrade PHP on a Debian LAMP Stack for the full upgrade process. Updating the single variable at the top of the script is all that is required.

Dry Run

Before running the script against a live server, use the --dry-run flag to preview every action without making any changes:

bash ~/scripts/maintenance.sh --dry-run

In dry-run mode, each state-changing command is printed to the terminal prefixed with [DRY RUN] Would run: rather than executed. Read-only commands such as aws lightsail get-instance-snapshots still run during a dry run. Commands that would modify the system are not executed, although commands used to evaluate the process may still write error output to the log file.

The find commands that build the list of old log files run unconditionally, before the dry-run check, so dry-run output shows the actual filenames that would be deleted rather than the find expression itself. This is useful for confirming which log files would be removed before the command runs.

The snapshot listing command that drives pruning is read-only and intentionally runs in dry-run mode so the output shows which snapshots would be deleted.

Example dry-run output for a run where PHP packages were updated:

=== DRY RUN MODE: No changes will be made ===
[2026-06-29 03:00:01] === Maintenance started ===
[2026-06-29 03:00:01] Creating snapshot: your-instance-name-maintenance-20260629
[DRY RUN] Would run: aws lightsail create-instance-snapshot --instance-name your-instance-name --instance-snapshot-name your-instance-name-maintenance-20260629
[2026-06-29 03:00:02] Snapshot creation initiated.
[2026-06-29 03:00:02] Pruning old maintenance snapshots (retaining 4 most recent)...
[2026-06-29 03:00:03] Deleting old snapshot: your-instance-name-maintenance-20260525
[DRY RUN] Would run: aws lightsail delete-instance-snapshot --instance-snapshot-name your-instance-name-maintenance-20260525
[2026-06-29 03:00:03] Would delete snapshot: your-instance-name-maintenance-20260525
[2026-06-29 03:00:03] Snapshot pruning complete. Retained 4 most recent snapshots.
[2026-06-29 03:00:03] Running apt update...
[DRY RUN] Would run: sudo apt update
[2026-06-29 03:00:03] Running apt upgrade...
[DRY RUN] Would run: sudo apt upgrade -y
[DRY RUN] Would check for PHP package updates and restart php8.4-fpm if needed.
[DRY RUN] Would check for Apache package updates and restart apache2 if needed.
[DRY RUN] Would check for MariaDB package updates and restart mariadb if needed.
[2026-06-29 03:00:03] Running apt autoclean...
[DRY RUN] Would run: sudo apt autoclean
[2026-06-29 03:00:03] Cleaning up old Apache rotated log files...
[DRY RUN] Would run: sudo rm -f /var/log/apache2/access.log.6.gz /var/log/apache2/error.log.6.gz
[2026-06-29 03:00:03] Cleaning up old PHP-FPM rotated log files...
[DRY RUN] Would run: sudo rm -f /var/log/php8.4-fpm.log.6.gz
[2026-06-29 03:00:04] Current disk usage on /: 34%
[2026-06-29 03:00:04] === Maintenance completed successfully ===

Verify the Script Runs Correctly

After reviewing the dry-run output, run the script to confirm it executes without errors:

bash ~/scripts/maintenance.sh

Review the log file to confirm each step completed successfully:

cat ~/scripts/maintenance.log

The log shows timestamped entries for each step. If PHP-FPM, Apache, or MariaDB fail to restart, the log includes an ERROR line identifying which service failed. Run sudo systemctl status php8.4-fpm, sudo systemctl status apache2, or sudo systemctl status mariadb to investigate before proceeding.

Confirm the snapshot was created in the Lightsail console under the Snapshots tab, or verify via the CLI:

aws lightsail get-instance-snapshots --query 'instanceSnapshots[*].[name,state]'

NOTE: Lightsail snapshot creation is asynchronous. The create-instance-snapshot command returns after the snapshot request has been accepted, not after the snapshot has finished. Wait until the snapshot state is available before relying on it as a restore point.

Schedule the Script With cron

Open the current user’s crontab for editing:

crontab -e

Add the following line to run the script at 3:00 AM every Sunday:

0 3 * * 0 /bin/bash /home/username/scripts/maintenance.sh

Replace username with the actual system username. Using the full path to both bash and the script avoids PATH resolution issues that can cause cron jobs to fail silently.

NOTE: The script writes its log to ~/scripts/maintenance.log. In a cron context, ~ resolves to the home directory of the user running the cron job. Ensure the cron entry runs as the same user who created the script, or replace ~ in the LOG_FILE variable with an absolute path such as /home/username/scripts/maintenance.log.

Save and exit the editor. cron will now run the maintenance script automatically each week.

Review the Log

After the first scheduled run, review the log to confirm the script executed as expected:

cat ~/scripts/maintenance.log

Each run appends a timestamped block to the log file. Over time the file will grow. Truncate it periodically with truncate -s 0 ~/scripts/maintenance.log or add a logrotate entry to rotate and compress it automatically.

Full Source Code

#!/bin/bash

# -------------------------------------------------------
# Configuration
# -------------------------------------------------------

INSTANCE_NAME="your-instance-name"
PHP_VERSION="8.4"
LOG_FILE=~/scripts/maintenance.log
SNAPSHOT_NAME="${INSTANCE_NAME}-maintenance-$(date +%Y%m%d)"

# Number of weekly maintenance snapshots to retain.
# Snapshots older than this count will be deleted.
# Lightsail charges for snapshot storage; adjust based
# on retention needs and cost tolerance.
SNAPSHOT_RETAIN=4

# Maximum time to wait for the new snapshot to become
# available, in seconds.
SNAPSHOT_TIMEOUT=600

# How often to check the snapshot state, in seconds.
SNAPSHOT_POLL_INTERVAL=10

# Disk usage percentage threshold for the root filesystem.
# A warning is written to the log if usage exceeds this value.
DISK_WARN_THRESHOLD=80

# -------------------------------------------------------
# Dry-run mode
# -------------------------------------------------------

DRY_RUN=false

if [ "$1" = "--dry-run" ]; then
    DRY_RUN=true
    echo "=== DRY RUN MODE: No changes will be made ==="
fi

# Track whether any maintenance operation failed.
MAINTENANCE_FAILED=false

# -------------------------------------------------------
# Wrap commands that make changes with this function.
# In dry-run mode, the command is printed but not executed.
# -------------------------------------------------------

run() {
    if [ "$DRY_RUN" = true ]; then
        echo "[DRY RUN] Would run: $*"
    else
        "$@"
    fi
}

# -------------------------------------------------------
# Logging helper
# In dry-run mode, log messages go to stdout rather than the
# log file. Read-only commands may still write error output
# to the log file.
# -------------------------------------------------------

log() {
    if [ "$DRY_RUN" = false ]; then
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
    else
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    fi
}

log "=== Maintenance started ==="

# -------------------------------------------------------
# Step 1: Take a Lightsail snapshot before patching
# -------------------------------------------------------

log "Creating snapshot: ${SNAPSHOT_NAME}"

run aws lightsail create-instance-snapshot \
    --instance-name "$INSTANCE_NAME" \
    --instance-snapshot-name "$SNAPSHOT_NAME"

if [ $? -ne 0 ] && [ "$DRY_RUN" = false ]; then
    log "ERROR: Snapshot creation failed. Aborting maintenance."
    exit 1
else
    log "Snapshot creation initiated."
fi

# Wait for the snapshot to become available before making changes.
if [ "$DRY_RUN" = false ]; then
    log "Waiting for snapshot to become available..."

    SNAPSHOT_WAITED=0
    SNAPSHOT_STATE=""

    while [ "$SNAPSHOT_WAITED" -lt "$SNAPSHOT_TIMEOUT" ]; do
        SNAPSHOT_STATE=$(aws lightsail get-instance-snapshot \
            --instance-snapshot-name "$SNAPSHOT_NAME" \
            --query 'instanceSnapshot.state' \
            --output text 2>/dev/null)

        if [ $? -ne 0 ]; then
            log "ERROR: Unable to determine snapshot state. Aborting maintenance."
            exit 1
        fi

        if [ "$SNAPSHOT_STATE" = "available" ]; then
            log "Snapshot is available."
            break
        fi

        if [ "$SNAPSHOT_STATE" = "error" ]; then
            log "ERROR: Snapshot entered an error state. Aborting maintenance."
            exit 1
        fi

        log "Snapshot state: ${SNAPSHOT_STATE}. Waiting ${SNAPSHOT_POLL_INTERVAL} seconds..."
        sleep "$SNAPSHOT_POLL_INTERVAL"
        SNAPSHOT_WAITED=$((SNAPSHOT_WAITED + SNAPSHOT_POLL_INTERVAL))
    done

    if [ "$SNAPSHOT_STATE" != "available" ]; then
        log "ERROR: Snapshot did not become available within ${SNAPSHOT_TIMEOUT} seconds. Aborting maintenance."
        exit 1
    fi
fi

# -------------------------------------------------------
# Step 2: Prune old maintenance snapshots
# This listing command is read-only and intentionally runs
# in dry-run mode so the output shows which snapshots
# would be deleted.
# -------------------------------------------------------

log "Pruning old maintenance snapshots (retaining ${SNAPSHOT_RETAIN} most recent)..."

SNAPSHOTS=$(
    aws lightsail get-instance-snapshots \
        --query "instanceSnapshots[?starts_with(name, '${INSTANCE_NAME}-maintenance-') == \`true\`]|sort_by(@, &createdAt)|reverse(@)[*].name" \
        --output text 2>>"$LOG_FILE"
)

if [ $? -ne 0 ]; then
    log "ERROR: Failed to retrieve maintenance snapshots. Skipping snapshot pruning."
    MAINTENANCE_FAILED=true
elif [ -z "$SNAPSHOTS" ]; then
    log "No maintenance snapshots found to prune."
else
    COUNT=0

    for SNAP in $SNAPSHOTS; do
        COUNT=$((COUNT + 1))

        if [ $COUNT -gt $SNAPSHOT_RETAIN ]; then
            log "Deleting old snapshot: ${SNAP}"

            run aws lightsail delete-instance-snapshot \
                --instance-snapshot-name "$SNAP"

            if [ $? -ne 0 ] && [ "$DRY_RUN" = false ]; then
                log "ERROR: Failed to delete snapshot ${SNAP}."
                MAINTENANCE_FAILED=true
            else
                if [ "$DRY_RUN" = true ]; then
                    log "Would delete snapshot: ${SNAP}"
                else
                    log "Deleted snapshot: ${SNAP}"
                fi
            fi
        fi
    done

    log "Snapshot pruning complete. Retained ${SNAPSHOT_RETAIN} most recent snapshots."
fi

# -------------------------------------------------------
# Step 3: Update package index and upgrade packages
# -------------------------------------------------------

log "Running apt update..."

if [ "$DRY_RUN" = true ]; then
    echo "[DRY RUN] Would run: sudo apt update"
else
    if ! sudo apt update; then
        log "ERROR: apt update failed. Aborting maintenance."
        exit 1
    fi
fi

log "Running apt upgrade..."

if [ "$DRY_RUN" = true ]; then
    echo "[DRY RUN] Would run: sudo apt upgrade -y"
    UPGRADE_OUTPUT=""
    UPGRADE_STATUS=0
else
    UPGRADE_OUTPUT=$(sudo apt upgrade -y 2>&1)
    UPGRADE_STATUS=$?

    echo "$UPGRADE_OUTPUT" >> "$LOG_FILE"

    if [ "$UPGRADE_STATUS" -ne 0 ]; then
        log "ERROR: apt upgrade failed."
        MAINTENANCE_FAILED=true
    fi
fi

# -------------------------------------------------------
# Step 4: Conditionally restart services
# -------------------------------------------------------

# Restart PHP-FPM only if a PHP package was upgraded.
if [ "$DRY_RUN" = false ] && echo "$UPGRADE_OUTPUT" | grep -q "php${PHP_VERSION}"; then
    log "PHP packages updated. Restarting php${PHP_VERSION}-fpm..."

    run sudo systemctl restart "php${PHP_VERSION}-fpm"

    if [ $? -ne 0 ]; then
        log "ERROR: php${PHP_VERSION}-fpm failed to restart. Check systemctl status."
        MAINTENANCE_FAILED=true
    else
        log "php${PHP_VERSION}-fpm restarted successfully."
    fi
elif [ "$DRY_RUN" = true ]; then
    echo "[DRY RUN] Would check for PHP package updates and restart php${PHP_VERSION}-fpm if needed."
else
    log "No PHP packages updated. Skipping php${PHP_VERSION}-fpm restart."
fi

# Restart Apache only if an Apache package was upgraded.
if [ "$DRY_RUN" = false ] && echo "$UPGRADE_OUTPUT" | grep -q "apache2"; then
    log "Apache packages updated. Restarting apache2..."

    run sudo systemctl restart apache2

    if [ $? -ne 0 ]; then
        log "ERROR: apache2 failed to restart. Check systemctl status."
        MAINTENANCE_FAILED=true
    else
        log "apache2 restarted successfully."
    fi
elif [ "$DRY_RUN" = true ]; then
    echo "[DRY RUN] Would check for Apache package updates and restart apache2 if needed."
else
    log "No Apache packages updated. Skipping apache2 restart."
fi

# Restart MariaDB only if a MariaDB package was upgraded.
# MariaDB restarts drop active database connections; the
# conditional check ensures this only happens when a
# relevant package was upgraded.
if [ "$DRY_RUN" = false ] && echo "$UPGRADE_OUTPUT" | grep -q "mariadb"; then
    log "MariaDB packages updated. Restarting mariadb..."

    run sudo systemctl restart mariadb

    if [ $? -ne 0 ]; then
        log "ERROR: mariadb failed to restart. Check systemctl status."
        MAINTENANCE_FAILED=true
    else
        log "mariadb restarted successfully."
    fi
elif [ "$DRY_RUN" = true ]; then
    echo "[DRY RUN] Would check for MariaDB package updates and restart mariadb if needed."
else
    log "No MariaDB packages updated. Skipping mariadb restart."
fi

# -------------------------------------------------------
# Step 5: Clean up stale package files
# -------------------------------------------------------

log "Running apt autoclean..."

run sudo apt autoclean

if [ $? -ne 0 ] && [ "$DRY_RUN" = false ]; then
    log "ERROR: apt autoclean failed."
    MAINTENANCE_FAILED=true
fi

# -------------------------------------------------------
# Step 6: Remove old rotated log files
# OLD_APACHE_LOGS and OLD_PHP_FPM_LOGS are built by find
# before the dry-run check runs, so dry-run output shows
# the actual filenames that would be deleted rather than
# the find expression itself.
#
# sort -V (version sort) is used instead of a reverse sort
# because logrotate numbers files with the newest rotation
# as .1 and the oldest as the highest number, so ascending
# numeric order already lists newest first. tail -n +6 then
# selects everything past the 5 most recent for deletion.
# -------------------------------------------------------

OLD_APACHE_LOGS=$(find /var/log/apache2 \
    -maxdepth 1 \
    -name '*.log.*' \
    -type f \
    | sort -V \
    | tail -n +6)

if [ -n "$OLD_APACHE_LOGS" ]; then
    log "Cleaning up old Apache rotated log files..."

    run sudo rm -f $OLD_APACHE_LOGS

    if [ $? -ne 0 ] && [ "$DRY_RUN" = false ]; then
        log "ERROR: Failed to clean up old Apache rotated log files."
        MAINTENANCE_FAILED=true
    fi
else
    log "No old Apache rotated log files to clean up."
fi

OLD_PHP_FPM_LOGS=$(find /var/log \
    -maxdepth 1 \
    -name "php${PHP_VERSION}-fpm.log.*" \
    -type f \
    2>/dev/null \
    | sort -V \
    | tail -n +6)

if [ -n "$OLD_PHP_FPM_LOGS" ]; then
    log "Cleaning up old PHP-FPM rotated log files..."

    run sudo rm -f $OLD_PHP_FPM_LOGS

    if [ $? -ne 0 ] && [ "$DRY_RUN" = false ]; then
        log "ERROR: Failed to clean up old PHP-FPM rotated log files."
        MAINTENANCE_FAILED=true
    fi
else
    log "No old PHP-FPM rotated log files to clean up."
fi

# -------------------------------------------------------
# Step 7: Check disk usage
# -------------------------------------------------------

DISK_USAGE=$(df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')

log "Current disk usage on /: ${DISK_USAGE}%"

if [ "$DISK_USAGE" -ge "$DISK_WARN_THRESHOLD" ]; then
    log "WARNING: Disk usage (${DISK_USAGE}%) has exceeded the ${DISK_WARN_THRESHOLD}% threshold. Review disk usage with: df -h"
fi

# -------------------------------------------------------
# Final status
# -------------------------------------------------------

if [ "$MAINTENANCE_FAILED" = true ]; then
    log "=== Maintenance completed with errors ==="
else
    log "=== Maintenance completed successfully ==="
fi

if [ "$DRY_RUN" = false ]; then
    echo "" >> "$LOG_FILE"
fi

if [ "$MAINTENANCE_FAILED" = true ] && [ "$DRY_RUN" = false ]; then
    exit 1
fi

Design Notes

Several decisions in this script are worth explaining.

Why Dry-Run Mode Matters for a Production Maintenance Script

An automated script that runs apt upgrade, restarts services, deletes snapshots, and removes log files is making consequential changes to a production server. The --dry-run flag gives readers a way to verify that the script is configured correctly and will behave as expected before committing to a live run. This is especially important for the snapshot pruning step, where a misconfigured INSTANCE_NAME variable could match unintended snapshots. Reviewing dry-run output before the first scheduled run is a low-cost way to catch configuration mistakes before they affect a live server.

Why Take a Snapshot Before Patching Rather Than After

A snapshot taken after a successful patch run captures a clean post-patch state, which seems useful. The more valuable recovery point is immediately before any changes are applied. If a package upgrade breaks a service, the pre-patch snapshot is what allows a full rollback. A post-patch snapshot of a broken server has limited value. The snapshot is initiated at the start of the script so it reflects the last known good state of the instance.

Why Prune Snapshots Automatically

Lightsail charges for snapshot storage. Without pruning, weekly maintenance snapshots accumulate indefinitely and the storage cost compounds over time with no corresponding benefit. A six-month-old snapshot is rarely useful for recovering from a patch failure. The script retains four snapshots by default, providing roughly a month of rollback history. The SNAPSHOT_RETAIN variable at the top of the script makes it easy to increase or decrease this based on cost tolerance and recovery requirements. The pruning logic filters specifically for snapshots whose names begin with the maintenance snapshot prefix, so manually created snapshots are never touched.

Why apt upgrade Rather Than apt full-upgrade

apt upgrade upgrades installed packages but never removes existing packages or installs new ones to satisfy dependencies. apt full-upgrade will remove packages if that is what dependency resolution requires. For a scheduled production script running unattended, apt upgrade is the safer choice. It applies security patches without the risk of removing a package the application depends on. apt full-upgrade is appropriate for major distribution upgrades handled manually and with a snapshot in place. This distinction is covered in more detail in Upgrade PHP on a Debian LAMP Stack.

Why apt autoclean Rather Than apt autoremove

apt autoclean removes obsolete package files from the local APT cache without affecting installed software. Although apt autoremove is generally safe, it removes packages that APT considers no longer necessary. For an unattended maintenance script running on a production server, this guide favors the more conservative approach of reclaiming disk space without automatically uninstalling packages. Any package removals can be reviewed and performed manually after major upgrades or other planned maintenance.

Why Restart PHP-FPM Before Apache

PHP-FPM runs as a separate service and exposes a Unix socket that Apache connects to via the FastCGI proxy. Restarting PHP-FPM first ensures the socket is ready before Apache attempts to reconnect to it. Restarting Apache first can produce a brief window where Apache is running but the PHP-FPM socket is unavailable, causing 503 errors for any requests that arrive during that window. The install guide establishes this ordering for the same reason.

Why Conditional Restarts Rather Than Always Restarting

Restarting PHP-FPM, Apache, and MariaDB terminates active connections and briefly interrupts service. On a week where no relevant packages were updated, those restarts accomplish nothing and carry unnecessary risk. The script checks the apt upgrade output for relevant package names before deciding whether to restart each service. MariaDB restarts are particularly disruptive since active database connections are dropped rather than handled gracefully, which makes the conditional check especially important for that service.

Why Package Detection Uses Simple Pattern Matching

The script determines whether to restart PHP-FPM, Apache, or MariaDB by searching the apt upgrade output for package names associated with each service. This intentionally avoids more complex package parsing logic. Debian package names for these components include identifiers such as php<version>, apache2, and mariadb, making a simple pattern match sufficient for deciding whether a restart is warranted while keeping the script easy to read and maintain.

Why the Sury Repository Matters Here

unattended-upgrades applies automatic security updates only to repositories explicitly listed in its allowed origins configuration. The Sury repository that provides PHP 8.4 is not included by default, which means PHP security updates are silently skipped when relying on unattended-upgrades alone. This script runs apt upgrade unconditionally across all configured repositories, so Sury updates are applied on the same weekly schedule as everything else without any additional configuration.

Why Keep Only Five Rotated Log Files

Apache’s own logrotate configuration rotates its logs daily and retains up to fourteen rotations; PHP-FPM’s rotates weekly and retains up to twelve. This script imposes a uniform retention of five files across both logs, which works out to roughly a week of Apache history but closer to five weeks of PHP-FPM history, since each PHP-FPM rotation spans a full week rather than a day. The tighter number for Apache and the looser one for PHP-FPM are both intentional trade-offs against Apache’s fourteen-day and PHP-FPM’s twelve-week defaults, chosen to keep disk usage predictable on a small Lightsail instance rather than to match either service’s own rotation schedule. Adjust the tail -n +6 value independently for each cleanup command if the two logs warrant different retention windows.

Why Check Disk Usage at the End Rather Than the Beginning

Checking disk usage at the end of the script reflects the state of the filesystem after log cleanup has run, which is the most accurate picture of ongoing disk pressure. A high reading at that point means the cleanup step did not resolve the problem and something else is consuming space (for example, uploaded content, database growth, or an application generating its own logs outside the standard paths). A high reading before cleanup could be partially attributable to the log files the script is about to remove, which would be a false alarm.

Summary

The script takes a pre-patch snapshot, prunes old snapshots to control storage costs, keeps the server patched across all configured repositories including Sury, restarts services conditionally and in the correct order, clears stale APT cache entries, trims old log files, and checks disk usage after cleanup. The --dry-run flag allows every action to be previewed before the first live run. The maintenance log provides a timestamped record of each run, and the pre-patch snapshot provides a rollback point if any update causes problems.