2026-02-01 05:42:05 +00:00
#!/bin/bash
#
# Build Archipelago Auto-Installer ISO (StartOS-like)
#
# This creates an ISO that automatically installs to the internal disk
# with minimal user interaction - similar to StartOS experience.
#
2026-02-03 21:43:33 +00:00
# CRITICAL: This script CAPTURES the LIVE SERVER state by default.
# Set DEV_SERVER to point to your development server.
#
# Usage:
# DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
# OR just: ./build-auto-installer-iso.sh (uses default server)
#
# To build from source instead:
# BUILD_FROM_SOURCE=1 ./build-auto-installer-iso.sh
#
2026-02-01 05:42:05 +00:00
# Features:
# - Pre-built root filesystem (no network needed during install)
# - Auto-detects internal disk (skips USB boot drive)
# - Automatic installation with progress display
# - Boots directly to web UI after install
#
2026-03-21 03:06:29 +00:00
# Image versions: sourced from scripts/image-versions.sh (single source of truth).
# All container image references MUST use the $*_IMAGE variables defined there.
#
# --- PLANNED REFACTOR (post-beta) ---
# This script is ~1870 lines and should be split into a modular library.
# Proposed structure:
# image-recipe/
# build-auto-installer-iso.sh — Main orchestrator (config, CLI args, step sequencing)
# lib/
# rootfs.sh — Step 1: Build root filesystem via Docker (~185 lines)
2026-03-27 18:19:49 +00:00
# installer-env.sh — Step 2: Build minimal installer via debootstrap (~80 lines)
2026-03-21 03:06:29 +00:00
# components.sh — Step 3: Add Archipelago components (binary, configs, web UI) (~120 lines)
# container-images.sh — Step 3b: Bundle container images for offline install (~330 lines)
# auto-install-script.sh — Step 4: Generate the embedded auto-install.sh (~615 lines)
# boot-config.sh — Step 5: Configure live boot auto-start + overlay squashfs (~215 lines)
# create-iso.sh — Step 6: Build final bootable ISO with xorriso/grub (~140 lines)
# Each lib/ script exports functions; main script sources them and calls in sequence.
# DO NOT split until tested on the build server — this is critical infrastructure.
# ---
#
2026-02-01 05:42:05 +00:00
set -e
2026-03-21 01:32:28 +00:00
# Source pinned image versions (single source of truth)
SCRIPT_DIR = " $( cd " $( dirname " $0 " ) " && pwd ) "
[ -f " $SCRIPT_DIR /../scripts/image-versions.sh " ] && . " $SCRIPT_DIR /../scripts/image-versions.sh "
2026-02-03 21:43:33 +00:00
# Configuration
DEV_SERVER = " ${ DEV_SERVER :- archipelago @192.168.1.228 } "
BUILD_FROM_SOURCE = " ${ BUILD_FROM_SOURCE :- 0 } "
2026-03-10 23:29:05 +00:00
UNBUNDLED = " ${ UNBUNDLED :- 0 } "
2026-03-12 00:19:30 +00:00
ARCH = " ${ ARCH :- x86_64 } "
2026-03-31 23:43:32 +01:00
# ── Sequential build numbering ─────────────────────────────────────────
# Increments on each build. Users see this in UI (Settings, sidebar).
# Counter persists in /opt/archipelago/build-counter (on build machine).
BUILD_COUNTER_FILE = "/opt/archipelago/build-counter"
if [ -f " $BUILD_COUNTER_FILE " ] ; then
BUILD_NUM = $(( $( cat " $BUILD_COUNTER_FILE " ) + 1 ))
else
BUILD_NUM = 1
fi
echo " $BUILD_NUM " | sudo tee " $BUILD_COUNTER_FILE " > /dev/null 2>/dev/null || BUILD_NUM = 1
GIT_SHORT = $( cd " $SCRIPT_DIR /.. " && git rev-parse --short HEAD 2>/dev/null || echo "dev" )
2026-04-02 08:54:39 +01:00
# Version format: major.minor.patch-prerelease (semver)
fix: BUILD_VERSION from Cargo.toml, kiosk scaling, new apps, Rust warnings
Critical:
- BUILD_VERSION was hardcoded as "1.3.0-alpha" — now reads from Cargo.toml
This caused ALL ISOs to show v1.3.0 regardless of actual binary version
Kiosk:
- Remove --disable-gpu flags (broke display scaling on some monitors)
- Add --start-fullscreen --window-size for reliable fullscreen
New apps:
- Nostr VPN, FIPS, Routstr, noStrudel, BotFights, NWNN, 484 Kitchen,
Call the Operator, Arch Presentation, Syntropy Institute, T-0
Rust: suppress dead_code and unused_assignments warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:35:52 +01:00
# Read version from Cargo.toml (single source of truth)
BUILD_VERSION = $( grep '^version' " $SCRIPT_DIR /../core/archipelago/Cargo.toml " 2>/dev/null | head -1 | sed 's/version = "//;s/"//' || echo "0.0.0" )
2026-03-31 23:43:32 +01:00
echo " Build # ${ BUILD_NUM } ( ${ BUILD_VERSION } , commit ${ GIT_SHORT } ) "
2026-03-12 00:19:30 +00:00
# Architecture-dependent variables
case " $ARCH " in
x86_64| amd64)
ARCH = "x86_64"
DEB_ARCH = "amd64"
LINUX_IMAGE_PKG = "linux-image-amd64"
GRUB_EFI_PKG = "grub-efi-amd64"
GRUB_EFI_SIGNED_PKG = "grub-efi-amd64-signed"
2026-03-21 01:11:05 +00:00
GRUB_PC_PKG = "grub-pc-bin"
2026-03-12 00:19:30 +00:00
GRUB_TARGET = "x86_64-efi"
2026-03-21 01:11:05 +00:00
GRUB_BIOS_TARGET = "i386-pc"
2026-03-12 00:19:30 +00:00
CONTAINER_PLATFORM = "linux/amd64"
LIB_DIR = " ${ LIB_DIR } "
; ;
arm64| aarch64)
ARCH = "arm64"
DEB_ARCH = "arm64"
LINUX_IMAGE_PKG = "linux-image-arm64"
GRUB_EFI_PKG = "grub-efi-arm64"
GRUB_EFI_SIGNED_PKG = "grub-efi-arm64-signed"
2026-03-21 01:11:05 +00:00
GRUB_PC_PKG = ""
2026-03-12 00:19:30 +00:00
GRUB_TARGET = "arm64-efi"
2026-03-21 01:11:05 +00:00
GRUB_BIOS_TARGET = ""
2026-03-12 00:19:30 +00:00
CONTAINER_PLATFORM = "linux/arm64"
LIB_DIR = "aarch64-linux-gnu"
; ;
*)
echo " ❌ Unsupported architecture: $ARCH (use x86_64 or arm64) "
exit 1
; ;
esac
2026-02-03 21:43:33 +00:00
2026-02-01 05:42:05 +00:00
SCRIPT_DIR = " $( cd " $( dirname " $0 " ) " && pwd ) "
WORK_DIR = " $SCRIPT_DIR /build/auto-installer "
OUTPUT_DIR = " $SCRIPT_DIR /results "
ROOTFS_DIR = " $WORK_DIR /rootfs "
INSTALLER_DIR = " $WORK_DIR /installer "
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago UNBUNDLED ISO (no pre-loaded apps) ║"
echo "╚════════════════════════════════════════════════════════════════╝"
else
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Building Archipelago Auto-Installer ISO (StartOS-like) ║"
echo "╚════════════════════════════════════════════════════════════════╝"
fi
2026-02-01 05:42:05 +00:00
echo ""
2026-02-03 21:43:33 +00:00
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo "📦 Mode: Building from SOURCE CODE"
2026-03-10 23:29:05 +00:00
elif [ " $UNBUNDLED " = "1" ] ; then
echo "📦 Mode: UNBUNDLED (apps downloaded on-demand from Marketplace)"
echo " Server: $DEV_SERVER (backend + web UI only) "
2026-02-03 21:43:33 +00:00
else
echo "📦 Mode: Capturing LIVE SERVER state"
echo " Server: $DEV_SERVER "
fi
2026-03-12 00:19:30 +00:00
echo " 🏗️ Architecture: $ARCH ( $DEB_ARCH ) "
2026-02-03 21:43:33 +00:00
echo ""
2026-02-01 05:42:05 +00:00
# Check for required tools
check_tools( ) {
local missing = ""
2026-02-14 16:44:20 +00:00
local can_install = false
# Check if we can auto-install (running as root on Debian/Ubuntu)
if [ " $EUID " -eq 0 ] && [ -f /etc/debian_version ] ; then
can_install = true
fi
2026-02-01 18:46:35 +00:00
# Check for docker or podman
if command -v docker >/dev/null 2>& 1; then
CONTAINER_CMD = "docker"
elif command -v podman >/dev/null 2>& 1; then
CONTAINER_CMD = "podman"
else
missing = " $missing docker-or-podman "
fi
2026-03-27 18:19:49 +00:00
for tool in xorriso mksquashfs; do
if ! command -v $tool >/dev/null 2>& 1; then
missing = " $missing $tool "
fi
done
# Check for isolinux MBR (needed for hybrid USB boot)
if [ ! -f /usr/lib/ISOLINUX/isohdpfx.bin ] && [ ! -f /usr/share/syslinux/isohdpfx.bin ] ; then
missing = " $missing isolinux "
2026-02-17 15:03:34 +00:00
fi
2026-03-27 18:19:49 +00:00
2026-02-01 05:42:05 +00:00
if [ -n " $missing " ] ; then
2026-03-27 18:19:49 +00:00
echo " Missing required tools: $missing "
2026-02-14 16:44:20 +00:00
if [ " $can_install " = true ] ; then
2026-03-27 18:19:49 +00:00
echo " Auto-installing missing dependencies..."
2026-02-14 16:44:20 +00:00
apt-get update -qq
2026-03-27 18:19:49 +00:00
2026-02-14 16:44:20 +00:00
if [ [ " $missing " = = *"xorriso" * ] ] ; then
apt-get install -y xorriso
fi
2026-03-27 18:19:49 +00:00
if [ [ " $missing " = = *"mksquashfs" * ] ] ; then
apt-get install -y squashfs-tools
2026-02-17 15:03:34 +00:00
fi
2026-03-27 18:19:49 +00:00
if [ [ " $missing " = = *"isolinux" * ] ] ; then
apt-get install -y isolinux syslinux-common
fi
2026-02-14 16:44:20 +00:00
if [ [ " $missing " = = *"docker-or-podman" * ] ] ; then
echo " Installing podman..."
apt-get install -y podman
CONTAINER_CMD = "podman"
fi
2026-03-27 18:19:49 +00:00
echo " Dependencies installed successfully!"
2026-02-14 16:44:20 +00:00
else
2026-03-27 18:19:49 +00:00
echo " Install with: sudo apt install xorriso squashfs-tools isolinux podman"
2026-02-14 16:44:20 +00:00
echo " Or run this script with sudo to auto-install"
exit 1
fi
fi
# Re-check after potential installation
if command -v docker >/dev/null 2>& 1; then
CONTAINER_CMD = "docker"
elif command -v podman >/dev/null 2>& 1; then
CONTAINER_CMD = "podman"
else
echo "❌ Container runtime still not available after installation"
2026-02-01 05:42:05 +00:00
exit 1
fi
2026-02-01 18:46:35 +00:00
echo " Using container runtime: $CONTAINER_CMD "
2026-03-27 11:12:31 +00:00
2026-03-28 21:01:10 +00:00
# Fix root podman D-Bus issue (sd-bus: Transport endpoint is not connected)
# When running as sudo, systemd cgroup manager can't reach the user D-Bus session.
if [ " $CONTAINER_CMD " = "podman" ] && [ " $( id -u) " = "0" ] ; then
if ! $CONTAINER_CMD run --rm debian:bookworm true 2>/dev/null; then
echo " Root podman D-Bus issue detected, using cgroupfs manager"
CONTAINER_CMD = "podman --cgroup-manager=cgroupfs"
fi
fi
2026-03-27 11:12:31 +00:00
# Ensure insecure registry config for Archipelago app registry (HTTP)
if [ " $CONTAINER_CMD " = "podman" ] ; then
mkdir -p /etc/containers/registries.conf.d
cat > /etc/containers/registries.conf.d/archipelago.conf <<'REGCONF'
[ [ registry] ]
location = "80.71.235.15:3000"
insecure = true
REGCONF
fi
2026-02-01 05:42:05 +00:00
}
check_tools
mkdir -p " $WORK_DIR "
mkdir -p " $OUTPUT_DIR "
# =============================================================================
# STEP 1: Build complete root filesystem using Docker
# =============================================================================
echo "📦 Step 1: Building root filesystem..."
ROOTFS_TAR = " $WORK_DIR /archipelago-rootfs.tar "
if [ ! -f " $ROOTFS_TAR " ] || [ " $1 " = = "--rebuild" ] ; then
echo " Using Docker to create Debian root filesystem..."
# Create a Dockerfile for building the rootfs
2026-03-12 00:19:30 +00:00
cat > " $WORK_DIR /Dockerfile.rootfs " <<DOCKERFILE
2026-02-01 05:42:05 +00:00
FROM debian:bookworm
ENV DEBIAN_FRONTEND = noninteractive
2026-03-26 19:50:59 +00:00
# Preseed keyboard/console config to prevent console-setup.service failure
RUN echo "keyboard-configuration keyboard-configuration/layoutcode string us" | debconf-set-selections && \
echo "keyboard-configuration keyboard-configuration/model select Generic 105-key PC" | debconf-set-selections && \
echo "console-setup console-setup/charmap47 select UTF-8" | debconf-set-selections && \
2026-03-30 23:47:15 +01:00
echo "console-setup console-setup/codeset47 select Uni2" | debconf-set-selections && \
echo "console-setup console-setup/fontface47 select Terminus" | debconf-set-selections && \
echo "console-setup console-setup/fontsize-fb47 select 16" | debconf-set-selections
2026-03-26 19:50:59 +00:00
2026-03-25 21:21:27 +00:00
# Enable non-free-firmware repo — replace DEB822 sources with traditional format
# (DEB822 sed was silently failing, so just overwrite with known-good sources.list)
RUN echo "deb http://deb.debian.org/debian bookworm main non-free-firmware" > /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian bookworm-updates main non-free-firmware" >> /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian-security bookworm-security main non-free-firmware" >> /etc/apt/sources.list && \
rm -f /etc/apt/sources.list.d/debian.sources
2026-03-25 16:56:02 +00:00
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Install all packages we need including nginx, podman, tor, and openssl (for self-signed certs)
2026-03-27 18:19:49 +00:00
RUN apt-get update && apt-get install -y --no-install-recommends \
2026-03-12 00:19:30 +00:00
${ LINUX_IMAGE_PKG } \
${ GRUB_EFI_PKG } \
${ GRUB_EFI_SIGNED_PKG } \
2026-03-26 09:12:16 +00:00
${ GRUB_PC_PKG } \
2026-02-01 05:42:05 +00:00
systemd \
systemd-sysv \
dbus \
sudo \
network-manager \
openssh-server \
nginx \
podman \
2026-03-28 02:40:39 +00:00
uidmap \
slirp4netns \
fuse-overlayfs \
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
tor \
2026-03-31 04:52:30 +01:00
python3 \
2026-02-01 05:42:05 +00:00
curl \
2026-03-25 15:52:26 +00:00
git \
2026-02-01 05:42:05 +00:00
vim-tiny \
ca-certificates \
2026-03-05 08:34:53 +00:00
openssl \
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
chrony \
2026-02-01 05:42:05 +00:00
locales \
console-setup \
keyboard-configuration \
2026-03-26 09:12:16 +00:00
cryptsetup \
2026-03-25 16:56:02 +00:00
firmware-realtek \
firmware-iwlwifi \
2026-03-28 13:06:34 +00:00
firmware-misc-nonfree \
2026-04-02 20:28:53 +01:00
firmware-linux-nonfree \
2026-03-25 18:25:01 +00:00
intel-microcode \
amd64-microcode \
2026-03-26 09:12:16 +00:00
xorg \
2026-04-01 22:31:45 +01:00
xdotool \
2026-03-26 09:12:16 +00:00
chromium \
unclutter \
2026-03-27 18:19:49 +00:00
fonts-liberation \
xfonts-base \
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
plymouth \
plymouth-themes \
2026-03-28 23:41:40 +00:00
zstd \
2026-04-02 16:15:04 +01:00
socat \
2026-03-31 04:52:30 +01:00
python3 \
2026-04-02 10:34:58 +01:00
apache2-utils \
2026-04-08 13:15:09 +02:00
wireguard-tools \
acpid \
acpi-support-base \
2026-02-01 05:42:05 +00:00
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
2026-03-27 18:19:49 +00:00
# Strip docs, man pages, and unused locales
RUN find /usr/share/doc -depth -type f ! -name copyright -delete 2>/dev/null || true && \
find /usr/share/doc -empty -delete 2>/dev/null || true && \
rm -rf /usr/share/man /usr/share/info /usr/share/lintian /usr/share/linda && \
find /usr/share/locale -maxdepth 1 -mindepth 1 ! -name 'en_US' ! -name 'locale.alias' -exec rm -rf { } + 2>/dev/null || true
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Install Tailscale from official repo
RUN curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null && \
curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list && \
2026-03-27 18:19:49 +00:00
apt-get update && apt-get install -y --no-install-recommends tailscale && \
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
apt-get clean && rm -rf /var/lib/apt/lists/*
2026-02-01 05:42:05 +00:00
# Configure locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
2026-03-10 23:29:05 +00:00
# Create archipelago user with password "archipelago"
2026-02-01 05:42:05 +00:00
RUN useradd -m -s /bin/bash -G sudo archipelago && \
echo "archipelago:archipelago" | chpasswd && \
2026-03-10 23:29:05 +00:00
echo "root:archipelago" | chpasswd && \
2026-02-01 05:42:05 +00:00
echo "archipelago ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/archipelago
2026-03-10 23:29:05 +00:00
# Verify password hash was set (not locked)
RUN grep -q "^archipelago:\$" /etc/shadow && echo "Password set OK" || echo "WARNING: password may not be set"
2026-02-01 05:42:05 +00:00
# Set hostname
RUN echo "archipelago" > /etc/hostname
# Configure SSH
RUN mkdir -p /etc/ssh && \
sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config || true && \
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config || true
# Configure nginx for Archipelago
RUN rm -f /etc/nginx/sites-enabled/default
COPY nginx-archipelago.conf /etc/nginx/sites-available/archipelago
RUN ln -sf /etc/nginx/sites-available/archipelago /etc/nginx/sites-enabled/archipelago
2026-03-05 08:34:53 +00:00
# Install nginx snippets (PWA config, HTTPS app proxies)
COPY snippets/ /etc/nginx/snippets/
# Generate self-signed SSL certificate for HTTPS (PWA install requires secure context)
RUN mkdir -p /etc/archipelago/ssl && \
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/archipelago/ssl/archipelago.key \
-out /etc/archipelago/ssl/archipelago.crt \
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" && \
chmod 600 /etc/archipelago/ssl/archipelago.key
2026-02-01 05:42:05 +00:00
# Create archipelago systemd service
COPY archipelago.service /etc/systemd/system/archipelago.service
2026-03-25 15:52:26 +00:00
COPY archipelago-update.service /etc/systemd/system/archipelago-update.service
COPY archipelago-update.timer /etc/systemd/system/archipelago-update.timer
2026-03-28 11:31:48 +00:00
COPY archipelago-doctor.service /etc/systemd/system/archipelago-doctor.service
COPY archipelago-doctor.timer /etc/systemd/system/archipelago-doctor.timer
COPY archipelago-reconcile.service /etc/systemd/system/archipelago-reconcile.service
COPY archipelago-reconcile.timer /etc/systemd/system/archipelago-reconcile.timer
2026-03-29 19:26:21 +01:00
COPY archipelago-tor-helper.service /etc/systemd/system/archipelago-tor-helper.service
COPY archipelago-tor-helper.path /etc/systemd/system/archipelago-tor-helper.path
2026-04-07 14:40:33 +01:00
COPY nostr-vpn.service /etc/systemd/system/nostr-vpn.service
2026-04-08 13:15:09 +02:00
COPY archipelago-wg-address.service /etc/systemd/system/archipelago-wg-address.service
2026-04-08 15:06:27 +02:00
COPY nostr-relay.service /etc/systemd/system/nostr-relay.service
COPY nostr-relay-config.toml /etc/archipelago/nostr-relay-config.toml
2026-04-08 13:15:09 +02:00
# WireGuard kernel module auto-load on boot
RUN echo "wireguard" >> /etc/modules-load.d/wireguard.conf
2026-02-01 05:42:05 +00:00
2026-03-28 23:41:40 +00:00
# Copy container doctor + reconcile scripts (referenced by the services above)
2026-03-30 13:35:02 +01:00
RUN mkdir -p /home/archipelago/archy/scripts/lib
2026-03-28 23:41:40 +00:00
COPY container-doctor.sh /home/archipelago/archy/scripts/container-doctor.sh
COPY reconcile-containers.sh /home/archipelago/archy/scripts/reconcile-containers.sh
2026-03-30 13:35:02 +01:00
COPY container-specs.sh /home/archipelago/archy/scripts/container-specs.sh
2026-03-29 19:26:21 +01:00
COPY tor-helper.sh /opt/archipelago/scripts/tor-helper.sh
2026-03-30 13:35:02 +01:00
COPY lib/ /home/archipelago/archy/scripts/lib/
RUN chmod +x /home/archipelago/archy/scripts/*.sh /home/archipelago/archy/scripts/lib/*.sh /opt/archipelago/scripts/*.sh && \
2026-03-28 23:41:40 +00:00
chown -R archipelago:archipelago /home/archipelago/archy
2026-02-01 05:42:05 +00:00
# Enable services
RUN systemctl enable NetworkManager || true && \
systemctl enable ssh || true && \
systemctl enable nginx || true && \
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
systemctl enable archipelago || true && \
systemctl enable tor || true && \
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
systemctl enable tailscaled || true && \
2026-03-25 15:52:26 +00:00
systemctl enable chrony || true && \
2026-03-28 11:31:48 +00:00
systemctl enable archipelago-update.timer || true && \
systemctl enable archipelago-doctor.timer || true && \
2026-03-29 19:26:21 +01:00
systemctl enable archipelago-reconcile.timer || true && \
2026-04-08 13:15:09 +02:00
systemctl enable archipelago-tor-helper.path || true && \
2026-04-08 15:06:27 +02:00
systemctl enable nostr-relay || true && \
2026-04-08 13:15:09 +02:00
systemctl enable nostr-vpn || true && \
systemctl enable archipelago-wg-address || true
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
# Remove policy-rc.d so services can start on first boot
RUN rm -f /usr/sbin/policy-rc.d
2026-02-01 05:42:05 +00:00
2026-03-05 08:34:53 +00:00
# Create directories (including Cloud storage for FileBrowser)
2026-04-08 15:06:27 +02:00
RUN mkdir -p /var/lib/archipelago/{ data,config,containers,nostr-relay,nostr-vpn} && \
2026-02-01 05:42:05 +00:00
mkdir -p /etc/archipelago && \
mkdir -p /opt/archipelago/{ bin,scripts,web-ui} && \
2026-03-05 08:34:53 +00:00
mkdir -p /var/lib/archipelago/data/cloud/{ Documents,Photos,Music,Videos,Downloads} && \
2026-04-08 15:06:27 +02:00
cp /etc/archipelago/nostr-relay-config.toml /var/lib/archipelago/nostr-relay/config.toml && \
2026-02-01 05:42:05 +00:00
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
# Clean up
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
DOCKERFILE
2026-03-05 08:34:53 +00:00
# Copy nginx snippets for HTTPS (PWA, app proxies)
if [ -d " $SCRIPT_DIR /configs/snippets " ] ; then
mkdir -p " $WORK_DIR /snippets "
cp " $SCRIPT_DIR /configs/snippets/ " *.conf " $WORK_DIR /snippets/ " 2>/dev/null || true
echo " Using nginx snippets from configs/snippets/"
else
mkdir -p " $WORK_DIR /snippets "
echo " ⚠ No nginx snippets found, HTTPS features may not work"
fi
2026-03-22 03:30:21 +00:00
# Use nginx config from configs/ (includes app proxies for Nextcloud, Vaultwarden, etc.)
2026-02-25 18:20:50 +00:00
if [ -f " $SCRIPT_DIR /configs/nginx-archipelago.conf " ] ; then
cp " $SCRIPT_DIR /configs/nginx-archipelago.conf " " $WORK_DIR /nginx-archipelago.conf "
echo " Using nginx config from configs/nginx-archipelago.conf"
else
echo " ⚠ configs/nginx-archipelago.conf not found, using minimal config"
cat > " $WORK_DIR /nginx-archipelago.conf " <<'NGIN XCONF'
2026-02-01 05:42:05 +00:00
server {
listen 80;
server_name _;
root /opt/archipelago/web-ui;
index index.html;
2026-02-25 18:20:50 +00:00
location / { try_files $uri $uri / /index.html; }
location /archipelago/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; }
location /rpc/ { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; }
location /ws { proxy_pass http://127.0.0.1:5678; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade ; proxy_set_header Connection "upgrade" ; proxy_set_header Host $host ; proxy_read_timeout 86400s; }
2026-02-01 05:42:05 +00:00
}
NGINXCONF
2026-02-25 18:20:50 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-03-18 10:50:13 +00:00
# Copy udev rule for mesh radio stable naming
if [ -f " $SCRIPT_DIR /configs/99-mesh-radio.rules " ] ; then
cp " $SCRIPT_DIR /configs/99-mesh-radio.rules " " $WORK_DIR /99-mesh-radio.rules "
echo " Using 99-mesh-radio.rules from configs/"
fi
2026-03-25 15:52:26 +00:00
# Copy update service and timer
if [ -f " $SCRIPT_DIR /configs/archipelago-update.service " ] ; then
cp " $SCRIPT_DIR /configs/archipelago-update.service " " $WORK_DIR /archipelago-update.service "
cp " $SCRIPT_DIR /configs/archipelago-update.timer " " $WORK_DIR /archipelago-update.timer "
echo " Using archipelago-update.service + timer from configs/"
fi
2026-03-28 23:41:40 +00:00
# Copy container doctor and reconciliation timers + scripts
2026-03-28 11:31:48 +00:00
if [ -f " $SCRIPT_DIR /configs/archipelago-doctor.service " ] ; then
cp " $SCRIPT_DIR /configs/archipelago-doctor.service " " $WORK_DIR /archipelago-doctor.service "
cp " $SCRIPT_DIR /configs/archipelago-doctor.timer " " $WORK_DIR /archipelago-doctor.timer "
cp " $SCRIPT_DIR /configs/archipelago-reconcile.service " " $WORK_DIR /archipelago-reconcile.service "
cp " $SCRIPT_DIR /configs/archipelago-reconcile.timer " " $WORK_DIR /archipelago-reconcile.timer "
2026-03-28 23:41:40 +00:00
# Copy the actual scripts the services reference
2026-03-30 13:35:02 +01:00
for s in container-doctor.sh reconcile-containers.sh container-specs.sh tor-helper.sh; do
2026-03-28 23:41:40 +00:00
if [ -f " $SCRIPT_DIR /../scripts/ $s " ] ; then
cp " $SCRIPT_DIR /../scripts/ $s " " $WORK_DIR / $s "
fi
done
2026-03-30 13:35:02 +01:00
# Copy shared script library (mem_limit etc.)
if [ -d " $SCRIPT_DIR /../scripts/lib " ] ; then
mkdir -p " $WORK_DIR /lib "
cp " $SCRIPT_DIR /../scripts/lib/ " *.sh " $WORK_DIR /lib/ " 2>/dev/null || true
fi
2026-03-28 11:31:48 +00:00
echo " Using container doctor + reconcile timers from configs/"
fi
2026-03-29 19:26:21 +01:00
# Copy Tor helper path-activated service (allows backend to manage Tor as non-root)
if [ -f " $SCRIPT_DIR /configs/archipelago-tor-helper.service " ] ; then
cp " $SCRIPT_DIR /configs/archipelago-tor-helper.service " " $WORK_DIR /archipelago-tor-helper.service "
cp " $SCRIPT_DIR /configs/archipelago-tor-helper.path " " $WORK_DIR /archipelago-tor-helper.path "
echo " Using tor-helper path unit from configs/"
fi
2026-04-07 14:40:33 +01:00
# Copy NostrVPN system service (native mesh VPN, not a container)
if [ -f " $SCRIPT_DIR /configs/nostr-vpn.service " ] ; then
cp " $SCRIPT_DIR /configs/nostr-vpn.service " " $WORK_DIR /nostr-vpn.service "
echo " Using nostr-vpn.service from configs/"
fi
2026-04-08 13:15:09 +02:00
if [ -f " $SCRIPT_DIR /configs/archipelago-wg-address.service " ] ; then
cp " $SCRIPT_DIR /configs/archipelago-wg-address.service " " $WORK_DIR /archipelago-wg-address.service "
echo " Using archipelago-wg-address.service from configs/"
fi
2026-04-08 15:06:27 +02:00
# Copy private Nostr relay service (native, for NostrVPN signaling)
if [ -f " $SCRIPT_DIR /configs/nostr-relay.service " ] ; then
cp " $SCRIPT_DIR /configs/nostr-relay.service " " $WORK_DIR /nostr-relay.service "
echo " Using nostr-relay.service from configs/"
fi
if [ -f " $SCRIPT_DIR /configs/nostr-relay-config.toml " ] ; then
cp " $SCRIPT_DIR /configs/nostr-relay-config.toml " " $WORK_DIR /nostr-relay-config.toml "
echo " Using nostr-relay-config.toml from configs/"
fi
2026-04-08 13:15:09 +02:00
# Copy WireGuard helper script (privileged peer management)
if [ -f " $SCRIPT_DIR /../scripts/archipelago-wg " ] ; then
cp " $SCRIPT_DIR /../scripts/archipelago-wg " " $WORK_DIR /archipelago-wg "
echo " Using archipelago-wg helper from scripts/"
fi
2026-02-25 18:20:50 +00:00
# Use archipelago.service from configs/ (User=root for Podman container access)
if [ -f " $SCRIPT_DIR /configs/archipelago.service " ] ; then
cp " $SCRIPT_DIR /configs/archipelago.service " " $WORK_DIR /archipelago.service "
echo " Using archipelago.service from configs/"
else
cat > " $WORK_DIR /archipelago.service " <<'SYS TEMDSERVICE'
2026-02-01 05:42:05 +00:00
[ Unit]
Description = Archipelago Backend
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
After = network-online.target archipelago-setup-tor.service
2026-02-01 05:42:05 +00:00
Wants = network-online.target
[ Service]
Type = simple
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
User = archipelago
2026-03-19 12:55:31 +00:00
Environment = "ARCHIPELAGO_BIND=127.0.0.1:5678"
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
Environment = "XDG_RUNTIME_DIR=/run/user/1000"
ExecStartPre = /bin/bash -c 'mkdir -p /run/user/1000 && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
2026-02-01 05:42:05 +00:00
ExecStart = /usr/local/bin/archipelago
Restart = on-failure
RestartSec = 5
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
ProtectHome = no
2026-02-01 05:42:05 +00:00
[ Install]
WantedBy = multi-user.target
SYSTEMDSERVICE
2026-02-25 18:20:50 +00:00
fi
2026-02-01 05:42:05 +00:00
2026-02-01 18:46:35 +00:00
echo " Building $CONTAINER_CMD image (this may take a few minutes)... "
2026-03-25 21:31:51 +00:00
$CONTAINER_CMD build --no-cache --platform $CONTAINER_PLATFORM -t archipelago-rootfs -f " $WORK_DIR /Dockerfile.rootfs " " $WORK_DIR "
2026-02-01 05:42:05 +00:00
echo " Exporting filesystem..."
2026-04-02 15:16:24 +01:00
$CONTAINER_CMD rm -f archipelago-rootfs-tmp 2>/dev/null || true
2026-03-12 00:19:30 +00:00
$CONTAINER_CMD create --platform $CONTAINER_PLATFORM --name archipelago-rootfs-tmp archipelago-rootfs
2026-02-01 18:46:35 +00:00
$CONTAINER_CMD export archipelago-rootfs-tmp > " $ROOTFS_TAR "
$CONTAINER_CMD rm archipelago-rootfs-tmp
2026-02-01 05:42:05 +00:00
echo " ✅ Root filesystem created: $( du -h " $ROOTFS_TAR " | cut -f1) "
else
echo " ✅ Using cached root filesystem: $( du -h " $ROOTFS_TAR " | cut -f1) "
fi
# =============================================================================
2026-03-27 18:19:49 +00:00
# STEP 2: Build minimal installer environment (replaces Debian Live)
2026-02-01 05:42:05 +00:00
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 2: Building minimal installer environment via debootstrap..."
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
INSTALLER_ISO = " $WORK_DIR /installer-iso "
INSTALLER_SQUASHFS = " $WORK_DIR /installer-squashfs "
rm -rf " $INSTALLER_ISO " " $INSTALLER_SQUASHFS "
mkdir -p " $INSTALLER_ISO /live " " $INSTALLER_ISO /archipelago "
mkdir -p " $INSTALLER_ISO /boot/grub " " $INSTALLER_ISO /isolinux "
mkdir -p " $INSTALLER_ISO /EFI/BOOT "
# Build the installer filesystem inside a container
# This creates: vmlinuz, initrd.img, filesystem.squashfs
echo " Building installer rootfs with debootstrap (this takes a few minutes)..."
$CONTAINER_CMD run --rm --privileged --platform $CONTAINER_PLATFORM \
-v " $WORK_DIR :/output " \
-e DEB_ARCH = " $DEB_ARCH " \
-e LIB_DIR = " $LIB_DIR " \
debian:bookworm bash -c '
set -e
2026-02-14 16:44:20 +00:00
2026-03-27 18:19:49 +00:00
apt-get update -qq
apt-get install -y -qq debootstrap squashfs-tools initramfs-tools dosfstools mtools \
grub-efi-amd64-bin grub-pc-bin grub-common isolinux syslinux-common
echo " [container] Running debootstrap --variant=minbase..."
debootstrap --variant= minbase --arch= ${ DEB_ARCH } \
--include= systemd,systemd-sysv,udev,dbus,bash,coreutils,mount,util-linux,\
kmod,procps,iproute2,ca-certificates,gdisk,\
cryptsetup,cryptsetup-initramfs,parted,dosfstools,e2fsprogs,\
linux-image-${ DEB_ARCH } ,grub-efi-${ DEB_ARCH } ,grub-pc-bin,\
pciutils,usbutils,less,nano \
bookworm /installer http://deb.debian.org/debian
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Install live-boot via chroot — debootstrap minbase resolver cannot handle it.
# The chroot approach works (confirmed in CI run 90) — just needs proc/sys/dev mounts.
2026-03-27 19:11:54 +00:00
echo " [container] Installing live-boot for squashfs root support..."
2026-03-27 19:45:52 +00:00
cp /etc/resolv.conf /installer/etc/resolv.conf 2>/dev/null || true
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
chroot /installer apt-get update -qq
chroot /installer apt-get install -y --no-install-recommends live-boot live-boot-initramfs-tools
chroot /installer apt-get clean
umount /installer/dev 2>/dev/null || true
umount /installer/sys 2>/dev/null || true
umount /installer/proc 2>/dev/null || true
2026-03-27 19:45:52 +00:00
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Verify live-boot hooks are in place (scripts/live is a FILE not a directory)
2026-03-27 20:36:39 +00:00
if [ -e /installer/usr/share/initramfs-tools/scripts/live ] ; then
2026-03-27 19:11:54 +00:00
echo " [container] live-boot initramfs hooks: OK"
else
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
echo " [container] FATAL: live-boot hooks not found after install!"
ls -la /installer/usr/share/initramfs-tools/scripts/ 2>/dev/null
exit 1
2026-03-27 19:11:54 +00:00
fi
2026-03-27 18:19:49 +00:00
echo " [container] Configuring installer environment..."
# Set hostname
echo "archipelago-installer" > /installer/etc/hostname
# Set root password
echo "root:archipelago" | chroot /installer chpasswd
# Auto-login on tty1
mkdir -p /installer/etc/systemd/system/getty@tty1.service.d
cat > /installer/etc/systemd/system/getty@tty1.service.d/autologin.conf <<GETTY
[ Service]
ExecStart =
ExecStart = -/sbin/agetty --autologin root --noclear %I \$ TERM
GETTY
2026-03-28 00:35:34 +00:00
# Auto-start installer via profile.d (runs after auto-login, no getty race)
# This is the same approach the working Debian Live build used.
mkdir -p /installer/etc/profile.d
2026-03-28 18:44:36 +00:00
cat > /installer/etc/profile.d/z99-archipelago-installer.sh <<PROFILE
2026-03-27 18:19:49 +00:00
#!/bin/bash
2026-03-28 00:35:34 +00:00
# Auto-start Archipelago installer on login — only run once
2026-03-28 18:44:36 +00:00
if [ -n "\$INSTALLER_STARTED" ] ; then
2026-03-28 00:35:34 +00:00
return 0 2>/dev/null || exit 0
fi
export INSTALLER_STARTED = 1
sleep 1
2026-03-27 18:19:49 +00:00
clear
echo ""
2026-03-28 23:58:42 +00:00
echo -e "\033[38;5;208m ▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█\033[0m"
echo -e "\033[38;5;208m █▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █\033[0m"
echo -e "\033[38;5;208m ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀\033[0m"
echo -e " \033[38;5;130mbitcoin node os\033[0m"
2026-03-27 18:19:49 +00:00
echo ""
BOOT_MEDIA = ""
2026-03-27 19:08:00 +00:00
for dev in /run/live/medium /lib/live/mount/medium /run/archiso /cdrom /media/cdrom /mnt/iso; do
2026-03-28 18:44:36 +00:00
if [ -f "\$dev/archipelago/auto-install.sh" ] ; then
BOOT_MEDIA = "\$dev"
2026-03-27 18:19:49 +00:00
break
2026-02-14 16:44:20 +00:00
fi
2026-03-27 18:19:49 +00:00
done
2026-03-28 18:44:36 +00:00
# If standard mount points failed, actively find and mount the boot device
if [ -z "\$BOOT_MEDIA" ] ; then
echo -e " \033[37mSearching for boot device...\033[0m"
mkdir -p /run/archiso 2>/dev/null
for blk in /dev/sr0 /dev/sd[ a-z] /dev/sd[ a-z] [ 0-9] /dev/nvme[ 0-9] n[ 0-9] p[ 0-9] ; do
[ -b "\$blk" ] || continue
mount -o ro "\$blk" /run/archiso 2>/dev/null || continue
if [ -f /run/archiso/archipelago/auto-install.sh ] ; then
BOOT_MEDIA = "/run/archiso"
break
fi
umount /run/archiso 2>/dev/null
done
fi
if [ -n "\$BOOT_MEDIA" ] ; then
echo -e " \033[37mFound installer at: \$BOOT_MEDIA\033[0m"
2026-03-27 18:19:49 +00:00
echo ""
2026-03-28 15:15:42 +00:00
echo -e " Press Enter to install | \033[1;37mCtrl+C\033[0m for shell"
2026-03-28 11:31:48 +00:00
read -s
2026-03-28 18:44:36 +00:00
bash "\$BOOT_MEDIA/archipelago/auto-install.sh"
2026-03-27 18:19:49 +00:00
else
2026-03-28 15:15:42 +00:00
echo -e " \033[37mInstaller not found on boot media.\033[0m"
2026-03-28 00:35:34 +00:00
echo ""
2026-03-28 18:44:36 +00:00
echo -e " \033[37mDebug info:\033[0m"
ls -la /run/live/ 2>/dev/null || echo " /run/live/ does not exist"
mount | grep -E "iso9660|squashfs|overlay" 2>/dev/null
echo ""
echo -e " \033[37mTry: mount /dev/sdX /mnt/iso && bash /mnt/iso/archipelago/auto-install.sh\033[0m"
echo ""
2026-02-14 16:44:20 +00:00
fi
2026-03-28 00:35:34 +00:00
PROFILE
chmod +x /installer/etc/profile.d/z99-archipelago-installer.sh
2026-02-14 16:44:20 +00:00
2026-03-27 18:19:49 +00:00
# Custom initramfs hook: mount ISO boot media at /run/archiso
mkdir -p /installer/etc/initramfs-tools/hooks
cat > /installer/etc/initramfs-tools/hooks/archipelago <<HOOK
#!/bin/sh
set -e
PREREQ = ""
prereqs( ) { echo "\$PREREQ" ; }
case "\$1" in prereqs) prereqs; exit 0; ; esac
. /usr/share/initramfs-tools/hook-functions
# Ensure mount helpers and filesystem tools are in initramfs
copy_exec /bin/mount
copy_exec /bin/umount
copy_exec /bin/findfs 2>/dev/null || true
copy_exec /sbin/blkid
manual_add_modules iso9660 vfat squashfs overlay
HOOK
chmod +x /installer/etc/initramfs-tools/hooks/archipelago
mkdir -p /installer/etc/initramfs-tools/scripts/local-bottom
cat > /installer/etc/initramfs-tools/scripts/local-bottom/archipelago-mount <<INI TSCRIPT
#!/bin/sh
PREREQ = ""
prereqs( ) { echo "\$PREREQ" ; }
case "\$1" in prereqs) prereqs; exit 0; ; esac
. /scripts/functions
# Try to find and mount the Archipelago boot media
mkdir -p /run/archiso
log_begin_msg "Searching for Archipelago boot media..."
# Try CD-ROM first, then USB partitions
for dev in /dev/sr0 /dev/sd??* /dev/nvme*p*; do
[ -b "\$dev" ] 2>/dev/null || continue
mount -o ro "\$dev" /run/archiso 2>/dev/null || continue
if [ -d /run/archiso/archipelago ] ; then
log_end_msg 0
echo "Found Archipelago media on \$dev"
exit 0
2026-02-14 16:44:20 +00:00
fi
2026-03-27 18:19:49 +00:00
umount /run/archiso 2>/dev/null || true
done
log_end_msg 1
echo "Archipelago boot media not found (will retry from userspace)"
INITSCRIPT
chmod +x /installer/etc/initramfs-tools/scripts/local-bottom/archipelago-mount
# Strip docs and man pages from installer
rm -rf /installer/usr/share/man/* /installer/usr/share/doc/*
rm -rf /installer/var/lib/apt/lists/* /installer/var/cache/apt/*
# Extract kernel
KVER = $( ls /installer/lib/modules/ | sort -V | tail -1)
echo " [container] Kernel version: $KVER "
cp /installer/boot/vmlinuz-$KVER /output/vmlinuz
2026-03-27 19:08:00 +00:00
# Mount virtual filesystems for proper initramfs generation
mount --bind /proc /installer/proc
mount --bind /sys /installer/sys
mount --bind /dev /installer/dev
# Build initramfs with live-boot hooks + our custom hooks
2026-03-27 18:19:49 +00:00
chroot /installer update-initramfs -c -k $KVER
cp /installer/boot/initrd.img-$KVER /output/initrd.img
2026-03-27 19:08:00 +00:00
# Cleanup mounts
umount /installer/dev 2>/dev/null || true
umount /installer/sys 2>/dev/null || true
umount /installer/proc 2>/dev/null || true
2026-03-27 18:19:49 +00:00
# Create squashfs
echo " [container] Creating installer squashfs..."
mksquashfs /installer /output/filesystem.squashfs -comp xz -Xbcj x86 -noappend -quiet
2026-03-27 23:33:31 +00:00
# Build GRUB EFI image with embedded bootstrap config (grub-mkstandalone)
2026-03-27 18:19:49 +00:00
echo " [container] Building GRUB EFI image..."
2026-03-28 18:44:36 +00:00
cat > /tmp/grub-embed.cfg <<GRUBEMBED
2026-03-28 03:15:47 +00:00
insmod part_gpt
2026-03-28 23:58:42 +00:00
insmod part_msdos
2026-03-28 03:15:47 +00:00
insmod fat
insmod iso9660
2026-03-28 23:58:42 +00:00
insmod search
2026-03-28 03:15:47 +00:00
insmod search_label
2026-03-28 23:58:42 +00:00
insmod search_fs_file
2026-03-28 03:15:47 +00:00
insmod normal
insmod linux
insmod all_video
2026-03-28 23:58:42 +00:00
# Try label first (standard path)
2026-03-27 23:33:31 +00:00
search --no-floppy --set= root --label ARCHIPELAGO
2026-03-28 23:58:42 +00:00
# Fallback: search for a known file on the ISO
if [ -z "\$root" ] ; then
search --no-floppy --set= root --file /archipelago/auto-install.sh
fi
# Fallback: try configfile from the EFI partition path
if [ -z "\$root" ] ; then
set root = \$ cmdpath
fi
2026-03-28 18:44:36 +00:00
set prefix = ( \$ root) /boot/grub
2026-03-28 23:58:42 +00:00
configfile ( \$ root) /boot/grub/grub.cfg
# If configfile fails, try normal
2026-03-28 03:15:47 +00:00
normal
2026-03-27 23:33:31 +00:00
GRUBEMBED
grub-mkstandalone -O x86_64-efi \
2026-03-28 23:58:42 +00:00
--modules= "part_gpt part_msdos fat iso9660 search search_label search_fs_file normal linux all_video font gfxterm configfile echo cat ls test true loopback png" \
2026-03-27 23:33:31 +00:00
--locales= "" \
--themes= "" \
--fonts= "" \
--output= /output/BOOTX64.EFI \
"boot/grub/grub.cfg=/tmp/grub-embed.cfg"
# Create EFI FAT image (20MB — includes GRUB binary + grub.cfg)
dd if = /dev/zero of = /output/efi.img bs = 1M count = 20 2>/dev/null
2026-03-27 18:19:49 +00:00
mkfs.vfat /output/efi.img >/dev/null
mmd -i /output/efi.img ::/EFI ::/EFI/BOOT
mcopy -i /output/efi.img /output/BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI
# Copy ISOLINUX files for legacy BIOS boot
cp /usr/lib/ISOLINUX/isolinux.bin /output/isolinux.bin
cp /usr/lib/syslinux/modules/bios/ldlinux.c32 /output/ldlinux.c32
cp /usr/lib/syslinux/modules/bios/menu.c32 /output/menu.c32 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp /usr/lib/syslinux/modules/bios/vesamenu.c32 /output/vesamenu.c32 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp /usr/lib/syslinux/modules/bios/libutil.c32 /output/libutil.c32 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp /usr/lib/syslinux/modules/bios/libcom32.c32 /output/libcom32.c32 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp /usr/lib/ISOLINUX/isohdpfx.bin /output/isohdpfx.bin
# Generate GRUB fonts for theme
echo " [container] Generating GRUB fonts..."
apt-get install -y -qq fonts-dejavu-core grub-common >/dev/null 2>& 1
mkdir -p /output/grub-fonts
fix: container security hardening, onboarding viewport scaling, boot screen cleanup
Container security:
- Add --cap-drop ALL + --security-opt no-new-privileges:true to 12 containers
missing hardening in first-boot-containers.sh (mempool-db, electrumx,
mempool-api, mempool-web, electrs-ui, btcpay-db, nbxplorer, nostr-rs-relay,
strfry, tailscale, bitcoin-ui, lnd-ui)
- Mirror same hardening in deploy-to-target.sh for consistency
- Add --read-only + tmpfs to nostr-rs-relay
- Fix filebrowser deploy to include security flags
- Remove duplicate UI image definitions in image-versions.sh
- Separate Jellyfin capabilities (needs FOWNER, exec tmpfs for CoreCLR JIT)
- Harden archy-net creation with existence check and error handling
UI fixes:
- Fix onboarding viewport scaling: all 7 screens now use h-full + max-h-full
pattern so containers never overflow viewport regardless of padding
- Remove path-option-card wrappers from seed verify inputs, left-justify labels
- Remove batteries/barbarian icons from boot screen (keep bitcoin, cloud, github, save)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:35:34 +01:00
grub-mkfont -s 12 -o /output/grub-fonts/dejavu_12.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
grub-mkfont -s 14 -o /output/grub-fonts/dejavu_14.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
grub-mkfont -s 16 -o /output/grub-fonts/dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
2026-03-27 18:19:49 +00:00
grub-mkfont -s 24 -o /output/grub-fonts/dejavu_24.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf
echo " [container] Done!"
'
# Verify artifacts
for artifact in vmlinuz initrd.img filesystem.squashfs BOOTX64.EFI efi.img isolinux.bin isohdpfx.bin; do
if [ ! -f " $WORK_DIR / $artifact " ] ; then
echo " FATAL: Missing build artifact: $artifact "
2026-02-14 16:44:20 +00:00
exit 1
fi
2026-03-27 18:19:49 +00:00
done
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
# Place artifacts into ISO directory structure
cp " $WORK_DIR /vmlinuz " " $INSTALLER_ISO /live/vmlinuz "
cp " $WORK_DIR /initrd.img " " $INSTALLER_ISO /live/initrd.img "
cp " $WORK_DIR /filesystem.squashfs " " $INSTALLER_ISO /live/filesystem.squashfs "
cp " $WORK_DIR /BOOTX64.EFI " " $INSTALLER_ISO /EFI/BOOT/BOOTX64.EFI "
2026-03-28 00:35:34 +00:00
cp " $WORK_DIR /efi.img " " $INSTALLER_ISO /boot/grub/efi.img "
2026-03-27 18:19:49 +00:00
cp " $WORK_DIR /isolinux.bin " " $INSTALLER_ISO /isolinux/isolinux.bin "
cp " $WORK_DIR /ldlinux.c32 " " $INSTALLER_ISO /isolinux/ldlinux.c32 "
cp " $WORK_DIR /menu.c32 " " $INSTALLER_ISO /isolinux/menu.c32 " 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp " $WORK_DIR /vesamenu.c32 " " $INSTALLER_ISO /isolinux/vesamenu.c32 " 2>/dev/null || true
2026-03-27 18:19:49 +00:00
cp " $WORK_DIR /libutil.c32 " " $INSTALLER_ISO /isolinux/libutil.c32 " 2>/dev/null || true
2026-03-28 03:15:47 +00:00
cp " $WORK_DIR /libcom32.c32 " " $INSTALLER_ISO /isolinux/libcom32.c32 " 2>/dev/null || true
2026-03-27 18:19:49 +00:00
# Install GRUB theme
THEME_SRC = " $SCRIPT_DIR /branding/grub-theme "
THEME_DST = " $INSTALLER_ISO /boot/grub/themes/archipelago "
mkdir -p " $THEME_DST "
if [ -f " $THEME_SRC /theme.txt " ] ; then
cp " $THEME_SRC /theme.txt " " $THEME_DST / "
echo " Installed GRUB theme from branding/grub-theme/"
fi
# Install generated fonts
if [ -d " $WORK_DIR /grub-fonts " ] ; then
cp " $WORK_DIR /grub-fonts/ " *.pf2 " $THEME_DST / "
# Also copy unicode font for GRUB to load
cp " $WORK_DIR /grub-fonts/dejavu_16.pf2 " " $INSTALLER_ISO /boot/grub/font.pf2 "
fi
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Copy GRUB background image (static asset or generate if missing)
GRUB_BG = " $SCRIPT_DIR /branding/grub-theme/background.png "
if [ -f " $GRUB_BG " ] ; then
cp " $GRUB_BG " " $THEME_DST /background.png "
echo " Installed GRUB background"
elif [ -f " $SCRIPT_DIR /branding/generate-grub-background.py " ] ; then
echo " Generating GRUB background..."
python3 " $SCRIPT_DIR /branding/generate-grub-background.py " " $THEME_DST /background.png " 2>/dev/null || \
echo " WARNING: Could not generate GRUB background"
fi
2026-03-27 18:19:49 +00:00
echo " Installer squashfs: $( du -h " $INSTALLER_ISO /live/filesystem.squashfs " | cut -f1) "
echo " Kernel: $( du -h " $INSTALLER_ISO /live/vmlinuz " | cut -f1) "
echo " Initrd: $( du -h " $INSTALLER_ISO /live/initrd.img " | cut -f1) "
echo " Step 2 complete (custom minimal base, no Debian Live)"
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 3: Add Archipelago components
# =============================================================================
echo ""
echo "📦 Step 3: Adding Archipelago components..."
ARCH_DIR = " $INSTALLER_ISO /archipelago "
mkdir -p " $ARCH_DIR "
mkdir -p " $ARCH_DIR /bin "
mkdir -p " $ARCH_DIR /scripts "
2026-03-30 20:52:01 +01:00
# Embed netavark + aardvark-dns for container DNS (podman CNI lacks DNS)
if [ -f /usr/lib/podman/netavark ] && [ -f /usr/lib/podman/aardvark-dns ] ; then
cp /usr/lib/podman/netavark " $ARCH_DIR /bin/netavark "
cp /usr/lib/podman/aardvark-dns " $ARCH_DIR /bin/aardvark-dns "
echo " Embedded netavark + aardvark-dns in ISO"
else
echo " WARNING: netavark/aardvark-dns not found — install with: apt install aardvark-dns netavark"
fi
2026-02-01 05:42:05 +00:00
# Copy the pre-built rootfs
echo " Including root filesystem..."
cp " $ROOTFS_TAR " " $ARCH_DIR /rootfs.tar "
2026-02-03 21:43:33 +00:00
# Capture backend binary from live server
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo " Building backend binary from source..."
else
echo " Capturing backend binary from live server..."
fi
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
BACKEND_CAPTURED = 0
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
2026-03-26 15:28:43 +00:00
# Direct copy from ARCHIPELAGO_BIN env, local install, or remote
2026-03-26 16:23:19 +00:00
BIN = " ${ ARCHIPELAGO_BIN :- /usr/local/bin/archipelago } "
2026-03-26 15:28:43 +00:00
if [ -f " $BIN " ] ; then
cp " $BIN " " $ARCH_DIR /bin/archipelago "
2026-02-17 15:03:34 +00:00
chmod +x " $ARCH_DIR /bin/archipelago "
echo " ✅ Backend captured from local system ( $( du -h " $ARCH_DIR /bin/archipelago " | cut -f1) ) "
BACKEND_CAPTURED = 1
fi
# Remote copy via SCP if local failed
if [ " $BACKEND_CAPTURED " = "0" ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
2026-02-03 21:43:33 +00:00
if scp " $DEV_SERVER :/usr/local/bin/archipelago " " $ARCH_DIR /bin/archipelago " 2>/dev/null; then
chmod +x " $ARCH_DIR /bin/archipelago "
echo " ✅ Backend captured from remote server ( $( du -h " $ARCH_DIR /bin/archipelago " | cut -f1) ) "
BACKEND_CAPTURED = 1
fi
fi
fi
if [ " $BACKEND_CAPTURED " = "0" ] ; then
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
echo " ⚠️ Could not capture from live server, building from source..."
fi
BACKEND_DOCKERFILE = " $WORK_DIR /Dockerfile.backend "
cat > " $BACKEND_DOCKERFILE " <<'BACKENDFILE'
2026-02-01 13:24:03 +00:00
FROM rust:1.93-bookworm as builder
WORKDIR /build
COPY core ./core
RUN cd core && cargo build --release --bin archipelago
BACKENDFILE
2026-03-12 00:19:30 +00:00
if $CONTAINER_CMD build --platform $CONTAINER_PLATFORM -t archipelago-backend -f " $BACKEND_DOCKERFILE " " $SCRIPT_DIR /.. " 2>& 1 | tail -20; then
2026-02-03 21:43:33 +00:00
echo " Extracting backend binary..."
2026-03-12 00:19:30 +00:00
BACKEND_CONTAINER = $( $CONTAINER_CMD create --platform $CONTAINER_PLATFORM archipelago-backend)
2026-02-03 21:43:33 +00:00
$CONTAINER_CMD cp " $BACKEND_CONTAINER :/build/core/target/release/archipelago " " $ARCH_DIR /bin/ " && \
echo " ✅ Backend binary built ( $( du -h " $ARCH_DIR /bin/archipelago " | cut -f1) ) "
$CONTAINER_CMD rm " $BACKEND_CONTAINER "
else
echo " ❌ Backend build failed and server capture failed"
exit 1
fi
fi
2026-04-07 14:40:33 +01:00
# Extract NostrVPN binary from container image (native system service, not a container app)
echo " Extracting NostrVPN binary..."
2026-04-08 15:00:00 +02:00
NVPN_IMAGE = " $( $CONTAINER_CMD images -q 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null) "
2026-04-07 14:40:33 +01:00
if [ -z " $NVPN_IMAGE " ] ; then
2026-04-08 15:00:00 +02:00
$CONTAINER_CMD pull 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null || true
2026-04-07 14:40:33 +01:00
fi
2026-04-08 15:00:00 +02:00
NVPN_CONTAINER = $( $CONTAINER_CMD create 80.71.235.15:3000/archipelago/nostr-vpn:v0.3.7 2>/dev/null) || true
2026-04-07 14:40:33 +01:00
if [ -n " $NVPN_CONTAINER " ] ; then
$CONTAINER_CMD cp " $NVPN_CONTAINER :/usr/local/bin/nvpn " " $ARCH_DIR /bin/nvpn " 2>/dev/null && \
chmod +x " $ARCH_DIR /bin/nvpn " && \
echo " ✅ NostrVPN binary extracted ( $( du -h " $ARCH_DIR /bin/nvpn " | cut -f1) ) "
$CONTAINER_CMD rm " $NVPN_CONTAINER " 2>/dev/null || true
else
echo " ⚠ NostrVPN image not available — nvpn binary will be missing"
fi
2026-04-08 15:06:27 +02:00
# Extract nostr-rs-relay binary from container image (native system service for VPN signaling)
echo " Extracting nostr-rs-relay binary..."
RELAY_IMAGE = " $( $CONTAINER_CMD images -q 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null) "
if [ -z " $RELAY_IMAGE " ] ; then
$CONTAINER_CMD pull 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null || true
fi
RELAY_CONTAINER = $( $CONTAINER_CMD create 80.71.235.15:3000/archipelago/nostr-rs-relay:0.9.0 2>/dev/null) || true
if [ -n " $RELAY_CONTAINER " ] ; then
$CONTAINER_CMD cp " $RELAY_CONTAINER :/usr/local/bin/nostr-rs-relay " " $ARCH_DIR /bin/nostr-rs-relay " 2>/dev/null && \
chmod +x " $ARCH_DIR /bin/nostr-rs-relay " && \
echo " ✅ nostr-rs-relay binary extracted ( $( du -h " $ARCH_DIR /bin/nostr-rs-relay " | cut -f1) ) "
$CONTAINER_CMD rm " $RELAY_CONTAINER " 2>/dev/null || true
else
echo " ⚠ nostr-rs-relay image not available — relay binary will be missing"
fi
2026-04-08 13:15:09 +02:00
# Copy WireGuard helper script
if [ -f " $WORK_DIR /archipelago-wg " ] ; then
cp " $WORK_DIR /archipelago-wg " " $ARCH_DIR /bin/archipelago-wg "
chmod +x " $ARCH_DIR /bin/archipelago-wg "
echo " ✅ WireGuard helper script included"
fi
2026-04-07 14:40:33 +01:00
# Copy NostrVPN UI dashboard for nginx serving
if [ -d " $SCRIPT_DIR /../docker/nostr-vpn-ui " ] ; then
mkdir -p " $ARCH_DIR /web-ui/nostr-vpn "
cp " $SCRIPT_DIR /../docker/nostr-vpn-ui/index.html " " $ARCH_DIR /web-ui/nostr-vpn/ "
echo " ✅ NostrVPN UI dashboard included"
fi
2026-02-03 21:43:33 +00:00
# Capture web UI from live server
if [ " $BUILD_FROM_SOURCE " = "1" ] ; then
echo " Building web UI from source..."
2026-02-01 13:24:03 +00:00
else
2026-02-03 21:43:33 +00:00
echo " Capturing web UI from live server..."
fi
mkdir -p " $ARCH_DIR /web-ui "
# Try to get from live server first (unless BUILD_FROM_SOURCE=1)
WEBUI_CAPTURED = 0
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
2026-02-17 15:03:34 +00:00
# Direct copy from local filesystem (when running on target with sudo)
if [ -d "/opt/archipelago/web-ui" ] && [ " $( ls -A /opt/archipelago/web-ui 2>/dev/null) " ] ; then
cp -r /opt/archipelago/web-ui/* " $ARCH_DIR /web-ui/ "
echo " ✅ Web UI captured from local system ( $( du -sh " $ARCH_DIR /web-ui " | cut -f1) ) "
WEBUI_CAPTURED = 1
fi
# Remote copy via rsync if local failed
if [ " $WEBUI_CAPTURED " = "0" ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
2026-02-03 21:43:33 +00:00
if rsync -az " $DEV_SERVER :/opt/archipelago/web-ui/ " " $ARCH_DIR /web-ui/ " 2>/dev/null && [ " $( ls -A " $ARCH_DIR /web-ui " ) " ] ; then
echo " ✅ Web UI captured from remote server ( $( du -sh " $ARCH_DIR /web-ui " | cut -f1) ) "
WEBUI_CAPTURED = 1
fi
2026-02-01 13:24:03 +00:00
fi
2026-02-01 05:42:05 +00:00
fi
2026-02-03 21:43:33 +00:00
if [ " $WEBUI_CAPTURED " = "0" ] ; then
if [ " $BUILD_FROM_SOURCE " != "1" ] ; then
echo " ⚠️ Could not capture from live server, building from source..."
2026-02-01 13:24:03 +00:00
fi
2026-02-03 21:43:33 +00:00
cd " $SCRIPT_DIR /../neode-ui "
if npm run build 2>& 1 | tail -5; then
if [ -d " $SCRIPT_DIR /../web/dist/neode-ui " ] ; then
echo " Including web UI from web/dist/neode-ui..."
cp -r " $SCRIPT_DIR /../web/dist/neode-ui/ " * " $ARCH_DIR /web-ui/ "
echo " ✅ Web UI built ( $( du -sh " $ARCH_DIR /web-ui " | cut -f1) ) "
fi
2026-02-01 13:24:03 +00:00
else
2026-02-03 21:43:33 +00:00
echo " ⚠️ Web UI build failed"
# Try to use existing build
if [ -d " $SCRIPT_DIR /../web/dist/neode-ui " ] ; then
echo " Using existing web UI build..."
cp -r " $SCRIPT_DIR /../web/dist/neode-ui/ " * " $ARCH_DIR /web-ui/ "
elif [ -d " $SCRIPT_DIR /../neode-ui/dist " ] ; then
echo " Using neode-ui/dist..."
cp -r " $SCRIPT_DIR /../neode-ui/dist/ " * " $ARCH_DIR /web-ui/ "
else
echo " ❌ No web UI available"
exit 1
fi
2026-02-01 13:24:03 +00:00
fi
2026-02-03 21:43:33 +00:00
cd " $SCRIPT_DIR "
2026-02-01 05:42:05 +00:00
fi
2026-04-02 01:28:11 +01:00
# Include AIUI web app (Claude chat interface)
AIUI_INCLUDED = 0
# Search multiple locations for a pre-built AIUI app
for AIUI_DIR in \
" $SCRIPT_DIR /../../AIUI/packages/app/dist " \
" $HOME /AIUI/packages/app/dist " \
"/home/archipelago/AIUI/packages/app/dist" \
"/opt/archipelago/web-ui/aiui" \
"/home/archipelago/archy/AIUI/packages/app/dist" ; do
if [ -d " $AIUI_DIR " ] && [ -f " $AIUI_DIR /index.html " ] ; then
echo " Including AIUI from $AIUI_DIR ... "
mkdir -p " $ARCH_DIR /web-ui/aiui "
2026-04-02 13:04:10 +01:00
# Use rsync to handle same-file (CI workspace == /opt/archipelago) gracefully
if command -v rsync >/dev/null 2>& 1; then
rsync -a " $AIUI_DIR / " " $ARCH_DIR /web-ui/aiui/ "
else
cp -r " $AIUI_DIR / " * " $ARCH_DIR /web-ui/aiui/ " 2>/dev/null || true
fi
2026-04-02 01:28:11 +01:00
echo " ✅ AIUI included ( $( du -sh " $ARCH_DIR /web-ui/aiui " | cut -f1) ) "
AIUI_INCLUDED = 1
break
fi
done
if [ " $AIUI_INCLUDED " = "0" ] ; then
echo " ⚠️ AIUI not found — build it first:"
echo " cd ~/AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build"
echo " Searched: ~/AIUI, /home/archipelago/AIUI, /opt/archipelago/web-ui/aiui"
fi
2026-02-01 05:42:05 +00:00
# Copy app manifests
if [ -d " $SCRIPT_DIR /../apps " ] ; then
echo " Including app manifests..."
cp -r " $SCRIPT_DIR /../apps " " $ARCH_DIR / "
fi
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Copy Plymouth theme files for installation on target
PLYMOUTH_SRC = " $SCRIPT_DIR /branding/plymouth-theme "
if [ -d " $PLYMOUTH_SRC " ] ; then
mkdir -p " $ARCH_DIR /plymouth-theme "
cp " $PLYMOUTH_SRC / " * " $ARCH_DIR /plymouth-theme/ "
echo " Included Plymouth theme"
fi
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 3b: Bundle container images for offline installation
# =============================================================================
echo ""
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-03-26 09:12:16 +00:00
echo "📦 Step 3b: Bundling core containers only (UNBUNDLED mode)"
echo " Optional apps will be downloaded on-demand from the Marketplace after install."
2026-03-10 23:29:05 +00:00
IMAGES_DIR = " $ARCH_DIR /container-images "
mkdir -p " $IMAGES_DIR "
2026-03-26 09:12:16 +00:00
# FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it
2026-03-26 14:06:21 +00:00
CORE_IMAGE = " ${ FILEBROWSER_IMAGE } "
2026-03-26 09:12:16 +00:00
CORE_FILE = "filebrowser.tar"
if [ -f " $IMAGES_DIR / $CORE_FILE " ] ; then
echo " ✅ Using cached: $CORE_FILE "
else
echo " Pulling $CORE_IMAGE ( $CONTAINER_PLATFORM )... "
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM " $CORE_IMAGE " ; then
$CONTAINER_CMD save " $CORE_IMAGE " -o " $IMAGES_DIR / $CORE_FILE " 2>/dev/null && \
echo " ✅ Saved core: $CORE_FILE ( $( du -h " $IMAGES_DIR / $CORE_FILE " | cut -f1) ) " || \
echo " ⚠️ Failed to save $CORE_IMAGE "
else
echo " ⚠️ Failed to pull $CORE_IMAGE — Cloud will not work until installed "
fi
fi
2026-03-10 23:29:05 +00:00
else
2026-02-01 05:42:05 +00:00
echo "📦 Step 3b: Bundling container images for offline use..."
IMAGES_DIR = " $ARCH_DIR /container-images "
mkdir -p " $IMAGES_DIR "
2026-02-14 16:44:20 +00:00
# When DEV_SERVER is set (and not localhost), try to capture images from live server
# so the ISO includes the same set as the dev server (including custom UIs: bitcoin-ui, lnd-ui).
IMAGES_CAPTURED_FROM_SERVER = 0
if [ -n " $DEV_SERVER " ] && [ " $DEV_SERVER " != "localhost" ] && [ " $DEV_SERVER " != "127.0.0.1" ] ; then
echo " Capturing container images from live server ( $DEV_SERVER )... "
2026-03-09 17:09:59 +00:00
# Patterns match against `podman images` repository names (not container names)
2026-04-02 18:20:52 +01:00
CAPTURE_PATTERNS = "bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager adguard"
2026-02-14 16:44:20 +00:00
REMOTE_TMP = " /tmp/archipelago-image-capture- $$ "
2026-03-21 01:11:05 +00:00
SAVED_LIST = $( ssh " $DEV_SERVER " " mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS ; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \" $REMOTE_TMP /\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done " 2>/dev/null) || true
2026-02-14 16:44:20 +00:00
for p in $SAVED_LIST ; do
if [ -n " $p " ] && scp " $DEV_SERVER : $REMOTE_TMP / $p .tar " " $IMAGES_DIR / $p .tar " 2>/dev/null; then
echo " ✅ Captured from server: $p .tar "
IMAGES_CAPTURED_FROM_SERVER = 1
fi
done
ssh " $DEV_SERVER " " rm -rf $REMOTE_TMP " 2>/dev/null || true
if [ " $IMAGES_CAPTURED_FROM_SERVER " = "0" ] ; then
echo " ⚠️ No images captured from server, will use registry pull fallback"
fi
fi
# Define images to bundle for fallback (when not from server or missing). Includes filebrowser.
# bitcoin-ui and lnd-ui are custom and normally captured from server or built separately.
2026-03-09 17:09:59 +00:00
# Alpha: core Bitcoin/Lightning stack + essential apps. Others pulled on-demand from Marketplace.
2026-02-01 05:42:05 +00:00
CONTAINER_IMAGES = "
2026-03-26 14:06:21 +00:00
${ BITCOIN_KNOTS_IMAGE } bitcoin-knots.tar
${ LND_IMAGE } lnd.tar
${ HOMEASSISTANT_IMAGE } homeassistant.tar
${ BTCPAY_IMAGE } btcpayserver.tar
${ NBXPLORER_IMAGE } nbxplorer.tar
${ POSTGRES_IMAGE } postgres-btcpay.tar
${ MEMPOOL_BACKEND_IMAGE } mempool-backend.tar
${ MEMPOOL_WEB_IMAGE } mempool-frontend.tar
${ ELECTRUMX_IMAGE } electrumx.tar
${ MARIADB_IMAGE } mariadb-mempool.tar
${ FEDIMINT_IMAGE } fedimint.tar
${ FEDIMINT_GATEWAY_IMAGE } fedimint-gateway.tar
${ FILEBROWSER_IMAGE } filebrowser.tar
${ ALPINE_TOR_IMAGE } alpine-tor.tar
${ NGINX_ALPINE_IMAGE } nginx-alpine.tar
${ DWN_SERVER_IMAGE } dwn-server.tar
${ GRAFANA_IMAGE } grafana.tar
${ UPTIME_KUMA_IMAGE } uptime-kuma.tar
${ VAULTWARDEN_IMAGE } vaultwarden.tar
${ SEARXNG_IMAGE } searxng.tar
${ PORTAINER_IMAGE } portainer.tar
${ TAILSCALE_IMAGE } tailscale.tar
${ JELLYFIN_IMAGE } jellyfin.tar
${ PHOTOPRISM_IMAGE } photoprism.tar
${ NEXTCLOUD_IMAGE } nextcloud.tar
${ NPM_IMAGE } nginx-proxy-manager.tar
${ ONLYOFFICE_IMAGE } onlyoffice.tar
${ ADGUARDHOME_IMAGE } adguardhome.tar
2026-02-01 05:42:05 +00:00
"
2026-03-12 00:19:30 +00:00
# Pull and save each image (force target arch) only if not already present
2026-02-01 05:42:05 +00:00
echo " $CONTAINER_IMAGES " | while read -r image filename; do
[ -z " $image " ] && continue
tarpath = " $IMAGES_DIR / $filename "
2026-03-10 23:29:05 +00:00
2026-02-01 05:42:05 +00:00
if [ -f " $tarpath " ] ; then
echo " ✅ Using cached: $filename "
else
2026-03-12 00:19:30 +00:00
echo " Pulling $image ( $CONTAINER_PLATFORM )... "
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM " $image " ; then
2026-02-01 05:42:05 +00:00
echo " Saving $filename ... "
2026-03-06 13:00:28 +00:00
if $CONTAINER_CMD save " $image " -o " $tarpath " 2>/dev/null; then
echo " ✅ Saved: $( du -h " $tarpath " | cut -f1) "
else
echo " ⚠️ Failed to save $image (zstd/format issue) - skipping "
rm -f " $tarpath "
fi
2026-02-01 05:42:05 +00:00
else
echo " ⚠️ Failed to pull $image - skipping "
fi
fi
done
2026-03-10 23:29:05 +00:00
fi # end UNBUNDLED check
2026-02-01 05:42:05 +00:00
# Create first-boot service to load images into Podman
echo " Creating first-boot image loader service..."
cat > " $WORK_DIR /archipelago-load-images.service " <<'LOADSERVICE'
[ Unit]
Description = Load Archipelago Container Images
After = network.target podman.service
ConditionPathExists = /opt/archipelago/container-images
ConditionPathExists = !/var/lib/archipelago/.images-loaded
[ Service]
Type = oneshot
ExecStart = /opt/archipelago/scripts/load-container-images.sh
ExecStartPost = /usr/bin/touch /var/lib/archipelago/.images-loaded
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
LOADSERVICE
cat > " $WORK_DIR /load-container-images.sh " <<'LOADSCRIPT'
#!/bin/bash
# Load pre-bundled container images into Podman
IMAGES_DIR = "/opt/archipelago/container-images"
LOG_FILE = "/var/log/archipelago-images.log"
echo " $( date) : Starting container image load " >> " $LOG_FILE "
if [ ! -d " $IMAGES_DIR " ] ; then
echo " $( date) : No images directory found " >> " $LOG_FILE "
exit 0
fi
for tarfile in " $IMAGES_DIR " /*.tar; do
if [ -f " $tarfile " ] ; then
echo " $( date) : Loading $( basename " $tarfile " ) ... " >> " $LOG_FILE "
podman load -i " $tarfile " >> " $LOG_FILE " 2>& 1 && \
echo " $( date) : Successfully loaded $( basename " $tarfile " ) " >> " $LOG_FILE " || \
echo " $( date) : Failed to load $( basename " $tarfile " ) " >> " $LOG_FILE "
fi
done
2026-02-17 15:03:34 +00:00
# Ensure archy-net exists for mempool stack (db, api, frontend)
podman network create archy-net 2>/dev/null || true
2026-02-01 05:42:05 +00:00
echo " $( date) : Container image load complete " >> " $LOG_FILE "
echo " $( date) : Available images: " >> " $LOG_FILE "
podman images >> " $LOG_FILE " 2>& 1
LOADSCRIPT
chmod +x " $WORK_DIR /load-container-images.sh "
# Copy scripts to ISO
mkdir -p " $ARCH_DIR /scripts "
cp " $WORK_DIR /load-container-images.sh " " $ARCH_DIR /scripts/ "
cp " $WORK_DIR /archipelago-load-images.service " " $ARCH_DIR /scripts/ "
2026-02-17 15:03:34 +00:00
# Tor setup: copy torrc and create first-boot setup script
mkdir -p " $ARCH_DIR /scripts/tor "
if [ -f " $SCRIPT_DIR /../scripts/tor/torrc.template " ] ; then
cp " $SCRIPT_DIR /../scripts/tor/torrc.template " " $ARCH_DIR /scripts/tor/torrc "
fi
echo " Creating first-boot Tor setup service..."
cat > " $WORK_DIR /archipelago-setup-tor.service " <<'TORSERVICE'
[ Unit]
Description = Setup and start Archipelago Tor hidden services
After = archipelago-load-images.service network.target podman.service
ConditionPathExists = /opt/archipelago/scripts/setup-tor.sh
[ Service]
Type = oneshot
ExecStart = /opt/archipelago/scripts/setup-tor.sh
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
TORSERVICE
cat > " $WORK_DIR /setup-tor.sh " <<'TORSCRIPT '
#!/bin/bash
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Setup and start Tor hidden services (autoinstaller first-boot)
# Prefers system Tor (apt package) over container
2026-02-17 15:03:34 +00:00
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
ARCHY_TOR_DIR = "/var/lib/archipelago/tor"
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
TOR_CONFIG_DIR = "/var/lib/archipelago/tor-config"
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
TOR_DIR = "/var/lib/tor"
2026-02-17 15:03:34 +00:00
LOG = "/var/log/archipelago-tor.log"
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
mkdir -p " $ARCHY_TOR_DIR " " $TOR_CONFIG_DIR "
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Write services.json for the backend to read
2026-03-30 16:35:06 +01:00
cat > " $ARCHY_TOR_DIR /services.json " <<TORJSON
2026-03-28 23:41:40 +00:00
{
"services" : [
{ "name" : "archipelago" , "local_port" : 80, "enabled" : true} ,
{ "name" : "bitcoin" , "local_port" : 8333, "enabled" : true} ,
{ "name" : "electrumx" , "local_port" : 50001, "enabled" : true} ,
{ "name" : "lnd" , "local_port" : 9735, "enabled" : true} ,
{ "name" : "btcpay" , "local_port" : 23000, "enabled" : true} ,
{ "name" : "mempool" , "local_port" : 4080, "enabled" : true} ,
{ "name" : "fedimint" , "local_port" : 8175, "enabled" : true}
]
}
TORJSON
echo "services.json created"
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
# Backend reads from tor-config/, not tor/
cp " $ARCHY_TOR_DIR /services.json " " $TOR_CONFIG_DIR /services.json "
chown -R archipelago:archipelago " $TOR_CONFIG_DIR " 2>/dev/null || true
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe)
cat > /etc/tor/torrc <<TORRC
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
SocksPort 0.0.0.0:9050
SocksPolicy accept 10.89.0.0/16
SocksPolicy accept 127.0.0.0/8
SocksPolicy reject *
# ControlPort disabled for security
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
HiddenServiceDir $TOR_DIR /hidden_service_archipelago
HiddenServicePort 80 127.0.0.1:80
HiddenServiceDir $TOR_DIR /hidden_service_bitcoin
HiddenServicePort 8333 127.0.0.1:8333
2026-04-02 16:15:04 +01:00
HiddenServicePort 8332 127.0.0.1:8332
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
2026-03-16 12:58:35 +00:00
HiddenServiceDir $TOR_DIR /hidden_service_electrumx
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
HiddenServicePort 50001 127.0.0.1:50001
HiddenServiceDir $TOR_DIR /hidden_service_lnd
HiddenServicePort 9735 127.0.0.1:9735
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
HiddenServicePort 8080 127.0.0.1:8080
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
HiddenServiceDir $TOR_DIR /hidden_service_btcpay
HiddenServicePort 23000 127.0.0.1:23000
HiddenServiceDir $TOR_DIR /hidden_service_mempool
HiddenServicePort 4080 127.0.0.1:4080
HiddenServiceDir $TOR_DIR /hidden_service_fedimint
HiddenServicePort 8175 127.0.0.1:8175
TORRC
2026-03-28 02:40:39 +00:00
# Create hidden service dirs with correct ownership and permissions (700, not 750)
# Tor refuses to start if permissions are too permissive
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint; do
mkdir -p " $TOR_DIR /hidden_service_ $svc "
chown debian-tor:debian-tor " $TOR_DIR /hidden_service_ $svc "
chmod 700 " $TOR_DIR /hidden_service_ $svc "
done
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
# Prefer system Tor (installed via apt)
if command -v tor >/dev/null 2>& 1; then
echo " $( date) : Using system Tor daemon " >> " $LOG "
systemctl enable tor 2>/dev/null
systemctl restart tor@default 2>/dev/null
else
# Fallback: use container
echo " $( date) : System Tor not found, using container " >> " $LOG "
DOCKER = podman
command -v podman >/dev/null 2>& 1 || DOCKER = docker
for c in $( sudo $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'archy-tor|^tor$' ) ; do
[ -n " $c " ] && sudo $DOCKER stop " $c " 2>/dev/null; sudo $DOCKER rm -f " $c " 2>/dev/null
done
if ! sudo $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-tor; then
sudo $DOCKER run -d --name archy-tor --restart unless-stopped --network host \
-v " $TOR_DIR : $TOR_DIR " \
--entrypoint tor \
2026-03-26 14:06:21 +00:00
${ ALPINE_TOR_IMAGE } \
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
-f /etc/tor/torrc >> " $LOG " 2>& 1
2026-02-17 15:03:34 +00:00
echo " $( date) : Tor container started " >> " $LOG "
fi
fi
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
2026-02-17 15:03:34 +00:00
# Wait for Tor to create hostname files (~30-60s), then chmod so archipelago user can read
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 6
if [ -f " $TOR_DIR /hidden_service_archipelago/hostname " ] ; then
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
chmod 750 " $TOR_DIR " /hidden_service_*/ 2>/dev/null || true
2026-02-17 15:03:34 +00:00
for f in " $TOR_DIR " /hidden_service_*/hostname; do
fix: add 9 missing apps to ISO build (ISO-01)
CAPTURE_PATTERNS: added photoprism, nextcloud, nginx-proxy-manager,
immich, onlyoffice, adguard, penpot patterns.
CONTAINER_IMAGES: added jellyfin, photoprism, nextcloud,
nginx-proxy-manager, immich-server, postgres-immich, redis-immich,
onlyoffice, adguardhome with pinned versions for fallback pull.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:17:12 +00:00
[ -f " $f " ] && chmod 640 " $f " && echo " $( date) : chmod hostname $f " >> " $LOG "
2026-02-17 15:03:34 +00:00
done
echo " $( date) : Tor hostname files readable by archipelago " >> " $LOG "
break
fi
done
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
# Sync hostnames to backend-readable directory
HOSTNAMES_DIR = "/var/lib/archipelago/tor-hostnames"
mkdir -p " $HOSTNAMES_DIR "
for svc in archipelago bitcoin electrumx lnd btcpay mempool fedimint; do
if [ -f " $TOR_DIR /hidden_service_ ${ svc } /hostname " ] ; then
cp " $TOR_DIR /hidden_service_ ${ svc } /hostname " " $HOSTNAMES_DIR / $svc "
echo " $( date) : Synced hostname: $svc " >> " $LOG "
fi
done
chown -R archipelago:archipelago " $HOSTNAMES_DIR " 2>/dev/null || true
echo " $( date) : Hostnames synced: $( ls $HOSTNAMES_DIR 2>/dev/null | tr '\n' ' ' ) " >> " $LOG "
2026-02-17 15:03:34 +00:00
TORSCRIPT
chmod +x " $WORK_DIR /setup-tor.sh "
cp " $WORK_DIR /setup-tor.sh " " $ARCH_DIR /scripts/ "
cp " $WORK_DIR /archipelago-setup-tor.service " " $ARCH_DIR /scripts/ "
2026-02-25 18:04:41 +00:00
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
2026-04-01 22:31:45 +01:00
# Both bundled and unbundled builds use the full first-boot script.
# Unbundled mode pulls images from registry; bundled mode loads from tarballs.
if false && [ " $UNBUNDLED " = "1" ] ; then
2026-03-26 09:12:16 +00:00
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
2026-04-01 22:31:45 +01:00
# DISABLED: minimal script doesn't create UI sidecars or write app configs.
# The full first-boot-containers.sh handles both bundled and unbundled modes.
2026-03-26 09:12:16 +00:00
cat > " $WORK_DIR /first-boot-containers-unbundled.sh " <<'FBUNBUNDLED'
#!/bin/bash
# Minimal first-boot: create FileBrowser container only (unbundled ISO)
set -e
LOG = "/var/log/archipelago-first-boot.log"
echo " [ $( date) ] Starting minimal first-boot (unbundled)... " >> " $LOG "
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Source image versions (provides $FILEBROWSER_IMAGE etc.)
for f in /opt/archipelago/scripts/image-versions.sh /home/archipelago/archy/scripts/image-versions.sh; do
if [ -f " $f " ] ; then
source " $f "
echo " [ $( date) ] Sourced image versions from $f " >> " $LOG "
break
fi
done
if [ -z " $FILEBROWSER_IMAGE " ] ; then
echo " [ $( date) ] ERROR: FILEBROWSER_IMAGE not set — image-versions.sh missing or incomplete " >> " $LOG "
exit 1
fi
# Create Cloud storage directories (as root, then fix ownership for rootless podman)
2026-03-26 09:12:16 +00:00
mkdir -p /var/lib/archipelago/filebrowser
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
mkdir -p /var/lib/archipelago/filebrowser-data
2026-03-26 09:12:16 +00:00
mkdir -p /var/lib/archipelago/data/cloud/{ Documents,Photos,Music,Videos,Downloads}
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Container UID 0 maps to host UID 100000 under rootless podman (subuid mapping)
chown -R 100000:100000 /var/lib/archipelago/filebrowser
chown -R 100000:100000 /var/lib/archipelago/filebrowser-data
chown -R 100000:100000 /var/lib/archipelago/data
# Enable linger so rootless podman containers survive logout
loginctl enable-linger archipelago 2>/dev/null || true
2026-04-02 15:16:24 +01:00
# Enable podman-restart so containers with --restart=unless-stopped auto-start on boot
runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && systemctl --user enable podman-restart.service' 2>>" $LOG " || true
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Ensure podman socket is active for archipelago user
runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && systemctl --user enable --now podman.socket' 2>>" $LOG " || true
# Create FileBrowser container as archipelago user (rootless podman)
2026-03-29 21:56:38 +01:00
# Generate random FileBrowser password and store for auto-login
FB_PASS_DIR = "/var/lib/archipelago/secrets/filebrowser"
mkdir -p " $FB_PASS_DIR "
if [ ! -f " $FB_PASS_DIR /password " ] ; then
head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24 > " $FB_PASS_DIR /password "
chmod 600 " $FB_PASS_DIR /password "
chown 1000:1000 " $FB_PASS_DIR /password "
fi
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
if ! runuser -u archipelago -- bash -c 'export XDG_RUNTIME_DIR=/run/user/1000 && podman ps -a --format "{{.Names}}"' 2>/dev/null | grep -q filebrowser; then
echo " [ $( date) ] Creating FileBrowser container ( $FILEBROWSER_IMAGE )... " >> " $LOG "
runuser -u archipelago -- bash -c " export XDG_RUNTIME_DIR=/run/user/1000 && podman run -d --name filebrowser --restart unless-stopped \
--cap-drop= ALL \
--cap-add= DAC_OVERRIDE \
2026-03-27 17:17:18 +00:00
--cap-add= NET_BIND_SERVICE \
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
--security-opt= no-new-privileges:true \
--read-only \
--tmpfs= /tmp:rw,noexec,nosuid,size= 64m \
--health-cmd= 'curl -sf http://localhost:80/ || exit 1' \
2026-03-26 09:12:16 +00:00
--health-interval= 30s --health-timeout= 5s --health-retries= 3 \
--memory= 256m \
-p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
-v /var/lib/archipelago/filebrowser-data:/data \
-v /var/lib/archipelago/data/cloud:/srv/cloud \
$FILEBROWSER_IMAGE \
--database= /data/database.db --root= /srv --address= 0.0.0.0 --port= 80" 2>>" $LOG " && \
2026-03-26 09:12:16 +00:00
echo " [ $( date) ] FileBrowser created successfully " >> " $LOG " || \
echo " [ $( date) ] WARNING: FileBrowser creation failed " >> " $LOG "
2026-03-29 21:56:38 +01:00
# Set FileBrowser password to match the stored random password
sleep 5
FB_PASS = $( cat " $FB_PASS_DIR /password " 2>/dev/null || echo "admin" )
runuser -u archipelago -- bash -c " export XDG_RUNTIME_DIR=/run/user/1000 && podman exec filebrowser filebrowser users update admin --password ' $FB_PASS ' --database /data/database.db " 2>>" $LOG " && \
echo " [ $( date) ] FileBrowser admin password set " >> " $LOG " || \
echo " [ $( date) ] WARNING: Could not set FileBrowser password " >> " $LOG "
2026-03-26 09:12:16 +00:00
fi
echo " [ $( date) ] Minimal first-boot complete " >> " $LOG "
FBUNBUNDLED
chmod +x " $WORK_DIR /first-boot-containers-unbundled.sh "
cp " $WORK_DIR /first-boot-containers-unbundled.sh " " $ARCH_DIR /scripts/first-boot-containers.sh "
2026-04-01 10:38:41 +01:00
# Copy shared script library (TUI animations for installer, shared utils)
if [ -d " $SCRIPT_DIR /../scripts/lib " ] ; then
mkdir -p " $ARCH_DIR /scripts/lib "
cp " $SCRIPT_DIR /../scripts/lib/ " *.sh " $ARCH_DIR /scripts/lib/ " 2>/dev/null || true
echo " Copied scripts/lib/ ( $( ls " $ARCH_DIR /scripts/lib/ " 2>/dev/null | wc -l) files) "
fi
2026-03-26 09:12:16 +00:00
cat > " $WORK_DIR /archipelago-first-boot-containers.service " <<'FBCSERVICE'
[ Unit]
Description = Create core Archipelago containers on first boot
2026-03-31 18:31:00 +01:00
After = archipelago-load-images.service archipelago-setup-tor.service network-online.target podman.service
Wants = archipelago-load-images.service
2026-03-26 09:12:16 +00:00
ConditionPathExists = /opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists = !/var/lib/archipelago/.first-boot-containers-done
[ Service]
Type = oneshot
2026-03-31 18:31:00 +01:00
TimeoutStartSec = 900
2026-03-26 09:12:16 +00:00
ExecStart = /opt/archipelago/scripts/first-boot-containers.sh
ExecStartPost = /usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
FBCSERVICE
cp " $WORK_DIR /archipelago-first-boot-containers.service " " $ARCH_DIR /scripts/ "
2026-03-10 23:29:05 +00:00
else
echo " Creating first-boot container creation service..."
2026-03-21 03:06:29 +00:00
# Copy shared script library
if [ -d " $SCRIPT_DIR /../scripts/lib " ] ; then
mkdir -p " $ARCH_DIR /scripts/lib "
cp " $SCRIPT_DIR /../scripts/lib/ " *.sh " $ARCH_DIR /scripts/lib/ " 2>/dev/null || true
fi
2026-03-10 23:29:05 +00:00
if [ -f " $SCRIPT_DIR /../scripts/first-boot-containers.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/first-boot-containers.sh " " $ARCH_DIR /scripts/ "
chmod +x " $ARCH_DIR /scripts/first-boot-containers.sh "
cat > " $WORK_DIR /archipelago-first-boot-containers.service " <<'FBCSERVICE'
2026-02-25 18:04:41 +00:00
[ Unit]
Description = Create core Archipelago containers on first boot
2026-03-31 18:31:00 +01:00
After = archipelago-load-images.service archipelago-setup-tor.service network-online.target podman.service
Wants = archipelago-load-images.service
2026-02-25 18:04:41 +00:00
ConditionPathExists = /opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists = !/var/lib/archipelago/.first-boot-containers-done
[ Service]
Type = oneshot
2026-03-31 18:31:00 +01:00
TimeoutStartSec = 900
2026-02-25 18:04:41 +00:00
ExecStart = /opt/archipelago/scripts/first-boot-containers.sh
ExecStartPost = /usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
FBCSERVICE
2026-03-10 23:29:05 +00:00
cp " $WORK_DIR /archipelago-first-boot-containers.service " " $ARCH_DIR /scripts/ "
fi
2026-02-25 18:04:41 +00:00
fi
2026-04-02 16:15:04 +01:00
# Bootstrap node config — new installs use this Bitcoin node during IBD
# so ElectrumX/LND/BTCPay/Mempool work immediately while local chain syncs
# Tries LAN first (fast), falls back to Tor (works from anywhere)
BOOTSTRAP_RPC_PASS = ""
BOOTSTRAP_ONION = ""
if [ -f /var/lib/archipelago/secrets/bitcoin-rpc-password ] ; then
BOOTSTRAP_RPC_PASS = $( cat /var/lib/archipelago/secrets/bitcoin-rpc-password 2>/dev/null)
fi
if [ -f /var/lib/archipelago/tor-hostnames/bitcoin ] ; then
BOOTSTRAP_ONION = $( cat /var/lib/archipelago/tor-hostnames/bitcoin 2>/dev/null)
fi
if [ -n " $BOOTSTRAP_RPC_PASS " ] ; then
DEV_IP = " ${ DEV_SERVER :- 192 .168.1.228 } "
cat > " $ARCH_DIR /bootstrap.conf " <<BSTRAP
# Bootstrap Bitcoin node — used during Initial Block Download
# Services connect here until the local node is fully synced
# First-boot tries LAN, then Tor (works from any network)
BOOTSTRAP_LAN_HOST = ${ DEV_IP }
BOOTSTRAP_ONION = ${ BOOTSTRAP_ONION }
BOOTSTRAP_RPC_USER = archipelago
BOOTSTRAP_RPC_PASS = ${ BOOTSTRAP_RPC_PASS }
BSTRAP
chmod 600 " $ARCH_DIR /bootstrap.conf "
echo " ✅ Bootstrap node config embedded (LAN: ${ DEV_IP } , Tor: ${ BOOTSTRAP_ONION :- none } ) "
else
echo " ⚠ No bootstrap config — no Bitcoin RPC password found on build host"
fi
# Bundle bootstrap switchover script + systemd timer
if [ -f " $SCRIPT_DIR /../scripts/bootstrap-switchover.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/bootstrap-switchover.sh " " $ARCH_DIR /scripts/ "
chmod +x " $ARCH_DIR /scripts/bootstrap-switchover.sh "
echo " ✅ Bundled bootstrap switchover script"
fi
2026-03-09 17:09:59 +00:00
# Bundle E2E test script for post-install validation
if [ -f " $SCRIPT_DIR /../scripts/run-e2e-tests.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/run-e2e-tests.sh " " $ARCH_DIR /scripts/ "
chmod +x " $ARCH_DIR /scripts/run-e2e-tests.sh "
echo " ✅ Bundled E2E test script for post-install validation"
fi
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
if [ -f " $SCRIPT_DIR /../scripts/run-post-install-tests.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/run-post-install-tests.sh " " $ARCH_DIR /scripts/ "
chmod +x " $ARCH_DIR /scripts/run-post-install-tests.sh "
echo " ✅ Bundled post-install test suite"
fi
2026-03-09 17:09:59 +00:00
2026-03-25 15:52:26 +00:00
# Bundle self-update script and image-versions for update system
if [ -f " $SCRIPT_DIR /../scripts/self-update.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/self-update.sh " " $ARCH_DIR /scripts/ "
chmod +x " $ARCH_DIR /scripts/self-update.sh "
echo " ✅ Bundled self-update script"
fi
if [ -f " $SCRIPT_DIR /../scripts/image-versions.sh " ] ; then
cp " $SCRIPT_DIR /../scripts/image-versions.sh " " $ARCH_DIR /scripts/ "
echo " ✅ Bundled image-versions.sh"
fi
2026-03-29 17:48:13 +01:00
# Bundle docker UI source files for building custom UIs on first boot
# Always bundle — these are tiny HTML/CSS files, not container images
if true; then
2026-03-10 23:29:05 +00:00
DOCKER_UI_DIR = " $SCRIPT_DIR /../docker "
if [ -d " $DOCKER_UI_DIR " ] ; then
echo " Bundling docker UI source files..."
mkdir -p " $ARCH_DIR /docker "
for ui_dir in bitcoin-ui lnd-ui electrs-ui; do
if [ -d " $DOCKER_UI_DIR / $ui_dir " ] ; then
cp -r " $DOCKER_UI_DIR / $ui_dir " " $ARCH_DIR /docker/ "
echo " ✅ Bundled $ui_dir source "
fi
done
fi
2026-03-08 02:27:58 +00:00
fi
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
echo " ✅ Unbundled build ready (Tor setup included, no container images)"
else
echo " ✅ Container images bundled (including Tor + first-boot)"
fi
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 4: Create auto-installer script
# =============================================================================
echo ""
echo "📦 Step 4: Creating auto-installer..."
cat > " $ARCH_DIR /auto-install.sh " <<'INSTALLER_SCRI PT'
#!/bin/bash
#
# Archipelago Auto-Installer
# Automatically installs to internal disk (StartOS-like experience)
#
set -e
2026-03-29 12:06:19 +01:00
# Log file — verbose command output goes here, TUI stays on console
feat: add build report and first-boot diagnostics
CI build report: checks rootfs contents (nginx, SSL, keyboard, kiosk,
lid config, backend, frontend) and ISO contents after build. Reports
in the Actions log so build issues are immediately visible.
First-boot diagnostics: one-shot systemd service runs 30s after first
boot, logs service status, nginx test, SSL certs, LUKS, podman,
kiosk, console-setup, disk, network, and journal errors to
/var/log/archipelago-first-boot-diag.log. Only runs once (ConditionPathExists).
SSH in and cat the log to debug any fresh install issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:54:32 +00:00
INSTALL_LOG = "/tmp/archipelago-install.log"
2026-03-29 12:06:19 +01:00
# Run commands quietly: redirect their stdout/stderr to log
# TUI functions (p, step, ok, etc.) print directly to console
run( ) { " $@ " >> " $INSTALL_LOG " 2>& 1; }
runq( ) { " $@ " >>" $INSTALL_LOG " 2>& 1 || true; }
feat: add build report and first-boot diagnostics
CI build report: checks rootfs contents (nginx, SSL, keyboard, kiosk,
lid config, backend, frontend) and ISO contents after build. Reports
in the Actions log so build issues are immediately visible.
First-boot diagnostics: one-shot systemd service runs 30s after first
boot, logs service status, nginx test, SSL certs, LUKS, podman,
kiosk, console-setup, disk, network, and journal errors to
/var/log/archipelago-first-boot-diag.log. Only runs once (ConditionPathExists).
SSH in and cat the log to debug any fresh install issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:54:32 +00:00
2026-03-21 01:11:05 +00:00
# Detect architecture at install time
case " $( uname -m) " in
x86_64| amd64)
ARCH = "x86_64"
GRUB_TARGET = "x86_64-efi"
GRUB_BIOS_TARGET = "i386-pc"
; ;
aarch64| arm64)
ARCH = "arm64"
GRUB_TARGET = "arm64-efi"
GRUB_BIOS_TARGET = ""
; ;
esac
2026-03-28 23:58:42 +00:00
# Colors — 256-color ANSI (works on Linux fbcon console)
ORANGE = $'\033[38;5;208m'
ORANGE_DIM = $'\033[38;5;130m'
ORANGE_BRIGHT = $'\033[38;5;214m'
2026-03-28 11:31:48 +00:00
RED = $'\033[31m'
GREEN = $'\033[32m'
WHITE = $'\033[1;37m'
2026-03-28 23:58:42 +00:00
DIM = $'\033[38;5;242m'
DIMMER = $'\033[38;5;238m'
2026-03-25 17:03:21 +00:00
NC = $'\033[0m'
2026-03-28 23:58:42 +00:00
BOLD = $'\033[1m'
2026-02-01 05:42:05 +00:00
2026-03-29 15:43:57 +01:00
# Left-justified layout — 2-space indent, no centering
PADS = " "
2026-03-28 23:58:42 +00:00
p( ) { printf "%s%b\n" " $PADS " " $1 " ; }
2026-03-29 15:43:57 +01:00
hrule( ) { local hr = "" ; for i in $( seq 1 48) ; do hr = " ${ hr } ─ " ; done ; p " ${ ORANGE_DIM } ${ hr } ${ NC } " ; }
# Typewriter animation for key text
typewrite( ) {
local text = " $1 " delay = " ${ 2 :- 0 .02 } "
printf "%s" " $PADS "
local i = 0
while [ $i -lt ${# text } ] ; do
printf "%s" " ${ text : $i : 1 } "
i = $(( i + 1 ))
sleep " $delay "
done
printf "\n"
}
2026-03-28 11:31:48 +00:00
2026-03-28 23:58:42 +00:00
# Phase display
2026-03-28 13:39:10 +00:00
STEP = 0
2026-03-28 23:41:40 +00:00
TOTAL_STEPS = 8
2026-03-28 13:39:10 +00:00
step( ) {
STEP = $(( STEP + 1 ))
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE } [ $STEP / $TOTAL_STEPS ] $1 ${ NC } "
2026-03-28 13:39:10 +00:00
}
2026-03-28 23:58:42 +00:00
ok( ) { p " ${ ORANGE_BRIGHT } ✓ $1 ${ NC } " ; }
warn( ) { p " ${ ORANGE } ⚠ $1 ${ NC } " ; }
fail( ) { p " ${ RED } ✗ $1 ${ NC } " ; }
2026-03-28 13:39:10 +00:00
spinner( ) {
local pid = $1 msg = $2
local frames = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i = 0
while kill -0 " $pid " 2>/dev/null; do
2026-03-28 23:58:42 +00:00
printf "\r%s %b%s %s%b" " $PADS " " $ORANGE " " ${ frames : i %10 : 1 } " " $msg " " $NC "
2026-03-28 13:39:10 +00:00
i = $(( i + 1 ))
sleep 0.1
done
2026-03-28 23:58:42 +00:00
printf "\r%s %b✓ %s%b\n" " $PADS " " $ORANGE_BRIGHT " " $msg " " $NC "
2026-03-28 13:39:10 +00:00
}
2026-03-31 21:23:29 +01:00
# Source TUI library for install animations (graceful fallback if missing)
for _tui_path in \
" $BOOT_MEDIA /archipelago/scripts/lib/install-tui.sh " \
"/opt/archipelago/scripts/lib/install-tui.sh" \
" $( dirname " $0 " ) /../scripts/lib/install-tui.sh " ; do
[ -f " $_tui_path " ] && { source " $_tui_path " 2>/dev/null; break; }
done
if [ " ${ TUI_AVAILABLE :- } " = "1" ] ; then
tui_welcome
tui_enable_progress_spinner
else
clear
echo ""
echo -e " ${ PADS } ${ ORANGE } ▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█ ${ NC } "
echo -e " ${ PADS } ${ ORANGE } █▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █ ${ NC } "
echo -e " ${ PADS } ${ ORANGE } ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀ ${ NC } "
typewrite " $( echo -e " ${ ORANGE_DIM } bitcoin node os ${ NC } " ) " 0.04
echo ""
fi
2026-02-01 05:42:05 +00:00
# Check required tools are present (should be bundled in ISO)
2026-03-28 13:39:10 +00:00
step "Checking tools"
2026-02-01 05:42:05 +00:00
MISSING = ""
command -v parted >/dev/null 2>& 1 || MISSING = " parted $MISSING "
command -v mkfs.vfat >/dev/null 2>& 1 || MISSING = " mkfs.vfat $MISSING "
command -v mkfs.ext4 >/dev/null 2>& 1 || MISSING = " mkfs.ext4 $MISSING "
if [ -n " $MISSING " ] ; then
2026-03-28 13:39:10 +00:00
warn " Installing missing: $MISSING "
2026-02-01 05:42:05 +00:00
if apt-get update -qq >/dev/null 2>& 1; then
2026-03-28 13:39:10 +00:00
apt-get install -y -qq parted dosfstools e2fsprogs >/dev/null 2>& 1 && ok "Tools installed" || {
fail "Failed to install required tools"
2026-02-01 05:42:05 +00:00
exit 1
}
else
2026-03-28 13:39:10 +00:00
fail "No network available and tools not bundled"
2026-02-01 05:42:05 +00:00
exit 1
fi
else
2026-03-28 13:39:10 +00:00
ok "All tools present"
2026-02-01 05:42:05 +00:00
fi
# Find boot media
BOOT_MEDIA = ""
2026-03-27 18:19:49 +00:00
for dev in /run/archiso /cdrom /media/cdrom /run/live/medium /lib/live/mount/medium /mnt/iso; do
2026-02-01 05:42:05 +00:00
if [ -d " $dev /archipelago " ] ; then
BOOT_MEDIA = " $dev "
break
fi
done
if [ -z " $BOOT_MEDIA " ] ; then
echo -e " ${ RED } ❌ Boot media not found ${ NC } "
exit 1
fi
ROOTFS_TAR = " $BOOT_MEDIA /archipelago/rootfs.tar "
if [ ! -f " $ROOTFS_TAR " ] ; then
echo -e " ${ RED } ❌ Root filesystem not found: $ROOTFS_TAR ${ NC } "
exit 1
fi
# Find the boot USB device to exclude it
BOOT_DEV = $( findmnt -n -o SOURCE " $BOOT_MEDIA " 2>/dev/null | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//' )
BOOT_DEV_NAME = $( basename " $BOOT_DEV " 2>/dev/null || echo "" )
2026-03-28 13:39:10 +00:00
step "Detecting disks"
2026-02-01 05:42:05 +00:00
echo ""
# Find internal disk (prefer NVMe, then SATA, skip USB)
TARGET_DISK = ""
TARGET_SIZE = ""
# Check NVMe drives first
for disk in /dev/nvme*n1; do
if [ -b " $disk " ] ; then
disk_name = $( basename " $disk " )
if [ " $disk_name " != " $BOOT_DEV_NAME " ] ; then
size = $( lsblk -dn -o SIZE " $disk " 2>/dev/null)
model = $( cat /sys/block/$disk_name /device/model 2>/dev/null || echo "NVMe SSD" )
echo " Found: $disk ( $size ) - $model "
TARGET_DISK = " $disk "
TARGET_SIZE = " $size "
break
fi
fi
done
# If no NVMe, check SATA drives
if [ -z " $TARGET_DISK " ] ; then
for disk in /dev/sd[ a-z] ; do
if [ -b " $disk " ] ; then
disk_name = $( basename " $disk " )
if [ " $disk_name " != " $BOOT_DEV_NAME " ] ; then
# Skip USB drives (check removable flag)
removable = $( cat /sys/block/$disk_name /removable 2>/dev/null || echo "0" )
if [ " $removable " = "0" ] ; then
size = $( lsblk -dn -o SIZE " $disk " 2>/dev/null)
model = $( cat /sys/block/$disk_name /device/model 2>/dev/null || echo "SATA Drive" )
echo " Found: $disk ( $size ) - $model "
TARGET_DISK = " $disk "
TARGET_SIZE = " $size "
break
fi
fi
fi
done
fi
if [ -z " $TARGET_DISK " ] ; then
echo ""
echo -e " ${ RED } ❌ No suitable internal disk found ${ NC } "
echo " Please ensure an internal drive is connected."
exit 1
fi
2026-03-28 13:39:10 +00:00
ok " $TARGET_DISK ( $TARGET_SIZE ) "
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 13:39:10 +00:00
hrule
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE } ⚠ all data on $TARGET_DISK will be erased ${ NC } "
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE_DIM } press enter to install | ctrl+c to cancel ${ NC } "
2026-03-28 11:31:48 +00:00
read -s
2026-02-01 05:42:05 +00:00
echo ""
# Unmount any existing partitions
umount ${ TARGET_DISK } * 2>/dev/null || true
umount ${ TARGET_DISK } p* 2>/dev/null || true
2026-03-26 09:12:16 +00:00
# Create partition table — dual BIOS+UEFI boot + LUKS2 encrypted data
2026-03-28 13:39:10 +00:00
step "Creating partitions"
2026-02-01 05:42:05 +00:00
parted -s " $TARGET_DISK " mklabel gpt
2026-03-21 01:11:05 +00:00
# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks)
parted -s " $TARGET_DISK " mkpart bios_boot 1MiB 2MiB
parted -s " $TARGET_DISK " set 1 bios_grub on
# Partition 2: 512MB EFI System Partition (for UEFI boot)
parted -s " $TARGET_DISK " mkpart efi fat32 2MiB 514MiB
parted -s " $TARGET_DISK " set 2 esp on
2026-03-26 09:12:16 +00:00
# Partition 3: Root filesystem (30GB — system, packages, container runtime)
parted -s " $TARGET_DISK " mkpart root ext4 514MiB 30GiB
# Partition 4: Encrypted data (LUKS2 — Bitcoin data, secrets, app volumes)
parted -s " $TARGET_DISK " mkpart data 30GiB 100%
2026-02-01 05:42:05 +00:00
sleep 2
# Determine partition names
if [ [ " $TARGET_DISK " = = *nvme* ] ] ; then
2026-03-21 01:11:05 +00:00
BIOS_PART = " ${ TARGET_DISK } p1 "
EFI_PART = " ${ TARGET_DISK } p2 "
ROOT_PART = " ${ TARGET_DISK } p3 "
2026-03-26 09:12:16 +00:00
DATA_PART = " ${ TARGET_DISK } p4 "
2026-02-01 05:42:05 +00:00
else
2026-03-21 01:11:05 +00:00
BIOS_PART = " ${ TARGET_DISK } 1 "
EFI_PART = " ${ TARGET_DISK } 2 "
ROOT_PART = " ${ TARGET_DISK } 3 "
2026-03-26 09:12:16 +00:00
DATA_PART = " ${ TARGET_DISK } 4 "
2026-02-01 05:42:05 +00:00
fi
# Format partitions
2026-03-28 13:39:10 +00:00
step "Formatting partitions"
2026-03-25 18:25:01 +00:00
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
dd if = /dev/zero of = " $BIOS_PART " bs = 1M count = 1 2>/dev/null || true
2026-03-29 12:06:19 +01:00
run mkfs.vfat -F32 -n EFI " $EFI_PART "
run mkfs.ext4 -F -L archipelago " $ROOT_PART "
2026-02-01 05:42:05 +00:00
2026-03-26 09:12:16 +00:00
# Mount root + extract rootfs (need cryptsetup from rootfs for LUKS)
2026-03-28 13:39:10 +00:00
ok "Partitions created"
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE_DIM } Mounting filesystems... ${ NC } "
2026-02-01 05:42:05 +00:00
mkdir -p /mnt/target
mount " $ROOT_PART " /mnt/target
mkdir -p /mnt/target/boot/efi
mount " $EFI_PART " /mnt/target/boot/efi
2026-03-28 13:39:10 +00:00
step "Installing system"
2026-03-29 12:06:19 +01:00
run tar -xf " $ROOTFS_TAR " -C /mnt/target
2026-02-01 05:42:05 +00:00
2026-03-26 09:12:16 +00:00
# LUKS2 encryption for data partition
2026-03-28 13:39:10 +00:00
step "Encrypting data partition"
2026-03-26 09:12:16 +00:00
# Generate random 4KB key file
dd if = /dev/urandom of = /mnt/target/root/.luks-archipelago.key bs = 4096 count = 1 2>/dev/null
chmod 600 /mnt/target/root/.luks-archipelago.key
2026-03-26 17:28:08 +00:00
# Load dm_mod kernel module (required for device-mapper / LUKS)
modprobe dm_mod 2>/dev/null || true
modprobe dm_crypt 2>/dev/null || true
# Bind-mount /dev, /proc, /sys so cryptsetup works in chroot
2026-03-26 09:12:16 +00:00
mount --bind /dev /mnt/target/dev
2026-03-26 17:28:08 +00:00
mount --bind /proc /mnt/target/proc
mount --bind /sys /mnt/target/sys
2026-03-26 09:12:16 +00:00
# Detect AES-NI support for cipher selection
if grep -q aes /proc/cpuinfo 2>/dev/null; then
LUKS_CIPHER = "aes-xts-plain64"
echo " AES-NI detected — using AES-256-XTS"
else
LUKS_CIPHER = "xchacha20,aes-adiantum-plain64"
echo " No AES-NI — using ChaCha20-Adiantum"
fi
# Format LUKS2 partition with key file
2026-03-29 12:06:19 +01:00
run chroot /mnt/target cryptsetup luksFormat --type luks2 \
2026-03-26 09:12:16 +00:00
--key-file /root/.luks-archipelago.key \
--cipher " $LUKS_CIPHER " --key-size 512 \
--pbkdf argon2id --batch-mode \
" $DATA_PART "
# Open the LUKS volume
2026-03-29 12:06:19 +01:00
run chroot /mnt/target cryptsetup open --type luks2 \
2026-03-26 09:12:16 +00:00
--key-file /root/.luks-archipelago.key \
" $DATA_PART " archipelago-data
2026-03-26 17:28:08 +00:00
# Unmount chroot bind mounts (will be re-mounted later for grub-install)
2026-03-29 12:06:19 +01:00
runq umount /mnt/target/sys
runq umount /mnt/target/proc
runq umount /mnt/target/dev
2026-03-26 09:12:16 +00:00
# Format the inner filesystem
2026-03-29 12:06:19 +01:00
run mkfs.ext4 -F -L archipelago-data /dev/mapper/archipelago-data
2026-03-26 09:12:16 +00:00
# Mount encrypted partition
mkdir -p /mnt/target/var/lib/archipelago
mount /dev/mapper/archipelago-data /mnt/target/var/lib/archipelago
# Recreate directory structure on encrypted partition
mkdir -p /mnt/target/var/lib/archipelago/{ data,config,containers,secrets,tor,identities,lnd}
2026-03-29 15:43:57 +01:00
mkdir -p /mnt/target/var/lib/archipelago/containers/storage
2026-03-26 09:12:16 +00:00
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{ Documents,Photos,Music,Videos,Downloads}
chown -R 1000:1000 /mnt/target/var/lib/archipelago
echo " ✅ Data partition encrypted with LUKS2 ( $LUKS_CIPHER ) "
# Configure auto-unlock via crypttab (key file on root partition)
2026-03-28 13:39:10 +00:00
step "Configuring system"
2026-03-26 09:12:16 +00:00
DATA_UUID = $( blkid -s UUID -o value " $DATA_PART " )
echo "# LUKS2 encrypted data — auto-unlock with key file" > /mnt/target/etc/crypttab
echo " archipelago-data UUID= $DATA_UUID /root/.luks-archipelago.key luks,discard " >> /mnt/target/etc/crypttab
2026-02-01 05:42:05 +00:00
# Create fstab
cat > /mnt/target/etc/fstab <<EOF
# Archipelago Bitcoin Node OS
2026-03-26 09:12:16 +00:00
UUID = $( blkid -s UUID -o value " $ROOT_PART " ) / ext4 errors = remount-ro 0 1
UUID = $( blkid -s UUID -o value " $EFI_PART " ) /boot/efi vfat umask = 0077 0 1
/dev/mapper/archipelago-data /var/lib/archipelago ext4 defaults 0 2
2026-02-01 05:42:05 +00:00
EOF
# Configure hostname
echo "archipelago" > /mnt/target/etc/hostname
cat > /mnt/target/etc/hosts <<EOF
127.0.0.1 localhost
127.0.1.1 archipelago
::1 localhost ip6-localhost ip6-loopback
EOF
fix: container install flow, filebrowser auth, AppCard enrichment
- Fix .198-style fresh installs: systemd service ExecStartPre creates
/run/user/1000, enable podman.socket, chmod 644 /etc/hosts
- Filebrowser: add /data volume for database (fixes read-only crash),
secure auth with random password via backend RPC (no more admin/admin)
- AppCard: enrich installing state with marketplace metadata (icon,
title, description, tier badge, author, version)
- Registry: btcpayserver 1.13.5 → 1.13.7, images mirrored
- ReadWritePaths: add home container paths for rootless podman
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:32:54 +00:00
chmod 644 /mnt/target/etc/hosts
2026-02-01 05:42:05 +00:00
2026-03-27 17:17:18 +00:00
# Pre-create container storage dirs (ReadWritePaths needs these to exist)
mkdir -p /mnt/target/home/archipelago/.local/share/containers
2026-03-26 10:46:26 +00:00
mkdir -p /mnt/target/home/archipelago/.config/containers
2026-03-27 17:17:18 +00:00
chown -R 1000:1000 /mnt/target/home/archipelago/.local
2026-03-29 15:43:57 +01:00
# Redirect container storage to encrypted LUKS partition (not root filesystem)
# Without this, pulling images fills the 29GB root partition
cat > /mnt/target/home/archipelago/.config/containers/storage.conf <<'STORAGECONF'
[ storage]
driver = "overlay"
graphroot = "/var/lib/archipelago/containers/storage"
runroot = "/run/user/1000/containers"
STORAGECONF
# Symlink for backward compat (some tools look in ~/.local/share/containers)
ln -sf /var/lib/archipelago/containers/storage /mnt/target/home/archipelago/.local/share/containers/storage 2>/dev/null || true
2026-03-27 17:17:18 +00:00
# Configure Archipelago app registry (HTTP, insecure)
2026-03-26 10:46:26 +00:00
cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF'
[ [ registry] ]
location = "80.71.235.15:3000"
insecure = true
REGCONF
chown -R 1000:1000 /mnt/target/home/archipelago/.config
2026-03-30 18:57:17 +01:00
# Install netavark + aardvark-dns for container DNS resolution on archy-net.
# Debian 12's podman defaults to CNI which lacks DNS. Netavark provides built-in DNS.
2026-03-30 20:52:01 +01:00
# Binaries are embedded in the ISO at build time (archipelago/bin/).
if [ -f " $BOOT_MEDIA /archipelago/bin/netavark " ] && [ -f " $BOOT_MEDIA /archipelago/bin/aardvark-dns " ] ; then
2026-03-30 18:57:17 +01:00
mkdir -p /mnt/target/usr/lib/podman
2026-03-30 20:52:01 +01:00
cp " $BOOT_MEDIA /archipelago/bin/netavark " /mnt/target/usr/lib/podman/netavark
cp " $BOOT_MEDIA /archipelago/bin/aardvark-dns " /mnt/target/usr/lib/podman/aardvark-dns
2026-03-30 18:57:17 +01:00
chmod +x /mnt/target/usr/lib/podman/netavark /mnt/target/usr/lib/podman/aardvark-dns
# Configure podman to use netavark backend (enables container DNS)
mkdir -p /mnt/target/home/archipelago/.config/containers
cat > /mnt/target/home/archipelago/.config/containers/containers.conf <<'CONTAINERSCON F'
[ network]
network_backend = "netavark"
CONTAINERSCONF
chown -R 1000:1000 /mnt/target/home/archipelago/.config/containers
echo " Installed netavark + aardvark-dns (container DNS enabled)"
else
2026-03-30 20:52:01 +01:00
echo " WARNING: netavark/aardvark-dns not found in ISO — container DNS will not work"
2026-03-30 18:57:17 +01:00
fi
2026-03-26 15:58:16 +00:00
# Laptop support: ignore lid close so server keeps running
mkdir -p /mnt/target/etc/systemd/logind.conf.d
cat > /mnt/target/etc/systemd/logind.conf.d/lid-ignore.conf <<'LIDCONF'
[ Login]
HandleLidSwitch = ignore
HandleLidSwitchExternalPower = ignore
HandleLidSwitchDocked = ignore
LIDCONF
2026-02-01 05:42:05 +00:00
# Copy Archipelago binaries and files
if [ -d " $BOOT_MEDIA /archipelago/bin " ] ; then
cp -r " $BOOT_MEDIA /archipelago/bin/ " * /mnt/target/usr/local/bin/ 2>/dev/null || true
chmod +x /mnt/target/usr/local/bin/* 2>/dev/null || true
fi
if [ -d " $BOOT_MEDIA /archipelago/web-ui " ] ; then
cp -r " $BOOT_MEDIA /archipelago/web-ui " /mnt/target/opt/archipelago/
fi
if [ -d " $BOOT_MEDIA /archipelago/apps " ] ; then
cp -r " $BOOT_MEDIA /archipelago/apps " /mnt/target/etc/archipelago/
fi
# Copy pre-bundled container images
if [ -d " $BOOT_MEDIA /archipelago/container-images " ] ; then
echo " Copying container images (this may take a moment)..."
mkdir -p /mnt/target/opt/archipelago/container-images
cp -r " $BOOT_MEDIA /archipelago/container-images/ " *.tar /mnt/target/opt/archipelago/container-images/ 2>/dev/null || true
# Copy first-boot loader script and service
mkdir -p /mnt/target/opt/archipelago/scripts
if [ -f " $BOOT_MEDIA /archipelago/scripts/load-container-images.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/load-container-images.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/load-container-images.sh
fi
if [ -f " $BOOT_MEDIA /archipelago/scripts/archipelago-load-images.service " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/archipelago-load-images.service " /mnt/target/etc/systemd/system/
fi
2026-02-17 15:03:34 +00:00
if [ -f " $BOOT_MEDIA /archipelago/scripts/setup-tor.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/setup-tor.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/setup-tor.sh
fi
if [ -d " $BOOT_MEDIA /archipelago/scripts/tor " ] ; then
mkdir -p /mnt/target/opt/archipelago/scripts/tor
cp -r " $BOOT_MEDIA /archipelago/scripts/tor/ " * /mnt/target/opt/archipelago/scripts/tor/ 2>/dev/null || true
fi
if [ -f " $BOOT_MEDIA /archipelago/scripts/archipelago-setup-tor.service " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/archipelago-setup-tor.service " /mnt/target/etc/systemd/system/
fi
2026-03-21 03:06:29 +00:00
# Copy shared script library
if [ -d " $BOOT_MEDIA /archipelago/scripts/lib " ] ; then
mkdir -p /mnt/target/opt/archipelago/scripts/lib
cp -r " $BOOT_MEDIA /archipelago/scripts/lib/ " * /mnt/target/opt/archipelago/scripts/lib/ 2>/dev/null || true
fi
2026-02-25 18:04:41 +00:00
if [ -f " $BOOT_MEDIA /archipelago/scripts/first-boot-containers.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/first-boot-containers.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-containers.sh
fi
if [ -f " $BOOT_MEDIA /archipelago/scripts/archipelago-first-boot-containers.service " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/archipelago-first-boot-containers.service " /mnt/target/etc/systemd/system/
fi
2026-03-08 02:27:58 +00:00
# Copy docker UI source files for first-boot container builds
if [ -d " $BOOT_MEDIA /archipelago/docker " ] ; then
mkdir -p /mnt/target/opt/archipelago/docker
cp -r " $BOOT_MEDIA /archipelago/docker/ " * /mnt/target/opt/archipelago/docker/ 2>/dev/null || true
fi
2026-02-01 05:42:05 +00:00
echo " ✅ Container images staged for first-boot loading"
fi
2026-03-09 17:09:59 +00:00
# Initialize backend data directories for seamless first boot
mkdir -p /mnt/target/var/lib/archipelago/tor-config
mkdir -p /mnt/target/var/lib/archipelago/identities
mkdir -p /mnt/target/var/lib/archipelago/lnd
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Copy test scripts for post-install validation
for test_script in run-e2e-tests.sh run-post-install-tests.sh; do
if [ -f " $BOOT_MEDIA /archipelago/scripts/ $test_script " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/ $test_script " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/$test_script
fi
done
2026-03-09 17:09:59 +00:00
2026-03-25 15:52:26 +00:00
# Copy self-update script
if [ -f " $BOOT_MEDIA /archipelago/scripts/self-update.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/self-update.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/self-update.sh
# Also place in home for the update timer to find
mkdir -p /mnt/target/home/archipelago/archy/scripts
cp " $BOOT_MEDIA /archipelago/scripts/self-update.sh " /mnt/target/home/archipelago/archy/scripts/
chmod +x /mnt/target/home/archipelago/archy/scripts/self-update.sh
fi
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Copy image-versions.sh (needed by first-boot-containers and updates)
if [ -f " $BOOT_MEDIA /archipelago/scripts/image-versions.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/image-versions.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/image-versions.sh
# Also place in home for container scripts to find
mkdir -p /mnt/target/home/archipelago/archy/scripts
cp " $BOOT_MEDIA /archipelago/scripts/image-versions.sh " /mnt/target/home/archipelago/archy/scripts/
fi
2026-03-25 15:52:26 +00:00
# Clone repo for git-based updates (first-boot will have network)
# Create a script that runs on first boot to clone the repo
cat > /mnt/target/opt/archipelago/scripts/setup-git-updates.sh <<'GITSETUP'
#!/bin/bash
# Clone the Archipelago repo for git-based self-updates
REPO_DIR = "/home/archipelago/archy"
if [ -d " $REPO_DIR /.git " ] ; then
exit 0 # Already cloned
fi
echo "[update] Cloning Archipelago repo for self-updates..."
su - archipelago -c " git clone https://git.tx1138.com/lfg2025/archy $REPO_DIR " 2>/dev/null || {
echo "[update] Git clone failed (network?). Updates will retry on next boot."
exit 0
}
chown -R 1000:1000 " $REPO_DIR "
echo "[update] Repo cloned. Self-updates enabled."
GITSETUP
chmod +x /mnt/target/opt/archipelago/scripts/setup-git-updates.sh
2026-02-01 05:42:05 +00:00
# Ensure correct ownership (use numeric UID:GID 1000:1000 since we're outside chroot)
chown -R 1000:1000 /mnt/target/opt/archipelago 2>/dev/null || true
chown -R 1000:1000 /mnt/target/var/lib/archipelago 2>/dev/null || true
# Create welcome profile (nginx serves on port 80)
cat > /mnt/target/etc/profile.d/archipelago.sh <<'PROFILE'
#!/bin/bash
2026-03-21 01:11:05 +00:00
# Ensure /sbin and /usr/sbin are in PATH (needed for reboot, shutdown, etc.)
case " : $PATH : " in
*:/sbin:*) ; ; *) export PATH = " $PATH :/sbin:/usr/sbin " ; ;
esac
2026-02-01 05:42:05 +00:00
if [ -t 0 ] && [ -z " $ARCHIPELAGO_WELCOMED " ] ; then
export ARCHIPELAGO_WELCOMED = 1
2026-03-28 13:06:34 +00:00
# Wait for network (DHCP may not be ready yet on first boot)
IP = ""
for i in 1 2 3 4 5; do
IP = $( hostname -I 2>/dev/null | awk '{print $1}' )
[ -n " $IP " ] && break
sleep 2
done
2026-03-28 23:41:40 +00:00
O = '\033[38;5;208m'
2026-03-28 23:58:42 +00:00
OD = '\033[38;5;130m'
W = '\033[1;37m'
2026-03-28 23:41:40 +00:00
N = '\033[0m'
2026-03-28 13:06:34 +00:00
clear
2026-03-28 23:58:42 +00:00
echo -e " ${ O } ▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█ ${ N } "
echo -e " ${ O } █▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █ ${ N } "
echo -e " ${ O } ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀ ${ N } "
echo -e " ${ OD } bitcoin node os ${ N } "
2026-02-01 05:42:05 +00:00
if [ -n " $IP " ] ; then
2026-03-28 23:58:42 +00:00
echo -e " ${ W } web ui http:// $IP ${ N } "
echo -e " ${ W } ssh archipelago@ $IP ${ N } "
echo -e " ${ W } password archipelago (SSH) / password123 (Web) ${ N } "
2026-03-28 13:06:34 +00:00
else
2026-03-28 23:58:42 +00:00
echo -e " ${ OD } Waiting for network... ${ N } "
2026-02-01 05:42:05 +00:00
fi
2026-03-30 16:35:06 +01:00
if [ -b /dev/mapper/archipelago-data ] || [ -b /dev/mapper/archipelago_crypt ] ; then
2026-03-28 23:58:42 +00:00
echo -e " ${ OD } storage LUKS2 encrypted ${ N } "
2026-03-26 09:12:16 +00:00
fi
if systemctl is-active archipelago-kiosk.service >/dev/null 2>& 1; then
2026-03-28 23:58:42 +00:00
echo -e " ${ OD } display Kiosk active (Ctrl+Alt+F1 for terminal) ${ N } "
2026-03-26 09:12:16 +00:00
else
2026-03-28 23:58:42 +00:00
echo -e " ${ OD } display Console (Ctrl+Alt+F7 for kiosk) ${ N } "
2026-03-26 09:12:16 +00:00
fi
echo ""
2026-02-01 05:42:05 +00:00
fi
PROFILE
chmod +x /mnt/target/etc/profile.d/archipelago.sh
2026-03-30 23:47:15 +01:00
# Force UTF-8 console with Terminus font (supports Unicode block chars for ASCII logo)
cat > /mnt/target/etc/default/console-setup <<'CONSOLESETUP'
ACTIVE_CONSOLES = "/dev/tty[1-6]"
CHARMAP = "UTF-8"
CODESET = "Uni2"
FONTFACE = "Terminus"
FONTSIZE = "16"
CONSOLESETUP
2026-03-30 16:35:06 +01:00
# Suppress default Debian MOTD (our profile.d script handles the welcome)
echo -n > /mnt/target/etc/motd
rm -f /mnt/target/etc/motd.d/* 2>/dev/null || true
2026-03-29 14:25:13 +01:00
# Ensure reboot/shutdown work without sudo for the archipelago user
# profile.d only runs for login shells; .bashrc handles SSH interactive sessions
if ! grep -q '/sbin' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then
echo 'export PATH="$PATH:/sbin:/usr/sbin"' >> /mnt/target/home/archipelago/.bashrc
fi
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Systemd service: use the production version from rootfs (configs/archipelago.service)
# Do NOT overwrite — the rootfs already has the correct User=archipelago, no DEV_MODE version
if [ ! -f /mnt/target/etc/systemd/system/archipelago.service ] ; then
echo " WARNING: archipelago.service missing from rootfs — copying from ISO"
cp " $BOOT_MEDIA /archipelago/configs/archipelago.service " /mnt/target/etc/systemd/system/archipelago.service 2>/dev/null || true
fi
2026-02-01 05:42:05 +00:00
2026-03-12 22:19:04 +00:00
# Claude API proxy — middleware that injects max_tokens, strips invalid fields
# API key must be set after install via setup-aiui-server.sh or manually
cat > /mnt/target/opt/archipelago/claude-api-proxy.py <<'CLAUDEPROXY'
#!/usr/bin/env python3
import http.server, json, ssl, sys, os, urllib.request, urllib.error
API_KEY = os.environ.get( "ANTHROPIC_API_KEY" , "" )
PORT = 3142
class Handler( http.server.BaseHTTPRequestHandler) :
def do_POST( self) :
if self.path = = "/health" :
self.send_response( 200) ; self.send_header( "Content-Type" ,"application/json" ) ; self.end_headers( )
self.wfile.write( b'{"status":"ok"}' ) ; return
cl = int( self.headers.get( "Content-Length" , 0) )
body = self.rfile.read( cl)
try: data = json.loads( body)
except: data = { }
if "max_tokens" not in data: data[ "max_tokens" ] = 8096
for f in [ "webSearch" ,"web_search" ] : data.pop( f, None)
2026-04-02 11:01:58 +01:00
# Normalize model IDs — map short/dotted names to full API model IDs
MODEL_MAP = {
"claude-haiku-4.5" : "claude-haiku-4-5-20251001" ,
"claude-haiku-4-5" : "claude-haiku-4-5-20251001" ,
"claude-sonnet-4" : "claude-sonnet-4-20250514" ,
"claude-sonnet-4.5" : "claude-sonnet-4-5-20250514" ,
"claude-sonnet-4-5" : "claude-sonnet-4-5-20250514" ,
"claude-opus-4" : "claude-opus-4-20250514" ,
}
m = data.get( "model" , "" )
if m in MODEL_MAP: data[ "model" ] = MODEL_MAP[ m]
2026-03-12 22:19:04 +00:00
body = json.dumps( data) .encode( )
2026-04-02 18:30:34 +01:00
if not API_KEY:
err = json.dumps( { "type" :"error" ,"error" :{ "type" :"auth_error" ,"message" :"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat." } } ) .encode( )
self.send_response( 401) ; self.send_header( "Content-Type" ,"application/json" ) ; self.send_header( "Content-Length" ,str( len( err) ) ) ; self.end_headers( ) ; self.wfile.write( err) ; return
2026-03-12 22:19:04 +00:00
headers = { "Content-Type" :"application/json" ,"x-api-key" :API_KEY,"anthropic-version" :"2023-06-01" ,"anthropic-dangerous-direct-browser-access" :"true" }
for h in [ "anthropic-version" ,"anthropic-beta" ] :
if self.headers.get( h) : headers[ h] = self.headers[ h]
req = urllib.request.Request( "https://api.anthropic.com" +self.path, data = body, headers = headers, method = "POST" )
try:
ctx = ssl.create_default_context( )
resp = urllib.request.urlopen( req, context = ctx, timeout = 300)
self.send_response( resp.status)
is_stream = "text/event-stream" in ( resp.headers.get( "Content-Type" ,"" ) or "" )
for k,v in resp.headers.items( ) :
if k.lower( ) not in ( "transfer-encoding" ,"connection" ) : self.send_header( k,v)
if is_stream: self.send_header( "Transfer-Encoding" ,"chunked" )
self.end_headers( )
if is_stream:
while True:
chunk = resp.read( 4096)
if not chunk: break
self.wfile.write( b"%x\r\n" % len( chunk) ) ; self.wfile.write( chunk) ; self.wfile.write( b"\r\n" ) ; self.wfile.flush( )
self.wfile.write( b"0\r\n\r\n" ) ; self.wfile.flush( )
else : self.wfile.write( resp.read( ) )
except urllib.error.HTTPError as e:
self.send_response( e.code) ; self.send_header( "Content-Type" ,"application/json" ) ; self.end_headers( ) ; self.wfile.write( e.read( ) )
except Exception as e:
self.send_response( 502) ; self.send_header( "Content-Type" ,"application/json" ) ; self.end_headers( ) ; self.wfile.write( json.dumps( { "error" :str( e) } ) .encode( ) )
def do_GET( self) :
if self.path = = "/health" :
self.send_response( 200) ; self.send_header( "Content-Type" ,"application/json" ) ; self.end_headers( ) ; self.wfile.write( b'{"status":"ok"}' )
else : self.send_response( 404) ; self.end_headers( )
def log_message( self, fmt, *args) : pass
2026-04-02 18:30:34 +01:00
if not API_KEY: print( "WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions" )
2026-03-12 22:19:04 +00:00
server = http.server.HTTPServer( ( "127.0.0.1" , PORT) , Handler)
print( f"Claude API proxy on port {PORT}" )
server.serve_forever( )
CLAUDEPROXY
chmod +x /mnt/target/opt/archipelago/claude-api-proxy.py
# Claude API proxy systemd service (disabled by default — enabled after API key is configured)
cat > /mnt/target/etc/systemd/system/claude-api-proxy.service <<'CLAUDESVC '
[ Unit]
Description = Claude API Proxy
After = network.target
[ Service]
Type = simple
2026-03-31 05:01:13 +01:00
Environment = ANTHROPIC_API_KEY = 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
2026-03-12 22:19:04 +00:00
ExecStart = /usr/bin/python3 /opt/archipelago/claude-api-proxy.py
Restart = always
RestartSec = 5
[ Install]
WantedBy = multi-user.target
CLAUDESVC
2026-03-26 09:12:16 +00:00
# Kiosk mode — X11 + Chromium fullscreen on attached display
# Not enabled by default; toggle via: sudo archipelago-kiosk enable/disable
cat > /mnt/target/usr/local/bin/archipelago-kiosk-launcher <<'KIOSK LAUNCHER'
#!/bin/bash
2026-03-26 21:16:07 +00:00
# Start X server on VT7 (VT1 stays on MOTD/console)
/usr/bin/Xorg :0 vt7 -nolisten tcp -keeptty &
2026-03-26 09:12:16 +00:00
XPID = $!
2026-03-26 21:16:07 +00:00
sleep 3
# Switch to kiosk display
chvt 7 2>/dev/null || true
2026-03-26 09:12:16 +00:00
if ! kill -0 $XPID 2>/dev/null; then
echo 'ERROR: Xorg failed to start'
exit 1
fi
export DISPLAY = :0
export HOME = /home/archipelago
xhost +SI:localuser:archipelago 2>/dev/null
xset s off 2>/dev/null
xset -dpms 2>/dev/null
xset s noblank 2>/dev/null
unclutter -idle 3 -root &
while true; do
2026-03-26 18:30:13 +00:00
# Get screen resolution for window sizing
SCREEN_RES = $( xdpyinfo 2>/dev/null | awk '/dimensions:/{print $2}' )
SCREEN_RES = ${ SCREEN_RES :- 1920x1080 }
2026-03-26 09:12:16 +00:00
sudo -u archipelago env DISPLAY = :0 HOME = /home/archipelago chromium \
--kiosk \
2026-03-26 18:30:13 +00:00
--start-fullscreen \
--start-maximized \
--window-position= 0,0 \
--window-size= ${ SCREEN_RES /x/, } \
2026-03-26 09:12:16 +00:00
--app= http://localhost/kiosk \
--noerrdialogs \
--disable-infobars \
--disable-translate \
--no-first-run \
--check-for-update-interval= 31536000 \
2026-03-30 16:35:06 +01:00
--disable-features= TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \
2026-03-26 09:12:16 +00:00
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
2026-03-26 21:16:07 +00:00
--password-store= basic \
2026-03-26 09:12:16 +00:00
--disable-component-update \
2026-03-27 18:19:49 +00:00
--credentials_enable_service= false \
2026-03-30 16:35:06 +01:00
--disable-gpu \
2026-03-31 23:43:32 +01:00
--disable-breakpad \
--disable-metrics \
--disable-metrics-reporting \
--metrics-recording-only \
--disable-domain-reliability \
--disable-background-networking \
--disable-background-timer-throttling \
--disable-backgrounding-occluded-windows \
2026-04-02 11:10:08 +01:00
--user-data-dir= /var/lib/archipelago/chromium-kiosk
2026-03-26 09:12:16 +00:00
sleep 3
done
kill $XPID 2>/dev/null
KIOSKLAUNCHER
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk-launcher
cat > /mnt/target/etc/systemd/system/archipelago-kiosk.service <<'KIOSK SVC'
[ Unit]
Description = Archipelago Kiosk ( X11 + Chromium)
After = archipelago.service
Wants = archipelago.service
ConditionPathExists = /usr/local/bin/archipelago-kiosk-launcher
[ Service]
Type = simple
2026-03-30 16:35:06 +01:00
ExecStartPre = /bin/bash -c 'for i in $(seq 1 30); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
2026-03-26 09:12:16 +00:00
ExecStart = /usr/local/bin/archipelago-kiosk-launcher
2026-03-30 16:35:06 +01:00
TimeoutStartSec = 90
2026-03-26 09:12:16 +00:00
Restart = always
RestartSec = 5
[ Install]
WantedBy = multi-user.target
KIOSKSVC
# Toggle script: sudo archipelago-kiosk enable|disable|status
cat > /mnt/target/usr/local/bin/archipelago-kiosk <<'KIOSK TOGGLE'
#!/bin/bash
set -e
case " ${ 1 :- status } " in
enable )
echo "Enabling kiosk mode (X11 + Chromium on display)..."
systemctl enable archipelago-kiosk.service
systemctl start archipelago-kiosk.service 2>/dev/null || true
echo "Kiosk mode ENABLED. Console login (tty1) is now disabled."
echo "To access the server, use SSH or the web UI."
; ;
disable)
echo "Disabling kiosk mode (restoring console login)..."
systemctl stop archipelago-kiosk.service 2>/dev/null || true
systemctl disable archipelago-kiosk.service
systemctl restart getty@tty1.service 2>/dev/null || true
echo "Kiosk mode DISABLED. Console login restored on tty1."
; ;
status)
if systemctl is-active archipelago-kiosk.service >/dev/null 2>& 1; then
echo "Kiosk mode: ACTIVE (display showing web UI)"
elif systemctl is-enabled archipelago-kiosk.service >/dev/null 2>& 1; then
echo "Kiosk mode: ENABLED (will start on next boot)"
else
echo "Kiosk mode: DISABLED (console login on tty1)"
fi
; ;
2026-03-26 18:30:13 +00:00
toggle)
if systemctl is-active archipelago-kiosk.service >/dev/null 2>& 1; then
systemctl stop archipelago-kiosk.service 2>/dev/null || true
systemctl restart getty@tty1.service 2>/dev/null || true
chvt 1 2>/dev/null || true
else
systemctl start archipelago-kiosk.service 2>/dev/null || true
fi
; ;
2026-03-26 09:12:16 +00:00
*)
2026-03-26 18:30:13 +00:00
echo "Usage: archipelago-kiosk [enable|disable|status|toggle]"
2026-03-26 09:12:16 +00:00
echo " enable — Start kiosk (fullscreen web UI on display)"
echo " disable — Stop kiosk, restore console login"
2026-03-26 18:30:13 +00:00
echo " toggle — Switch between kiosk and terminal"
2026-03-26 09:12:16 +00:00
echo " status — Show current mode"
2026-03-26 18:30:13 +00:00
echo ""
echo "Keyboard shortcuts (from terminal):"
echo " Ctrl+Alt+F7 — Switch to kiosk display"
echo " Ctrl+Alt+F1 — Switch to terminal"
2026-03-26 09:12:16 +00:00
exit 1
; ;
esac
KIOSKTOGGLE
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk
2026-02-01 05:42:05 +00:00
# Install GRUB
2026-03-28 13:39:10 +00:00
step "Installing bootloader"
2026-02-01 05:42:05 +00:00
mount --bind /dev /mnt/target/dev
mount --bind /dev/pts /mnt/target/dev/pts
mount --bind /proc /mnt/target/proc
mount --bind /sys /mnt/target/sys
mount --bind /run /mnt/target/run
2026-03-10 23:29:05 +00:00
# Set passwords reliably by directly editing /etc/shadow
# chpasswd fails silently in chroot due to missing PAM — use sed instead
echo " Setting user passwords..."
# Pre-computed SHA-512 hash of "archipelago"
ARCHY_HASH = '$6$archipelago.salt1$QpB5VPzGHOKRVKQ5cTfd4R7PYqmMH5MUx6MxFN7MbZkxWKR3WxFp.RV4tBVbJiv.6iWXfHeq3vDph7G.XfPz0'
# Generate hash at install time if openssl is available, otherwise use pre-computed
if command -v openssl >/dev/null 2>& 1; then
ARCHY_HASH = $( openssl passwd -6 -salt "archy.install" "archipelago" )
fi
# Direct shadow file manipulation — works without PAM
sed -i " s|^archipelago:[^:]*:|archipelago: ${ ARCHY_HASH } :| " /mnt/target/etc/shadow
sed -i " s|^root:[^:]*:|root: ${ ARCHY_HASH } :| " /mnt/target/etc/shadow
# Verify the password was set (not locked/empty)
if grep -q "^archipelago:[!*]" /mnt/target/etc/shadow 2>/dev/null; then
echo " WARNING: Password still locked, trying chpasswd fallback..."
chroot /mnt/target bash -c 'echo "archipelago:archipelago" | chpasswd' 2>/dev/null || true
fi
echo " Passwords set for archipelago and root users"
2026-03-26 17:31:26 +00:00
# Remove shim-signed before grub-install to prevent hooks re-creating shim files
chroot /mnt/target dpkg --purge shim-signed shim-helpers-amd64-signed shim-helpers-arm64-signed 2>/dev/null || true
2026-03-21 01:11:05 +00:00
# UEFI boot: install to fallback path (/EFI/BOOT/BOOTX64.EFI) for maximum compatibility
echo " Installing UEFI bootloader..."
2026-03-29 12:06:19 +01:00
if run chroot /mnt/target grub-install --target= ${ GRUB_TARGET } --efi-directory= /boot/efi --bootloader-id= archipelago --removable; then
ok "UEFI bootloader installed (removable/fallback path)"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
warn "UEFI removable install failed, trying standard..."
if run chroot /mnt/target grub-install --target= ${ GRUB_TARGET } --efi-directory= /boot/efi --bootloader-id= archipelago; then
ok "UEFI bootloader installed (standard)"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
fail "UEFI bootloader installation failed"
2026-03-21 01:11:05 +00:00
fi
fi
2026-03-26 09:12:16 +00:00
# EFI boot: grub-install --removable places unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
# No shim chain — Secure Boot must be disabled. shim-signed was removed from rootfs
# because it installs BOOTX64.CSV + shimx64.efi which cause "Failed to open \EFI\BOOT\"
# errors with garbled filenames on every boot.
2026-03-25 20:47:14 +00:00
echo " Verifying EFI boot files..."
EFI_BOOT_DIR = "/mnt/target/boot/efi/EFI/BOOT"
2026-03-21 01:11:05 +00:00
if [ " $ARCH " = "x86_64" ] ; then
EFI_BOOT_BINARY = "BOOTX64.EFI"
else
EFI_BOOT_BINARY = "BOOTAA64.EFI"
fi
2026-03-26 09:12:16 +00:00
# Remove any residual shim chain files (from grub-efi-*-signed package hooks)
# These cause firmware to try loading garbled vendor paths before falling back
for shim_file in shimx64.efi mmx64.efi fbx64.efi BOOTX64.CSV shimaa64.efi mmaa64.efi fbaa64.efi BOOTAA64.CSV; do
if [ -f " $EFI_BOOT_DIR / $shim_file " ] && [ " $shim_file " != " $EFI_BOOT_BINARY " ] ; then
rm -f " $EFI_BOOT_DIR / $shim_file "
echo " Removed shim artifact: $shim_file "
fi
done
# Also remove vendor-specific EFI directory (shim creates /EFI/archipelago/)
rm -rf "/mnt/target/boot/efi/EFI/archipelago" 2>/dev/null || true
2026-03-26 17:31:26 +00:00
# Nuclear cleanup: remove everything except the GRUB binary from EFI/BOOT
if [ -d " $EFI_BOOT_DIR " ] ; then
for f in " $EFI_BOOT_DIR " /*; do
[ " $( basename " $f " ) " = " $EFI_BOOT_BINARY " ] && continue
[ " $( basename " $f " ) " = "grub.cfg" ] && continue
rm -f " $f " 2>/dev/null && echo " Removed: $( basename " $f " ) "
done
fi
2026-03-25 20:47:14 +00:00
if [ -f " $EFI_BOOT_DIR / $EFI_BOOT_BINARY " ] ; then
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR / $EFI_BOOT_BINARY "
2026-03-25 19:25:55 +00:00
ls -la " $EFI_BOOT_DIR / "
2026-03-21 01:11:05 +00:00
else
2026-03-25 20:47:14 +00:00
echo " ❌ Missing $EFI_BOOT_DIR / $EFI_BOOT_BINARY — boot will fail! "
2026-03-21 01:11:05 +00:00
fi
# Legacy BIOS boot: only install if the installer booted in Legacy BIOS mode
# (if /sys/firmware/efi exists, the machine supports UEFI — no need for BIOS fallback)
if [ -n " ${ GRUB_BIOS_TARGET } " ] && [ ! -d /sys/firmware/efi ] ; then
echo " Installing Legacy BIOS bootloader (machine booted in BIOS mode)..."
2026-03-29 12:06:19 +01:00
if run chroot /mnt/target grub-install --target= ${ GRUB_BIOS_TARGET } " ${ TARGET_DISK } " ; then
ok "Legacy BIOS bootloader installed"
2026-03-21 01:11:05 +00:00
else
2026-03-29 12:06:19 +01:00
warn "Legacy BIOS bootloader failed (UEFI-only boot)"
2026-03-21 01:11:05 +00:00
fi
elif [ -n " ${ GRUB_BIOS_TARGET } " ] ; then
echo " Skipping Legacy BIOS bootloader (machine supports UEFI)"
fi
2026-03-27 18:19:49 +00:00
# Clean any stale live-boot artifacts (should not exist in the custom rootfs,
# but clean up defensively in case Docker base image pulled them in)
2026-03-25 16:51:14 +00:00
rm -f /mnt/target/etc/initramfs-tools/conf.d/live-boot* 2>/dev/null || true
rm -f /mnt/target/usr/share/initramfs-tools/scripts/live* 2>/dev/null || true
rm -f /mnt/target/usr/share/initramfs-tools/hooks/live* 2>/dev/null || true
2026-03-25 16:56:02 +00:00
# Suppress os-prober warning in GRUB
echo "GRUB_DISABLE_OS_PROBER=true" >> /mnt/target/etc/default/grub
2026-04-02 20:28:53 +01:00
# GFX fallback for hardware without graphical GRUB support
echo 'GRUB_GFXMODE=auto' >> /mnt/target/etc/default/grub
echo 'GRUB_GFXPAYLOAD_LINUX=keep' >> /mnt/target/etc/default/grub
echo 'GRUB_TERMINAL_OUTPUT=gfxterm' >> /mnt/target/etc/default/grub
2026-03-25 16:56:02 +00:00
2026-03-27 18:19:49 +00:00
# Install Archipelago GRUB theme on target system
if [ -d " $BOOT_MEDIA /boot/grub/themes/archipelago " ] ; then
mkdir -p /mnt/target/boot/grub/themes/archipelago
cp " $BOOT_MEDIA /boot/grub/themes/archipelago/ " * /mnt/target/boot/grub/themes/archipelago/
echo 'GRUB_THEME="/boot/grub/themes/archipelago/theme.txt"' >> /mnt/target/etc/default/grub
echo " Installed Archipelago GRUB theme on target"
fi
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Install Archipelago Plymouth theme on target system
if [ -d " $BOOT_MEDIA /archipelago/plymouth-theme " ] ; then
PLYMOUTH_DIR = "/mnt/target/usr/share/plymouth/themes/archipelago"
mkdir -p " $PLYMOUTH_DIR "
cp " $BOOT_MEDIA /archipelago/plymouth-theme/ " * " $PLYMOUTH_DIR / "
# Set as default Plymouth theme
chroot /mnt/target plymouth-set-default-theme archipelago 2>/dev/null || \
ln -sf /usr/share/plymouth/themes/archipelago/archipelago.plymouth \
/mnt/target/etc/alternatives/default.plymouth 2>/dev/null || true
2026-04-08 13:15:09 +02:00
# Enable splash and ACPI in GRUB
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
if ! grep -q "splash" /mnt/target/etc/default/grub; then
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 splash"/' \
/mnt/target/etc/default/grub 2>/dev/null || true
fi
2026-04-08 13:15:09 +02:00
if ! grep -q "acpi=force" /mnt/target/etc/default/grub; then
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 acpi=force"/' \
/mnt/target/etc/default/grub 2>/dev/null || true
fi
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
echo " Installed Archipelago Plymouth theme on target"
fi
2026-03-21 01:11:05 +00:00
# Regenerate initramfs — the one from Docker export is corrupt/incomplete
# (Docker builds have limited /proc, /sys, /dev so initramfs generation fails silently)
echo " Regenerating initramfs..."
2026-03-29 12:06:19 +01:00
run chroot /mnt/target update-initramfs -u -k all
2026-02-01 05:42:05 +00:00
2026-03-29 12:06:19 +01:00
run chroot /mnt/target update-grub
2026-02-01 05:42:05 +00:00
2026-03-28 13:39:10 +00:00
# CRITICAL: Write EFI grub.cfg that finds the root filesystem and loads the full config.
# grub-install --removable creates a BOOTX64.EFI that looks for grub.cfg on the
# EFI FAT partition (/EFI/BOOT/grub.cfg). This stub must search for the root FS
# and then load the full /boot/grub/grub.cfg from ext4.
ROOT_UUID = $( blkid -s UUID -o value " $ROOT_PART " )
if [ -n " $ROOT_UUID " ] && [ -d "/mnt/target/boot/efi/EFI/BOOT" ] ; then
cat > /mnt/target/boot/efi/EFI/BOOT/grub.cfg <<EFICFG
search.fs_uuid $ROOT_UUID root
set prefix = ( \$ root) /boot/grub
configfile \$ prefix/grub.cfg
EFICFG
echo " Wrote EFI grub.cfg (root UUID= $ROOT_UUID ) "
else
echo " WARNING: Could not write EFI grub.cfg (UUID= $ROOT_UUID ) "
fi
2026-03-18 10:50:13 +00:00
# Install udev rule for mesh radio stable naming (/dev/mesh-radio)
2026-03-27 18:19:49 +00:00
MESH_RULES = ""
for p in " $BOOT_MEDIA /99-mesh-radio.rules " /cdrom/99-mesh-radio.rules " $BOOT_MEDIA /archipelago/configs/99-mesh-radio.rules " ; do
[ -f " $p " ] && MESH_RULES = " $p " && break
done
if [ -n " $MESH_RULES " ] ; then
cp " $MESH_RULES " /mnt/target/etc/udev/rules.d/99-mesh-radio.rules
2026-03-18 10:50:13 +00:00
echo " Installed mesh radio udev rule"
fi
2026-03-26 20:58:34 +00:00
# First-boot diagnostics — runs once, captures system state for debugging
cat > /mnt/target/usr/local/bin/archipelago-diagnostics <<'DIAG'
#!/bin/bash
LOG = "/var/log/archipelago-first-boot-diagnostics.log"
echo "=== Archipelago First Boot Diagnostics ===" > " $LOG "
echo " Date: $( date -u) " >> " $LOG "
echo " Kernel: $( uname -r) " >> " $LOG "
echo " Hostname: $( hostname) " >> " $LOG "
echo " IP: $( hostname -I 2>/dev/null) " >> " $LOG "
echo "" >> " $LOG "
echo "=== Disk ===" >> " $LOG "
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE >> " $LOG " 2>& 1
df -h >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== LUKS ===" >> " $LOG "
ls -la /dev/mapper/archipelago-data 2>& 1 >> " $LOG "
cryptsetup status archipelago-data >> " $LOG " 2>& 1 || echo "No LUKS" >> " $LOG "
echo "" >> " $LOG "
echo "=== Services ===" >> " $LOG "
for svc in nginx archipelago archipelago-kiosk archipelago-load-images \
archipelago-first-boot-containers archipelago-setup-tor \
console-setup; do
STATUS = $( systemctl is-active " $svc " 2>/dev/null || echo "inactive" )
ENABLED = $( systemctl is-enabled " $svc " 2>/dev/null || echo "disabled" )
printf " %-40s %s / %s\n" " $svc " " $STATUS " " $ENABLED " >> " $LOG "
done
echo "" >> " $LOG "
echo "=== Failed Services ===" >> " $LOG "
systemctl --failed --no-pager >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== Nginx ===" >> " $LOG "
nginx -t >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== EFI Boot ===" >> " $LOG "
ls -laR /boot/efi/EFI/ >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== SSL Cert ===" >> " $LOG "
ls -la /etc/archipelago/ssl/ >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== Podman ===" >> " $LOG "
su - archipelago -c "podman ps -a --format '{{.Names}} {{.Status}}'" >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== Memory ===" >> " $LOG "
free -h >> " $LOG " 2>& 1
echo "" >> " $LOG "
echo "=== Journal Errors (last 50) ===" >> " $LOG "
journalctl -p err --no-pager -n 50 >> " $LOG " 2>& 1
echo " Diagnostics saved to $LOG "
DIAG
chmod +x /mnt/target/usr/local/bin/archipelago-diagnostics
cat > /mnt/target/etc/systemd/system/archipelago-diagnostics.service <<'DIAGSVC'
[ Unit]
Description = Archipelago First Boot Diagnostics
After = multi-user.target archipelago.service nginx.service
ConditionPathExists = !/var/log/archipelago-first-boot-diagnostics.log
[ Service]
Type = oneshot
ExecStartPre = /bin/sleep 30
ExecStart = /usr/local/bin/archipelago-diagnostics
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
DIAGSVC
2026-03-26 18:30:13 +00:00
# Ensure SSL cert exists for nginx HTTPS (safety net if rootfs build missed it)
if [ ! -f /mnt/target/etc/archipelago/ssl/archipelago.crt ] ; then
mkdir -p /mnt/target/etc/archipelago/ssl
chroot /mnt/target openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/archipelago/ssl/archipelago.key \
-out /etc/archipelago/ssl/archipelago.crt \
-subj "/C=XX/ST=Bitcoin/L=Node/O=Archipelago/CN=archipelago" 2>/dev/null
chmod 600 /mnt/target/etc/archipelago/ssl/archipelago.key
echo " Generated self-signed SSL certificate"
fi
fix: production onboarding, CI tests, container security, keyboard nav
Install & Onboarding:
- Remove DEV_MODE=true from production ISO service file (auto-created
users, skipped password setup)
- Auto-install no longer overwrites rootfs service file with bad template
- Login.vue always checks auth.isSetup — shows password creation form
on fresh install without requiring dev build flag
- Deploy image-versions.sh to /opt/archipelago/scripts/ on installed nodes
- First-boot-containers sources image-versions.sh, runs podman as
archipelago user (rootless), enables linger + podman.socket
- Correct volume ownership (100000:100000 for rootless UID mapping)
Container Security:
- FileBrowser: add --cap-add=DAC_OVERRIDE for rootless podman volume access
- FileBrowser: add --read-only, /data volume for database, proper cmd args
- First-boot script matches backend config (security hardening + health check)
CI Pipeline:
- Add vue-tsc type check + vitest run to build-iso.yml (runs every push)
- Add post-install-tests.yml workflow (workflow_dispatch, SSH to target)
- Build report: set +eo pipefail, fix rootfs path, add || true guards
- Bundle run-post-install-tests.sh into ISO
E2E Test Suite (scripts/run-post-install-tests.sh):
- Phase 1: Install verification (files, services, podman, linger, DEV_MODE check)
- Phase 2: Onboarding flow (auth.isSetup, auth.setup, login, DID, complete)
- Phase 3: Container lifecycle (install 3 apps via package.install RPC,
verify running, stop, verify stopped, restart, verify running, health)
- Phase 4: Log verification (first-boot log, diagnostics, journal errors)
- Correct package.install params: {"id", "dockerImage"}
Frontend:
- Fix backdrop-filter tab-switch bug (keep animations paused during rebuild)
- Dashboard glitch animations paused during tab-hidden
- Gamepad nav: auto-focus first container on route change
- Tab roving: Left/Right on role="tab" cycles and activates sibling tabs
- ContainerApps: data-controller-launch on running app cards
- 515 tests passing (fixed 30 broken, added 19 new keyboard nav tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:16:57 +00:00
# Enable linger for rootless podman (containers survive logout)
mkdir -p /mnt/target/var/lib/systemd/linger
touch /mnt/target/var/lib/systemd/linger/archipelago
# Enable podman socket for archipelago user (activated on first login/boot)
mkdir -p /mnt/target/home/archipelago/.config/systemd/user/sockets.target.wants
ln -sf /usr/lib/systemd/user/podman.socket /mnt/target/home/archipelago/.config/systemd/user/sockets.target.wants/podman.socket 2>/dev/null || true
chown -R 1000:1000 /mnt/target/home/archipelago/.config 2>/dev/null || true
# Ensure /run/user/1000 is created at boot for podman socket
mkdir -p /mnt/target/etc/tmpfiles.d
echo 'd /run/user/1000 0700 archipelago archipelago -' > /mnt/target/etc/tmpfiles.d/archipelago-runtime.conf
2026-04-02 16:15:04 +01:00
# Bootstrap switchover — checks when local Bitcoin finishes IBD and switches services
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.service <<'BSSERVICE'
[ Unit]
Description = Switch Bitcoin-dependent services from bootstrap to local node
After = archipelago-first-boot-containers.service
[ Service]
Type = oneshot
User = archipelago
ExecStart = /opt/archipelago/scripts/bootstrap-switchover.sh
BSSERVICE
cat > /mnt/target/etc/systemd/system/archipelago-bootstrap-switchover.timer <<'BSTIMER'
[ Unit]
Description = Periodically check if local Bitcoin is synced and switch from bootstrap
[ Timer]
OnBootSec = 10min
OnUnitActiveSec = 5min
Persistent = true
[ Install]
WantedBy = timers.target
BSTIMER
# Copy bootstrap config to install target
if [ -f " $BOOT_MEDIA /archipelago/bootstrap.conf " ] ; then
cp " $BOOT_MEDIA /archipelago/bootstrap.conf " /mnt/target/opt/archipelago/bootstrap.conf
chmod 600 /mnt/target/opt/archipelago/bootstrap.conf
chown root:root /mnt/target/opt/archipelago/bootstrap.conf
fi
# Copy bootstrap switchover script
if [ -f " $BOOT_MEDIA /archipelago/scripts/bootstrap-switchover.sh " ] ; then
cp " $BOOT_MEDIA /archipelago/scripts/bootstrap-switchover.sh " /mnt/target/opt/archipelago/scripts/
chmod +x /mnt/target/opt/archipelago/scripts/bootstrap-switchover.sh
fi
2026-02-01 05:42:05 +00:00
# Enable services
2026-04-02 16:15:04 +01:00
chroot /mnt/target systemctl enable archipelago-bootstrap-switchover.timer 2>/dev/null || true
2026-02-01 05:42:05 +00:00
chroot /mnt/target systemctl enable archipelago.service 2>/dev/null || true
chroot /mnt/target systemctl enable nginx.service 2>/dev/null || true
chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null || true
2026-02-17 15:03:34 +00:00
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
2026-02-25 18:04:41 +00:00
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
2026-03-26 19:50:59 +00:00
chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true
2026-04-02 01:28:11 +01:00
# Enable claude-api-proxy (create symlink manually — chroot systemctl can fail)
chroot /mnt/target systemctl enable claude-api-proxy.service 2>/dev/null || \
ln -sf /etc/systemd/system/claude-api-proxy.service /mnt/target/etc/systemd/system/multi-user.target.wants/claude-api-proxy.service 2>/dev/null || true
2026-03-27 11:12:31 +00:00
2026-03-27 17:17:18 +00:00
# Fix console-setup: setupcon needs /tmp writable, add ordering dependency
mkdir -p /mnt/target/etc/systemd/system/console-setup.service.d
cat > /mnt/target/etc/systemd/system/console-setup.service.d/fix-tmp.conf <<'CONSOLEFIX'
[ Unit]
After = tmp.mount systemd-tmpfiles-setup.service
Wants = tmp.mount
[ Service]
ExecStartPre = /bin/mkdir -p /tmp
CONSOLEFIX
2026-03-27 11:12:31 +00:00
# Auto-login on tty1 — no password prompt on console
mkdir -p /mnt/target/etc/systemd/system/getty@tty1.service.d
cat > /mnt/target/etc/systemd/system/getty@tty1.service.d/autologin.conf <<'AUTOLOGIN'
[ Service]
ExecStart =
ExecStart = -/sbin/agetty --autologin archipelago --noclear %I $TERM
AUTOLOGIN
2026-03-26 20:58:34 +00:00
chroot /mnt/target systemctl enable archipelago-diagnostics.service 2>/dev/null || true
2026-02-01 05:42:05 +00:00
2026-03-27 17:17:18 +00:00
# Post-install smoke test — runs Phase 1 (install verification) only on first boot
# Does NOT run onboarding or create passwords — lets user do that via the UI
2026-03-27 16:19:33 +00:00
cat > /mnt/target/etc/systemd/system/archipelago-post-install-tests.service <<'PITSERVICE'
[ Unit]
2026-03-27 17:17:18 +00:00
Description = Archipelago Install Verification ( first boot)
2026-03-27 16:19:33 +00:00
After = archipelago.service archipelago-first-boot-containers.service nginx.service
Wants = archipelago.service nginx.service
ConditionPathExists = !/var/lib/archipelago/.post-install-tests-done
[ Service]
Type = oneshot
ExecStartPre = /bin/bash -c 'for i in $(seq 1 30); do curl -sf http://127.0.0.1:5678/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
2026-03-27 17:17:18 +00:00
ExecStart = /bin/bash -c '/opt/archipelago/scripts/run-post-install-tests.sh --phase1-only 2>&1 | tee /var/log/archipelago-post-install-tests.log; touch /var/lib/archipelago/.post-install-tests-done'
2026-03-27 16:19:33 +00:00
RemainAfterExit = yes
StandardOutput = journal+console
StandardError = journal+console
2026-03-27 17:17:18 +00:00
TimeoutStartSec = 120
2026-03-27 16:19:33 +00:00
[ Install]
WantedBy = multi-user.target
PITSERVICE
chroot /mnt/target systemctl enable archipelago-post-install-tests.service 2>/dev/null || true
feat: add build report and first-boot diagnostics
CI build report: checks rootfs contents (nginx, SSL, keyboard, kiosk,
lid config, backend, frontend) and ISO contents after build. Reports
in the Actions log so build issues are immediately visible.
First-boot diagnostics: one-shot systemd service runs 30s after first
boot, logs service status, nginx test, SSL certs, LUKS, podman,
kiosk, console-setup, disk, network, and journal errors to
/var/log/archipelago-first-boot-diag.log. Only runs once (ConditionPathExists).
SSH in and cat the log to debug any fresh install issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:54:32 +00:00
# Install first-boot diagnostic script — runs once after first boot and logs system state
cat > /mnt/target/opt/archipelago/scripts/first-boot-diag.sh <<'DIAGSCRIPT'
#!/bin/bash
LOG = "/var/log/archipelago-first-boot-diag.log"
exec > " $LOG " 2>& 1
echo "=== Archipelago First Boot Diagnostics ==="
echo " Date: $( date -u) "
echo " Hostname: $( hostname) "
echo " Kernel: $( uname -r) "
echo " IP: $( hostname -I 2>/dev/null | awk '{print $1}' ) "
echo ""
echo "=== Build Info ==="
cat /opt/archipelago/build-info.txt 2>/dev/null || echo "No build-info.txt"
echo ""
echo "=== Services ==="
for svc in nginx archipelago archipelago-kiosk archipelago-load-images archipelago-first-boot-containers; do
STATUS = $( systemctl is-active " $svc " 2>/dev/null || echo "missing" )
ENABLED = $( systemctl is-enabled " $svc " 2>/dev/null || echo "missing" )
printf " %-45s active=%-10s enabled=%s\n" " $svc " " $STATUS " " $ENABLED "
done
echo ""
echo "=== Nginx Test ==="
nginx -t 2>& 1
echo ""
echo "=== SSL Cert ==="
ls -la /etc/archipelago/ssl/ 2>/dev/null || echo " No SSL directory"
echo ""
echo "=== EFI Boot ==="
ls -la /boot/efi/EFI/BOOT/ 2>/dev/null || echo " No EFI/BOOT directory"
echo ""
echo "=== LUKS ==="
ls -la /dev/mapper/archipelago-data 2>/dev/null && echo " LUKS volume open" || echo " No LUKS volume"
cat /etc/crypttab 2>/dev/null
echo ""
echo "=== Podman ==="
podman --version 2>/dev/null || echo " podman not found"
podman ps -a --format "{{.Names}} {{.Status}}" 2>/dev/null | head -20
echo ""
echo "=== Kiosk ==="
systemctl status archipelago-kiosk --no-pager 2>& 1 | head -10
echo ""
echo "=== Console Setup ==="
systemctl status console-setup --no-pager 2>& 1 | head -5
cat /etc/default/keyboard 2>/dev/null || echo " No keyboard config"
echo ""
echo "=== Logind (Lid) ==="
cat /etc/systemd/logind.conf.d/lid-ignore.conf 2>/dev/null || echo " No lid config"
echo ""
echo "=== Disk ==="
df -h / /boot/efi /var/lib/archipelago 2>/dev/null
echo ""
echo "=== Network ==="
ip addr show | grep -E "inet |link/" | head -10
echo ""
echo "=== Journal Errors (last boot) ==="
journalctl -b -p err --no-pager 2>/dev/null | tail -30
echo ""
echo "=== Done ==="
DIAGSCRIPT
chmod +x /mnt/target/opt/archipelago/scripts/first-boot-diag.sh
# Systemd oneshot service for first-boot diagnostics
cat > /mnt/target/etc/systemd/system/archipelago-diag.service <<'DIAGSVC'
[ Unit]
Description = Archipelago First Boot Diagnostics
After = multi-user.target archipelago.service nginx.service
ConditionPathExists = !/var/log/archipelago-first-boot-diag.log
[ Service]
Type = oneshot
ExecStartPre = /bin/sleep 30
ExecStart = /opt/archipelago/scripts/first-boot-diag.sh
RemainAfterExit = yes
[ Install]
WantedBy = multi-user.target
DIAGSVC
chroot /mnt/target systemctl enable archipelago-diag.service 2>/dev/null || true
# Write build info into the installed system
cat > /mnt/target/opt/archipelago/build-info.txt <<BUILDINFO
2026-03-31 23:43:32 +01:00
version = __BUILD_VERSION__
build = __BUILD_NUM__
commit = __GIT_SHORT__
feat: add build report and first-boot diagnostics
CI build report: checks rootfs contents (nginx, SSL, keyboard, kiosk,
lid config, backend, frontend) and ISO contents after build. Reports
in the Actions log so build issues are immediately visible.
First-boot diagnostics: one-shot systemd service runs 30s after first
boot, logs service status, nginx test, SSL certs, LUKS, podman,
kiosk, console-setup, disk, network, and journal errors to
/var/log/archipelago-first-boot-diag.log. Only runs once (ConditionPathExists).
SSH in and cat the log to debug any fresh install issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:54:32 +00:00
date = $( date -u '+%Y-%m-%d %H:%M:%S UTC' )
type = unbundled
BUILDINFO
2026-03-28 11:31:48 +00:00
# Save install log BEFORE unmounting target
cp " $INSTALL_LOG " /mnt/target/var/log/archipelago-install.log 2>/dev/null || true
2026-02-01 05:42:05 +00:00
# Cleanup
sync
umount /mnt/target/run 2>/dev/null || true
umount /mnt/target/sys 2>/dev/null || true
umount /mnt/target/proc 2>/dev/null || true
umount /mnt/target/dev/pts 2>/dev/null || true
umount /mnt/target/dev 2>/dev/null || true
umount /mnt/target/boot/efi 2>/dev/null || true
2026-03-26 09:12:16 +00:00
umount /mnt/target/var/lib/archipelago 2>/dev/null || true
cryptsetup close archipelago-data 2>/dev/null || true
2026-02-01 05:42:05 +00:00
umount /mnt/target 2>/dev/null || true
2026-02-14 16:44:20 +00:00
echo ""
2026-03-29 15:43:57 +01:00
hrule
2026-02-14 16:44:20 +00:00
echo ""
2026-03-31 21:23:29 +01:00
# Celebration animation if TUI available
[ " ${ TUI_AVAILABLE :- } " = "1" ] && tui_complete
2026-03-28 23:58:42 +00:00
p " ${ ORANGE_BRIGHT } ✓ Installation Complete ${ NC } "
2026-02-14 16:44:20 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE_DIM } After reboot, access from any device: ${ NC } "
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p " ${ ORANGE } http://<this machine's IP> ${ NC } "
2026-02-01 05:42:05 +00:00
echo ""
2026-03-28 23:58:42 +00:00
p " ${ WHITE } SSH ssh archipelago@<IP> ${ NC } "
p " ${ WHITE } Password archipelago ${ NC } "
p " ${ WHITE } Web Login password123 ${ NC } "
2026-03-25 16:56:02 +00:00
echo ""
2026-03-28 11:31:48 +00:00
hrule
2026-03-25 16:56:02 +00:00
echo ""
2026-03-28 13:14:33 +00:00
# Suppress kernel messages on console (SquashFS errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
feat: add build report and first-boot diagnostics
CI build report: checks rootfs contents (nginx, SSL, keyboard, kiosk,
lid config, backend, frontend) and ISO contents after build. Reports
in the Actions log so build issues are immediately visible.
First-boot diagnostics: one-shot systemd service runs 30s after first
boot, logs service status, nginx test, SSL certs, LUKS, podman,
kiosk, console-setup, disk, network, and journal errors to
/var/log/archipelago-first-boot-diag.log. Only runs once (ConditionPathExists).
SSH in and cat the log to debug any fresh install issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:54:32 +00:00
fix: CSRF 403 blocking all operations + reboot after install
CSRF fix (THE BLOCKER):
- After remember-me session restore, the browser has a stale CSRF
cookie but a new session token. ALL subsequent RPC calls return 403.
- Fix: exempt read-only polling methods (node-messages-received,
server.echo, system.stats, tor.status, etc.) from CSRF validation.
CSRF still protects state-changing operations (install, uninstall,
start, stop, restart, settings changes).
Reboot fix:
- The separate /tmp/archipelago-reboot.sh approach failed because
/bin/bash is on the squashfs which gets unmounted when USB is pulled.
- Fix: do everything inline in the installer script — show message,
unmount USB, wait for Enter, then reboot. Use sysrq-trigger first
(kernel-level, doesn't need userspace binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:42:09 +01:00
# Show completion message, unmount USB, then reboot
# All done inline — no separate script needed (avoids /bin/bash dependency on squashfs)
2026-03-28 23:41:40 +00:00
echo ""
2026-03-31 21:23:29 +01:00
if [ " ${ TUI_AVAILABLE :- } " = "1" ] ; then
tui_flash_remove_usb
else
p " ${ ORANGE } >>> REMOVE THE USB DRIVE NOW <<< ${ NC } "
fi
2026-03-28 23:41:40 +00:00
echo ""
fix: CSRF 403 blocking all operations + reboot after install
CSRF fix (THE BLOCKER):
- After remember-me session restore, the browser has a stale CSRF
cookie but a new session token. ALL subsequent RPC calls return 403.
- Fix: exempt read-only polling methods (node-messages-received,
server.echo, system.stats, tor.status, etc.) from CSRF validation.
CSRF still protects state-changing operations (install, uninstall,
start, stop, restart, settings changes).
Reboot fix:
- The separate /tmp/archipelago-reboot.sh approach failed because
/bin/bash is on the squashfs which gets unmounted when USB is pulled.
- Fix: do everything inline in the installer script — show message,
unmount USB, wait for Enter, then reboot. Use sysrq-trigger first
(kernel-level, doesn't need userspace binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:42:09 +01:00
p " ${ ORANGE_DIM } Press Enter to reboot (or wait 30 seconds) ${ NC } "
# Suppress kernel messages (squashfs errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
2026-03-28 23:41:40 +00:00
fix: CSRF 403 blocking all operations + reboot after install
CSRF fix (THE BLOCKER):
- After remember-me session restore, the browser has a stale CSRF
cookie but a new session token. ALL subsequent RPC calls return 403.
- Fix: exempt read-only polling methods (node-messages-received,
server.echo, system.stats, tor.status, etc.) from CSRF validation.
CSRF still protects state-changing operations (install, uninstall,
start, stop, restart, settings changes).
Reboot fix:
- The separate /tmp/archipelago-reboot.sh approach failed because
/bin/bash is on the squashfs which gets unmounted when USB is pulled.
- Fix: do everything inline in the installer script — show message,
unmount USB, wait for Enter, then reboot. Use sysrq-trigger first
(kernel-level, doesn't need userspace binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:42:09 +01:00
# Lazy-unmount live filesystem
2026-03-26 18:30:13 +00:00
exec 2>/dev/null
2026-03-28 13:14:33 +00:00
umount -l /run/live/medium 2>/dev/null || true
umount -l /lib/live/mount/medium 2>/dev/null || true
umount -l /run/archiso 2>/dev/null || true
umount -l /cdrom 2>/dev/null || true
BOOT_DEV = $( findmnt -n -o SOURCE /run/live/medium 2>/dev/null || findmnt -n -o SOURCE /cdrom 2>/dev/null || echo "" )
2026-03-25 16:56:02 +00:00
if [ -n " $BOOT_DEV " ] ; then
BOOT_DISK = $( lsblk -no PKNAME " $BOOT_DEV " 2>/dev/null | head -1)
2026-03-28 13:14:33 +00:00
[ -n " $BOOT_DISK " ] && eject " /dev/ $BOOT_DISK " 2>/dev/null || true
2026-03-25 16:56:02 +00:00
fi
2026-03-28 13:06:34 +00:00
exec 2>& 1
fix: CSRF 403 blocking all operations + reboot after install
CSRF fix (THE BLOCKER):
- After remember-me session restore, the browser has a stale CSRF
cookie but a new session token. ALL subsequent RPC calls return 403.
- Fix: exempt read-only polling methods (node-messages-received,
server.echo, system.stats, tor.status, etc.) from CSRF validation.
CSRF still protects state-changing operations (install, uninstall,
start, stop, restart, settings changes).
Reboot fix:
- The separate /tmp/archipelago-reboot.sh approach failed because
/bin/bash is on the squashfs which gets unmounted when USB is pulled.
- Fix: do everything inline in the installer script — show message,
unmount USB, wait for Enter, then reboot. Use sysrq-trigger first
(kernel-level, doesn't need userspace binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:42:09 +01:00
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
echo ""
p " ${ ORANGE_DIM } Rebooting... ${ NC } "
sleep 1
# Force reboot — multiple methods, first one that works wins
echo b > /proc/sysrq-trigger 2>/dev/null || \
/sbin/reboot -f 2>/dev/null || \
/usr/sbin/reboot -f 2>/dev/null || \
kill -9 1 2>/dev/null
2026-02-01 05:42:05 +00:00
INSTALLER_SCRIPT
2026-03-31 23:43:32 +01:00
# Inject build version into auto-install.sh (heredoc is single-quoted, can't expand variables)
sed -i " s|__BUILD_VERSION__| ${ BUILD_VERSION } |g " " $ARCH_DIR /auto-install.sh "
sed -i " s|__BUILD_NUM__| ${ BUILD_NUM } |g " " $ARCH_DIR /auto-install.sh "
sed -i " s|__GIT_SHORT__| ${ GIT_SHORT } |g " " $ARCH_DIR /auto-install.sh "
2026-03-10 23:29:05 +00:00
# For unbundled builds, patch the completion message to reflect no pre-loaded apps
if [ " $UNBUNDLED " = "1" ] ; then
sed -i 's/Pre-loaded apps (ready to start via Web UI):/Install apps from the Marketplace (internet required):/' " $ARCH_DIR /auto-install.sh "
sed -i 's/• Bitcoin Knots • LND • Home Assistant/ Open the Web UI → Marketplace → Install any app/' " $ARCH_DIR /auto-install.sh "
sed -i 's/• BTCPay Server • Mempool • Nostr Relays/ All apps download automatically via Podman /' " $ARCH_DIR /auto-install.sh "
fi
2026-02-01 05:42:05 +00:00
chmod +x " $ARCH_DIR /auto-install.sh "
# =============================================================================
2026-03-27 18:19:49 +00:00
# STEP 5: Configure boot loader and ISO structure
2026-02-01 05:42:05 +00:00
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 5: Configuring boot loaders..."
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
# The installer squashfs (from Step 2) already contains:
# - systemd service for auto-starting the installer
# - auto-login on tty1
# - custom initramfs hook for mounting boot media at /run/archiso
# - all partitioning tools (parted, mkfs.*, cryptsetup)
#
# Step 5 just needs to create the GRUB and ISOLINUX boot configs.
# Create GRUB configuration
echo " Writing GRUB config..."
cat > " $INSTALLER_ISO /boot/grub/grub.cfg " <<'GRUBCFG '
2026-03-27 23:33:31 +00:00
insmod part_gpt
insmod part_msdos
insmod fat
insmod iso9660
insmod all_video
2026-03-28 23:58:42 +00:00
insmod search
insmod search_label
insmod search_fs_file
2026-03-27 23:33:31 +00:00
2026-03-28 23:58:42 +00:00
# Find boot media — try label first, then known file fallback
2026-03-27 23:33:31 +00:00
search --no-floppy --set= root --label ARCHIPELAGO
2026-03-28 23:58:42 +00:00
if [ -z " $root " ] ; then
search --no-floppy --set= root --file /archipelago/auto-install.sh
fi
2026-03-27 23:33:31 +00:00
2026-03-27 18:19:49 +00:00
set timeout = 5
set default = 0
2026-04-02 20:28:53 +01:00
# Load font for graphical menu — fallback to text mode on hardware without gfxterm
2026-03-27 23:33:31 +00:00
if loadfont ( $root ) /boot/grub/font.pf2; then
2026-03-30 16:35:06 +01:00
set gfxmode = auto
2026-04-02 20:28:53 +01:00
set gfxpayload = keep
2026-03-27 18:19:49 +00:00
insmod gfxterm
insmod png
terminal_output gfxterm
2026-04-02 20:28:53 +01:00
else
terminal_output console
2026-03-27 18:19:49 +00:00
fi
# Archipelago GRUB theme
2026-03-27 23:33:31 +00:00
if [ -f ( $root ) /boot/grub/themes/archipelago/theme.txt ] ; then
loadfont ( $root ) /boot/grub/themes/archipelago/dejavu_12.pf2
loadfont ( $root ) /boot/grub/themes/archipelago/dejavu_14.pf2
loadfont ( $root ) /boot/grub/themes/archipelago/dejavu_16.pf2
loadfont ( $root ) /boot/grub/themes/archipelago/dejavu_24.pf2
set theme = ( $root ) /boot/grub/themes/archipelago/theme.txt
2026-02-01 05:42:05 +00:00
else
2026-03-27 18:19:49 +00:00
set menu_color_normal = light-gray/black
set menu_color_highlight = white/dark-gray
2026-02-01 05:42:05 +00:00
fi
2026-03-27 18:19:49 +00:00
menuentry "Install Archipelago" --hotkey= i {
2026-04-08 13:15:09 +02:00
linux ( $root ) /live/vmlinuz boot = live components quiet splash loglevel = 0 rd.systemd.show_status= false vt.global_cursor_default= 0 acpi = force
2026-03-27 23:33:31 +00:00
initrd ( $root ) /live/initrd.img
2026-03-27 18:19:49 +00:00
}
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
menuentry "Install Archipelago (verbose)" --hotkey= v {
2026-04-08 13:15:09 +02:00
linux ( $root ) /live/vmlinuz boot = live components loglevel = 4 console = ttyS0,115200 console = tty0 acpi = force
2026-03-27 23:33:31 +00:00
initrd ( $root ) /live/initrd.img
2026-03-27 18:19:49 +00:00
}
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
menuentry "Boot from local disk" --hotkey= b {
set root = ( hd0)
chainloader +1
}
GRUBCFG
2026-02-01 05:42:05 +00:00
2026-03-27 23:33:31 +00:00
# Copy grub.cfg to EFI/BOOT on ISO filesystem AND into the FAT EFI image
# The embedded grub bootstrap does configfile "${cmdpath}/grub.cfg"
cp " $INSTALLER_ISO /boot/grub/grub.cfg " " $INSTALLER_ISO /EFI/BOOT/grub.cfg "
if [ -f " $WORK_DIR /efi.img " ] ; then
mcopy -oi " $WORK_DIR /efi.img " " $INSTALLER_ISO /boot/grub/grub.cfg " ::/EFI/BOOT/grub.cfg 2>/dev/null || \
echo " WARNING: Could not copy grub.cfg into efi.img (mtools required)"
fi
2026-03-27 18:19:49 +00:00
# Create ISOLINUX configuration (legacy BIOS boot)
echo " Writing ISOLINUX config..."
2026-03-28 03:15:47 +00:00
# Copy background image for ISOLINUX graphical menu
ISOLINUX_BG = " $SCRIPT_DIR /branding/grub-theme/background.png "
if [ -f " $ISOLINUX_BG " ] ; then
cp " $ISOLINUX_BG " " $INSTALLER_ISO /isolinux/splash.png "
fi
# Copy vesamenu.c32 for graphical menu (with background support)
if [ -f " $WORK_DIR /vesamenu.c32 " ] ; then
cp " $WORK_DIR /vesamenu.c32 " " $INSTALLER_ISO /isolinux/vesamenu.c32 "
fi
2026-03-27 18:19:49 +00:00
cat > " $INSTALLER_ISO /isolinux/isolinux.cfg " <<'ISOCFG'
2026-03-28 03:15:47 +00:00
UI vesamenu.c32
2026-03-27 18:19:49 +00:00
PROMPT 0
2026-03-28 03:15:47 +00:00
TIMEOUT 0
2026-02-01 05:42:05 +00:00
fix: container security hardening, onboarding viewport scaling, boot screen cleanup
Container security:
- Add --cap-drop ALL + --security-opt no-new-privileges:true to 12 containers
missing hardening in first-boot-containers.sh (mempool-db, electrumx,
mempool-api, mempool-web, electrs-ui, btcpay-db, nbxplorer, nostr-rs-relay,
strfry, tailscale, bitcoin-ui, lnd-ui)
- Mirror same hardening in deploy-to-target.sh for consistency
- Add --read-only + tmpfs to nostr-rs-relay
- Fix filebrowser deploy to include security flags
- Remove duplicate UI image definitions in image-versions.sh
- Separate Jellyfin capabilities (needs FOWNER, exec tmpfs for CoreCLR JIT)
- Harden archy-net creation with existence check and error handling
UI fixes:
- Fix onboarding viewport scaling: all 7 screens now use h-full + max-h-full
pattern so containers never overflow viewport regardless of padding
- Remove path-option-card wrappers from seed verify inputs, left-justify labels
- Remove batteries/barbarian icons from boot screen (keep bitcoin, cloud, github, save)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:35:34 +01:00
MENU TITLE
2026-03-28 03:15:47 +00:00
MENU BACKGROUND splash.png
2026-03-28 13:14:33 +00:00
MENU RESOLUTION 1024 768
fix: container security hardening, onboarding viewport scaling, boot screen cleanup
Container security:
- Add --cap-drop ALL + --security-opt no-new-privileges:true to 12 containers
missing hardening in first-boot-containers.sh (mempool-db, electrumx,
mempool-api, mempool-web, electrs-ui, btcpay-db, nbxplorer, nostr-rs-relay,
strfry, tailscale, bitcoin-ui, lnd-ui)
- Mirror same hardening in deploy-to-target.sh for consistency
- Add --read-only + tmpfs to nostr-rs-relay
- Fix filebrowser deploy to include security flags
- Remove duplicate UI image definitions in image-versions.sh
- Separate Jellyfin capabilities (needs FOWNER, exec tmpfs for CoreCLR JIT)
- Harden archy-net creation with existence check and error handling
UI fixes:
- Fix onboarding viewport scaling: all 7 screens now use h-full + max-h-full
pattern so containers never overflow viewport regardless of padding
- Remove path-option-card wrappers from seed verify inputs, left-justify labels
- Remove batteries/barbarian icons from boot screen (keep bitcoin, cloud, github, save)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:35:34 +01:00
MENU VSHIFT 20
MENU HSHIFT 6
MENU WIDTH 68
MENU MARGIN 2
2026-03-28 11:31:48 +00:00
MENU ROWS 5
2026-03-28 13:06:34 +00:00
MENU TABMSG press tab to edit | archipelago.sh
2026-03-28 03:15:47 +00:00
MENU COLOR screen 37; 40 #00000000 #00000000 none
2026-03-28 00:35:34 +00:00
MENU COLOR border 30; 40 #00000000 #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR title 1; 37; 40 #80888888 #00000000 none
MENU COLOR sel 7; 37; 40 #ffffffff #c0181818 std
MENU COLOR unsel 37; 40 #ffaaaaaa #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR hotkey 1; 37; 40 #fffb923c #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR hotsel 1; 37; 40 #fffb923c #c0181818 std
MENU COLOR timeout_msg 37; 40 #ff555555 #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR timeout 1; 37; 40 #fffb923c #00000000 none
2026-03-28 11:31:48 +00:00
MENU COLOR tabmsg 37; 40 #ff444444 #00000000 none
2026-03-28 03:15:47 +00:00
MENU COLOR cmdmark 37; 40 #00000000 #00000000 none
MENU COLOR cmdline 37; 40 #00000000 #00000000 none
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
DEFAULT install
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
LABEL install
MENU LABEL Install Archipelago
KERNEL /live/vmlinuz
2026-04-08 13:15:09 +02:00
APPEND initrd = /live/initrd.img boot = live components quiet loglevel = 0 rd.systemd.show_status= false vt.global_cursor_default= 0 acpi = force
2026-03-27 18:19:49 +00:00
MENU DEFAULT
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
LABEL install-verbose
2026-03-28 11:31:48 +00:00
MENU LABEL Install ( verbose output)
2026-03-27 18:19:49 +00:00
KERNEL /live/vmlinuz
2026-04-08 13:15:09 +02:00
APPEND initrd = /live/initrd.img boot = live components loglevel = 4 acpi = force
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
LABEL local
MENU LABEL Boot from local disk
LOCALBOOT 0x80
ISOCFG
2026-02-01 05:42:05 +00:00
2026-03-27 18:19:49 +00:00
echo " Step 5 complete (GRUB + ISOLINUX configured)"
2026-02-01 05:42:05 +00:00
# =============================================================================
# STEP 6: Create final ISO
# =============================================================================
echo ""
2026-03-27 18:19:49 +00:00
echo "Step 6: Creating bootable ISO..."
2026-02-01 05:42:05 +00:00
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-03-12 00:19:30 +00:00
OUTPUT_ISO = " $OUTPUT_DIR /archipelago-installer-unbundled- ${ ARCH } .iso "
2026-03-10 23:29:05 +00:00
else
2026-03-12 00:19:30 +00:00
OUTPUT_ISO = " $OUTPUT_DIR /archipelago-installer- ${ ARCH } .iso "
2026-03-10 23:29:05 +00:00
fi
2026-02-01 05:42:05 +00:00
feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests
Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
the root cause of "machine doesn't pick up the USB".
Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging
CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
initrd has live-boot, confirms boot=live in grub.cfg. Fails build
before copying to Builds if any check fails.
Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
(~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging
Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:56 +00:00
# Use the proven MBR code for hybrid USB boot
# The ISOLINUX package's isohdpfx.bin (33 ed) doesn't boot on all hardware.
# We ship the Debian Live MBR (45 52) which is known to work with Balena Etcher.
ISOHDPFX = " $SCRIPT_DIR /branding/isohdpfx.bin "
if [ ! -f " $ISOHDPFX " ] ; then
ISOHDPFX = " $WORK_DIR /isohdpfx.bin "
fi
2026-03-27 18:19:49 +00:00
if [ ! -f " $ISOHDPFX " ] ; then
# Fallback to system-installed copy
2026-03-09 17:09:59 +00:00
for path in \
"/usr/lib/ISOLINUX/isohdpfx.bin" \
"/usr/share/syslinux/isohdpfx.bin" \
"/usr/local/share/syslinux/isohdpfx.bin" ; do
if [ -f " $path " ] ; then
ISOHDPFX = " $path "
2026-03-27 18:19:49 +00:00
echo " Using system isohdpfx.bin: $path "
2026-03-09 17:09:59 +00:00
break
fi
done
fi
2026-03-28 00:35:34 +00:00
# EFI boot image — embedded inside ISO (same approach as the working main ISO)
# The efi.img must be copied into the ISO directory in Step 2 artifact placement
EFI_IMG = " $INSTALLER_ISO /boot/grub/efi.img "
2026-02-01 05:42:05 +00:00
2026-03-28 00:35:34 +00:00
if [ ! -f " $EFI_IMG " ] ; then
2026-03-27 18:19:49 +00:00
echo " WARNING: No EFI boot image — ISO will only support Legacy BIOS boot"
2026-03-09 17:09:59 +00:00
xorriso -as mkisofs -o " $OUTPUT_ISO " \
-volid "ARCHIPELAGO" \
-iso-level 3 \
2026-03-28 00:35:34 +00:00
-J -joliet-long -R \
2026-03-09 17:09:59 +00:00
-isohybrid-mbr " $ISOHDPFX " \
2026-03-28 00:35:34 +00:00
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-partition_offset 16 \
2026-03-09 17:09:59 +00:00
" $INSTALLER_ISO "
else
2026-03-29 12:44:13 +01:00
# UEFI fix: append efi.img as a real EFI System Partition (ESP) in GPT
# instead of embedding it as "basic data". Strict UEFI firmware requires
# the correct ESP type GUID (C12A7328-F81F-11D2-BA4B-00A0C93EC93B).
# This is the same approach used by Arch Linux ISOs.
2026-03-09 17:09:59 +00:00
xorriso -as mkisofs -o " $OUTPUT_ISO " \
-volid "ARCHIPELAGO" \
-iso-level 3 \
2026-03-28 00:35:34 +00:00
-J -joliet-long -R \
2026-03-09 17:09:59 +00:00
-isohybrid-mbr " $ISOHDPFX " \
2026-03-28 00:35:34 +00:00
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
2026-03-09 17:09:59 +00:00
-eltorito-alt-boot \
2026-03-29 12:44:13 +01:00
-e --interval:appended_partition_2:all:: \
2026-03-28 00:35:34 +00:00
-no-emul-boot \
2026-03-29 12:44:13 +01:00
-appended_part_as_gpt \
-append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B " $WORK_DIR /efi.img " \
2026-03-28 00:35:34 +00:00
-partition_offset 16 \
2026-03-09 17:09:59 +00:00
" $INSTALLER_ISO "
fi
2026-02-01 05:42:05 +00:00
echo ""
2026-03-10 23:29:05 +00:00
if [ " $UNBUNDLED " = "1" ] ; then
2026-03-27 18:19:49 +00:00
echo "UNBUNDLED AUTO-INSTALLER ISO CREATED"
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Output: $OUTPUT_ISO "
echo " Size: $( du -h " $OUTPUT_ISO " | cut -f1) "
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Lightweight installer -- apps downloaded on-demand from Marketplace"
2026-03-10 23:29:05 +00:00
else
2026-03-27 18:19:49 +00:00
echo "AUTO-INSTALLER ISO CREATED"
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Output: $OUTPUT_ISO "
echo " Size: $( du -h " $OUTPUT_ISO " | cut -f1) "
2026-03-10 23:29:05 +00:00
echo ""
2026-03-27 18:19:49 +00:00
echo " Full installer with pre-bundled container apps"
2026-03-10 23:29:05 +00:00
fi
2026-02-01 05:42:05 +00:00
echo "To create USB:"
echo " 1. Flash with: sudo dd if= $OUTPUT_ISO of=/dev/rdiskX bs=4m "
echo " Or use Balena Etcher"
echo " 2. Boot from USB"
echo " 3. Press Enter to install"
echo " 4. Remove USB and reboot"
echo ""