Skip to main content

Backup Container Information & Change Log

Just like the NGINX reverse proxy backup script, I decided to take a snapshot of each container's DNA and get some metadata from it, then upload it here. I often create/destroy containers trying out new things, and sometimes I implement changes to containers, but if I don't log the changes somewhere, I'll completely forget about them. So, it's also got a change log upload.

Features

  • Creates a hash of each container and stores it to help track changes
  • Puts the DNA map in one chapter, and the change log in another
  • Pulls metadata:
    • RAM Assigned
    • CPU Cores
    • Bridge
    • IP Addresses
    • OS
    • Disk Usage
    • Users with Shells
    • SSH Status
    • Lists Available Updates
  • Prints a list of services installed (I picked the most commonly used in my containers):
    • NGINX, APACHE, PHP
    • MariaDB/MySQL, PostgreSQL
    • Redis
    • Node.js
    • Python
    • Java
    • Fail2Ban
    • UFW
    • ZFS
    • Podman
    • Snap
    • Flatpack
    • Mosquitto
    • Rsync
    • Rclond
    • Certbot

Script

#!/bin/bash

# === CONFIG ===
BOOKSTACK_URL="https://YOUR SITE HERE/api"
BOOK_ID=31                 # UPDATE THIS!
CHAPTER_ID_MAPS=157        # UPDATE THIS!
CHAPTER_ID_CHANGES=156     # UPDATE THIS!
TOKEN_ID="YOUR TOKEN HERE"
TOKEN_SECRET="YOUR SECRET HERE"
HASH_DIR="$HOME/.lxc-dna-tracker"

mkdir -p "$HASH_DIR"

dtg=$(date "+%d%b%y %H%M" | tr 'a-z' 'A-Z')
CHANGE_LOG_HTML="<h2>๐Ÿงฌ Container Change Log - $dtg</h2><ul>"
any_changes=false

echo "๐Ÿงพ Starting LXC DNA Audit..."

