214 lines
5.3 KiB
Bash
214 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ============================================================
|
|
# Golden Image Builder for Ubuntu 24.x (Example: Docker + n8n)
|
|
# - Installs prerequisites, Docker, n8n
|
|
# - Cleans system for templating (cloud-init, machine-id, SSH keys, logs)
|
|
# - Powers off at the end
|
|
# ============================================================
|
|
|
|
APP="${APP:-n8n}"
|
|
TIMEZONE="${TIMEZONE:-America/New_York}"
|
|
|
|
# n8n defaults inside the image (customer-specific values can be overridden later)
|
|
N8N_HOST_DEFAULT="${N8N_HOST_DEFAULT:-localhost}"
|
|
N8N_PROTOCOL_DEFAULT="${N8N_PROTOCOL_DEFAULT:-http}"
|
|
WEBHOOK_URL_DEFAULT="${WEBHOOK_URL_DEFAULT:-http://localhost:5678/}"
|
|
|
|
LOG="/root/golden-image-${APP}.log"
|
|
|
|
require_root() {
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
echo "ERROR: Run as root. Example: sudo bash $0"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
log() {
|
|
echo "[$(date -Is)] $*" | tee -a "$LOG"
|
|
}
|
|
|
|
check_ubuntu() {
|
|
if [[ ! -f /etc/os-release ]]; then
|
|
log "ERROR: /etc/os-release not found."
|
|
exit 1
|
|
fi
|
|
. /etc/os-release
|
|
if [[ "${ID}" != "ubuntu" ]]; then
|
|
log "ERROR: This script is for Ubuntu. Detected: ${ID}"
|
|
exit 1
|
|
fi
|
|
if [[ "${VERSION_ID}" != 24.* ]]; then
|
|
log "WARN: Expected Ubuntu 24.x. Detected: ${VERSION_ID}. Proceeding anyway."
|
|
fi
|
|
log "OS: ${PRETTY_NAME}"
|
|
}
|
|
|
|
apt_base() {
|
|
log "Updating system and installing base packages..."
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -y
|
|
apt-get upgrade -y
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates curl gnupg lsb-release \
|
|
cloud-init qemu-guest-agent \
|
|
sudo vim-tiny unzip
|
|
systemctl enable --now qemu-guest-agent || true
|
|
}
|
|
|
|
install_docker() {
|
|
log "Installing Docker Engine + Compose plugin (official repo)..."
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
|
|
if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then
|
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
|
fi
|
|
|
|
. /etc/os-release
|
|
echo \
|
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
|
https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
|
|
apt-get update -y
|
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
systemctl enable --now docker
|
|
docker --version | tee -a "$LOG"
|
|
docker compose version | tee -a "$LOG"
|
|
}
|
|
|
|
setup_timezone() {
|
|
log "Setting timezone: ${TIMEZONE}"
|
|
timedatectl set-timezone "${TIMEZONE}" || true
|
|
}
|
|
|
|
install_n8n() {
|
|
log "Installing ${APP}..."
|
|
mkdir -p /opt/n8n
|
|
cat >/opt/n8n/docker-compose.yml <<'YAML'
|
|
services:
|
|
n8n:
|
|
image: n8nio/n8n:latest
|
|
restart: always
|
|
ports:
|
|
- "5678:5678"
|
|
environment:
|
|
- N8N_HOST=${N8N_HOST}
|
|
- N8N_PROTOCOL=${N8N_PROTOCOL}
|
|
- N8N_PORT=5678
|
|
- WEBHOOK_URL=${WEBHOOK_URL}
|
|
- TZ=${TZ}
|
|
volumes:
|
|
- n8n_data:/home/node/.n8n
|
|
volumes:
|
|
n8n_data:
|
|
YAML
|
|
|
|
cat >/opt/n8n/.env <<ENV
|
|
N8N_HOST=${N8N_HOST_DEFAULT}
|
|
N8N_PROTOCOL=${N8N_PROTOCOL_DEFAULT}
|
|
WEBHOOK_URL=${WEBHOOK_URL_DEFAULT}
|
|
TZ=${TIMEZONE}
|
|
ENV
|
|
|
|
(cd /opt/n8n && docker compose up -d)
|
|
docker ps | tee -a "$LOG"
|
|
}
|
|
|
|
# Optional: ensure SSH host keys are regenerated on first boot (after we delete them)
|
|
configure_ssh_regen() {
|
|
log "Ensuring SSH host keys will be generated on first boot..."
|
|
# Ubuntu normally generates keys if missing, but we make it explicit.
|
|
cat >/etc/systemd/system/ssh-hostkey-regen.service <<'UNIT'
|
|
[Unit]
|
|
Description=Regenerate SSH host keys if missing
|
|
After=network.target
|
|
Before=ssh.service
|
|
ConditionPathExists=!/etc/ssh/ssh_host_rsa_key
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/bin/ssh-keygen -A
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
systemctl daemon-reload
|
|
systemctl enable ssh-hostkey-regen.service || true
|
|
}
|
|
|
|
cleanup_for_template() {
|
|
log "CLEANUP: preparing VM for templating..."
|
|
|
|
log "Stopping services..."
|
|
systemctl stop docker || true
|
|
systemctl stop ssh || true
|
|
|
|
log "Cleaning cloud-init state + logs..."
|
|
cloud-init clean --logs || true
|
|
rm -f /etc/cloud/cloud.cfg.d/99-installer.cfg 2>/dev/null || true
|
|
|
|
log "Resetting machine-id..."
|
|
truncate -s 0 /etc/machine-id || true
|
|
rm -f /var/lib/dbus/machine-id || true
|
|
ln -sf /etc/machine-id /var/lib/dbus/machine-id || true
|
|
|
|
log "Removing SSH host keys..."
|
|
rm -f /etc/ssh/ssh_host_* || true
|
|
|
|
log "Clearing bash history..."
|
|
rm -f /root/.bash_history || true
|
|
for d in /home/*; do
|
|
[[ -d "$d" ]] && rm -f "$d/.bash_history" || true
|
|
done
|
|
|
|
log "Cleaning apt cache..."
|
|
apt-get autoremove -y || true
|
|
apt-get clean || true
|
|
rm -rf /var/lib/apt/lists/* || true
|
|
|
|
log "Vacuuming journald..."
|
|
journalctl --rotate || true
|
|
journalctl --vacuum-time=1s || true
|
|
|
|
log "Removing logs (keeping directories)..."
|
|
find /var/log -type f -exec truncate -s 0 {} \; || true
|
|
|
|
log "Removing temporary files..."
|
|
rm -rf /tmp/* /var/tmp/* || true
|
|
|
|
log "Sync..."
|
|
sync
|
|
}
|
|
|
|
poweroff_vm() {
|
|
log "DONE. Powering off now (convert VM to template in Proxmox)."
|
|
poweroff
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
: > "$LOG"
|
|
check_ubuntu
|
|
setup_timezone
|
|
apt_base
|
|
|
|
case "$APP" in
|
|
n8n)
|
|
install_docker
|
|
install_n8n
|
|
configure_ssh_regen
|
|
;;
|
|
*)
|
|
log "ERROR: Unsupported APP='${APP}'. Supported: n8n"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
cleanup_for_template
|
|
poweroff_vm
|
|
}
|
|
|
|
main "$@" |