#!/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 </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 "$@"