for CTID in $(pct list | awk 'NR>1 {print $1}'); do
    HOSTNAME=$(pct exec $CTID -- hostname 2>/dev/null | xargs)
    CT_BASENAME=$(echo "$HOSTNAME" | tr ' ' '_' | tr '[:upper:]' '[:lower:]')
    HASH_FILE="$HASH_DIR/$CT_BASENAME.sha256"
    REPORT_FILE=$(mktemp)

    echo "๐Ÿ“ฆ Auditing CTID $CTID ($HOSTNAME)..."

    # === GENERATE DNA REPORT HTML ===
    {
        echo "<h2>๐Ÿ“ฆ $HOSTNAME (CTID $CTID)</h2><p><strong>Audit Timestamp:</strong> $dtg</p>"

        echo "<h3>๐Ÿ“Š Configuration</h3><table style=\"border: none; border-collapse: collapse; width: 100%;\">"
        echo "<tr><td style=\"border: none; padding: 4px;\">RAM Assigned</td><td style=\"border: none; padding: 4px;\">$(pct config $CTID | grep memory | awk '{print $2}') MB</td></tr>"
        echo "<tr><td style=\"border: none; padding: 4px;\">CPU Cores</td><td style=\"border: none; padding: 4px;\">$(pct config $CTID | grep cores | awk '{print $2}')</td></tr>"
        echo "<tr><td style=\"border: none; padding: 4px;\">Bridge</td><td style=\"border: none; padding: 4px;\">$(pct config $CTID | grep net0 | grep -oP 'bridge=\K[^, ]+')</td></tr>"
        echo "<tr><td style=\"border: none; padding: 4px;\">IP Address</td><td style=\"border: none; padding: 4px;\">$(pct exec $CTID -- hostname -I 2>/dev/null)</td></tr>"
        echo "<tr><td style=\"border: none; padding: 4px;\">OS</td><td style=\"border: none; padding: 4px;\">$(pct exec $CTID -- grep '^PRETTY_NAME=' /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '\"')</td></tr>"
        echo "</table>"

        echo "<h3>๐Ÿ’พ Disk Usage</h3><pre><code>$(pct exec $CTID -- df -h /)</code></pre>"

        echo "<h3>๐Ÿ”’ Users with Shells</h3><pre><code>$(pct exec $CTID -- getent passwd | grep '/bin/bash')</code></pre>"

        echo "<h3>๐Ÿ” SSH Status</h3><pre><code>$(pct exec $CTID -- systemctl is-active ssh 2>/dev/null || echo 'Not Running')</code></pre>"

        echo "<h3>๐Ÿ“ฅ Available Package Updates</h3><pre><code>$(pct exec $CTID -- bash -c 'apt list --upgradable 2>/dev/null | grep -v \"Listing...\"' || echo 'N/A')</code></pre>"

        echo "<h3>๐Ÿ” Services</h3><table style=\"border: none; border-collapse: collapse; width: 100%;\">"

        check() {
            local label=$1
            local cmd=$2
            local extract=$3
            local value
            if pct exec $CTID -- which $cmd > /dev/null 2>&1; then
                value=$(pct exec $CTID -- bash -c "$cmd $extract" 2>&1 | head -n1)
                echo "<tr><td style=\"border: none; padding: 4px;\">$label</td><td style=\"border: none; padding: 4px;\">$value</td></tr>"
            else
                echo "<tr><td style=\"border: none; padding: 4px;\">$label</td><td style=\"border: none; padding: 4px;\">โŒ Not installed</td></tr>"
            fi
        }

        check "๐ŸŒ Nginx" "nginx" "-v"
        check "๐ŸŒ Apache" "apache2" "-v"
        check "๐Ÿ˜ PHP" "php" "-v"
        check "๐Ÿฌ MariaDB/MySQL" "mysql" "--version"
        check "๐Ÿ˜ PostgreSQL" "psql" "--version"
        check "๐Ÿ“ฆ Redis" "redis-server" "--version"
        check "๐ŸŸข Node.js" "node" "-v"
        check "๐Ÿ Python3" "python3" "--version"
        check "โ˜• Java" "java" "-version"
        check "๐Ÿ”’ Fail2Ban" "fail2ban-server" "--version"
        check "๐Ÿ”ฅ UFW" "ufw" "--version"
        check "๐Ÿงฐ ZFS" "zpool" "--version"
        check "๐Ÿšข Podman" "podman" "--version"
        check "๐Ÿ“ฆ Snap" "snap" "--version"
        check "๐Ÿ“ฆ Flatpak" "flatpak" "--version"
        check "๐Ÿงต Mosquitto" "mosquitto" "-h"
        check "๐Ÿ—„๏ธ Rsync" "rsync" "--version"
        check "๐ŸŒŽ Rclone" "rclone" "--version"
        check "๐Ÿ›ก๏ธ Certbot" "certbot" "--version"

        echo "</table>"
    } > "$REPORT_FILE"

    # === HASH + VERSION ===
    REPORT_HASH=$(sha256sum "$REPORT_FILE" | awk '{print $1}')
    version=$(ls "$HASH_DIR/${CT_BASENAME}_v"*.sha256 2>/dev/null | wc -l)
    version=$((version + 1))
    cp "$REPORT_FILE" "$HASH_DIR/${CT_BASENAME}_v${version}.sha256"
    PAGE_NAME="$CTID $HOSTNAME Ver $version $dtg"

    # === NEW CONTAINER ===
    if [[ ! -f "$HASH_FILE" ]]; then
        echo "๐Ÿ†• New container: $HOSTNAME โ†’ Adding to Container Maps"
        json_payload=$(jq -n \
          --arg name "$PAGE_NAME" \
          --arg html "$(cat "$REPORT_FILE")" \
          --argjson book_id "$BOOK_ID" \
          --argjson chapter_id "$CHAPTER_ID_MAPS" \
          '{name: $name, book_id: $book_id, chapter_id: $chapter_id, html: $html}')

        curl -s -X POST "$BOOKSTACK_URL/pages" \
          -H "Authorization: Token $TOKEN_ID:$TOKEN_SECRET" \
          -H "Content-Type: application/json" \
          -d "$json_payload" >/dev/null

        echo "$REPORT_HASH" > "$HASH_FILE"

    # === CONTAINER CHANGED ===
    elif [[ "$REPORT_HASH" != "$(cat "$HASH_FILE")" ]]; then
        echo "๐Ÿ”„ Change: $HOSTNAME"
        any_changes=true
        echo "$REPORT_HASH" > "$HASH_FILE"
        CHANGE_LOG_HTML+="<li><strong>$CTID $HOSTNAME Ver $version</strong><pre><code>$(cat "$REPORT_FILE")</code></pre></li>"
    else
        echo "โœ… No change for $HOSTNAME"
    fi

    rm "$REPORT_FILE"
done

CHANGE_LOG_HTML+="</ul>"

# === UPLOAD CHANGE LOG IF NEEDED ===
if $any_changes; then
    echo "๐Ÿ“ค Uploading change log..."
    json_payload=$(jq -n \
      --arg name "DNA Change Log $dtg" \
      --arg html "$CHANGE_LOG_HTML" \
      --argjson book_id "$BOOK_ID" \
      --argjson chapter_id "$CHAPTER_ID_CHANGES" \
      '{name: $name, book_id: $book_id, chapter_id: $chapter_id, html: $html}')

    result=$(curl -s -X POST "$BOOKSTACK_URL/pages" \
      -H "Authorization: Token $TOKEN_ID:$TOKEN_SECRET" \
      -H "Content-Type: application/json" \
      -d "$json_payload")

    echo "๐Ÿ“ฌ BookStack response:"
    echo "$result"
else
    echo "๐Ÿ›‘ No container changes. Nothing uploaded."
fi