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>$(pct exec $CTID -- df -h /)"
echo "<h3>🔒 Users with Shells</h3>$(pct exec $CTID -- getent passwd | grep '/bin/bash')"
echo "<h3>🔐 SSH Status</h3>$(pct exec $CTID -- systemctl is-active ssh 2>/dev/null || echo 'Not Running')"
echo "<h3>📥 Available Package Updates</h3>$(pct exec $CTID -- bash -c 'apt list --upgradable 2>/dev/null | grep -v \"Listing...\"' || echo 'N/A')"
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>$(cat "$REPORT_FILE")</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