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