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>
This commit is contained in:
Dorian 2026-03-31 17:35:34 +01:00
parent 56151e26e7
commit 843d778f90
14 changed files with 160 additions and 51 deletions

View File

@ -59,13 +59,21 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
"--cap-add=NET_RAW".to_string(), "--cap-add=NET_RAW".to_string(),
], ],
"nextcloud" | "btcpay-server" | "btcpayserver" "nextcloud" | "btcpay-server" | "btcpayserver"
| "jellyfin" | "onlyoffice" | "onlyoffice-documentserver" | "portainer" => vec![ | "onlyoffice" | "onlyoffice-documentserver" | "portainer" => vec![
"--cap-add=CHOWN".to_string(), "--cap-add=CHOWN".to_string(),
"--cap-add=SETUID".to_string(), "--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(), "--cap-add=SETGID".to_string(),
"--cap-add=DAC_OVERRIDE".to_string(), "--cap-add=DAC_OVERRIDE".to_string(),
"--cap-add=NET_BIND_SERVICE".to_string(), "--cap-add=NET_BIND_SERVICE".to_string(),
], ],
// Jellyfin: CoreCLR needs exec-enabled tmpfs for JIT compilation
"jellyfin" => vec![
"--cap-add=CHOWN".to_string(),
"--cap-add=FOWNER".to_string(),
"--cap-add=SETUID".to_string(),
"--cap-add=SETGID".to_string(),
"--cap-add=DAC_OVERRIDE".to_string(),
],
// Nginx Proxy Manager needs to bind low ports // Nginx Proxy Manager needs to bind low ports
"nginx-proxy-manager" => vec![ "nginx-proxy-manager" => vec![
"--cap-add=CHOWN".to_string(), "--cap-add=CHOWN".to_string(),

View File

@ -158,11 +158,37 @@ impl RpcHandler {
run_args.push("--cap-add=NET_RAW"); run_args.push("--cap-add=NET_RAW");
run_args.push("--device=/dev/net/tun"); run_args.push("--device=/dev/net/tun");
} else if needs_archy_net(package_id) { } else if needs_archy_net(package_id) {
let _ = tokio::process::Command::new("podman") // Create archy-net if it doesn't exist (idempotent — "already exists" is fine)
match tokio::process::Command::new("podman")
.args(["network", "create", "archy-net"]) .args(["network", "create", "archy-net"])
.output() .output()
.await
{
Ok(output) if !output.status.success() => {
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.contains("already exists") {
tracing::warn!("Failed to create archy-net: {}", stderr.trim());
}
}
Err(e) => {
tracing::warn!("Failed to run podman network create: {}", e);
}
_ => {}
}
// Verify the network actually exists before using it
let net_check = tokio::process::Command::new("podman")
.args(["network", "exists", "archy-net"])
.status()
.await; .await;
run_args.push("--network=archy-net"); if net_check.map(|s| s.success()).unwrap_or(false) {
run_args.push("--network=archy-net");
} else {
tracing::error!(
"archy-net network does not exist — {} will use default network. \
Inter-container DNS will not work.",
package_id
);
}
} }
// Security hardening (skip for privileged containers) // Security hardening (skip for privileged containers)
@ -184,6 +210,11 @@ impl RpcHandler {
run_args.push("--tmpfs=/tmp:rw,noexec,nosuid,size=256m"); run_args.push("--tmpfs=/tmp:rw,noexec,nosuid,size=256m");
run_args.push("--tmpfs=/run:rw,noexec,nosuid,size=64m"); run_args.push("--tmpfs=/run:rw,noexec,nosuid,size=64m");
} }
// Jellyfin: .NET CoreCLR needs exec-enabled /tmp for JIT compilation
if package_id == "jellyfin" {
run_args.push("--tmpfs=/tmp:rw,exec,size=256m");
}
} }
// Create data directories // Create data directories

View File

@ -767,9 +767,9 @@ cp /usr/lib/ISOLINUX/isohdpfx.bin /output/isohdpfx.bin
echo " [container] Generating GRUB fonts..." echo " [container] Generating GRUB fonts..."
apt-get install -y -qq fonts-dejavu-core grub-common >/dev/null 2>&1 apt-get install -y -qq fonts-dejavu-core grub-common >/dev/null 2>&1
mkdir -p /output/grub-fonts mkdir -p /output/grub-fonts
grub-mkfont -s 12 -o /output/grub-fonts/dejavu_12.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf 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.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.ttf grub-mkfont -s 16 -o /output/grub-fonts/dejavu_16.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
grub-mkfont -s 24 -o /output/grub-fonts/dejavu_24.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf grub-mkfont -s 24 -o /output/grub-fonts/dejavu_24.pf2 /usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf
echo " [container] Done!" echo " [container] Done!"
@ -2817,13 +2817,13 @@ UI vesamenu.c32
PROMPT 0 PROMPT 0
TIMEOUT 0 TIMEOUT 0
MENU TITLE bitcoin node os MENU TITLE
MENU BACKGROUND splash.png MENU BACKGROUND splash.png
MENU RESOLUTION 1024 768 MENU RESOLUTION 1024 768
MENU VSHIFT 18 MENU VSHIFT 20
MENU HSHIFT 32 MENU HSHIFT 6
MENU WIDTH 18 MENU WIDTH 68
MENU MARGIN 1 MENU MARGIN 2
MENU ROWS 5 MENU ROWS 5
MENU TABMSG press tab to edit | archipelago.sh MENU TABMSG press tab to edit | archipelago.sh
MENU COLOR screen 37;40 #00000000 #00000000 none MENU COLOR screen 37;40 #00000000 #00000000 none

View File

@ -81,8 +81,6 @@ const iconSources = [
'/assets/icon/cloud-done.svg', '/assets/icon/cloud-done.svg',
'/assets/icon/github.svg', '/assets/icon/github.svg',
'/assets/icon/save.svg', '/assets/icon/save.svg',
'/assets/icon/batteries.svg',
'/assets/icon/barbarian.svg',
] ]
interface LogLine { prefix: string; text: string; type: string } interface LogLine { prefix: string; text: string; type: string }

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<!-- Main Glass Container - Scrollable --> <!-- Main Glass Container - Scrollable -->
<div class="max-w-[800px] w-full relative z-10 path-glass-container onb-scroll-container"> <div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Success Content --> <!-- Success Content -->
<div class="text-center space-y-4 sm:space-y-6 px-3 sm:px-4 py-4 sm:py-6"> <div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0 text-center space-y-4 sm:space-y-6 px-3 sm:px-4 py-4 sm:py-6">
<!-- Success Icon --> <!-- Success Icon -->
<div class="flex justify-center mb-4 sm:mb-6"> <div class="flex justify-center mb-4 sm:mb-6">
<div class="path-option-card cursor-default w-16 h-16 sm:w-20 sm:h-20 rounded-full flex items-center justify-center"> <div class="path-option-card cursor-default w-16 h-16 sm:w-20 sm:h-20 rounded-full flex items-center justify-center">

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full relative z-10 path-glass-container onb-scroll-container"> <div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Header --> <!-- Header -->
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6"> <div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-2 sm:mb-4 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-2 sm:mb-4 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
@ -11,7 +11,8 @@
</p> </p>
</div> </div>
<!-- Content --> <!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
<div class="flex flex-col items-center gap-4 sm:gap-6 mb-4 sm:mb-6 px-3 sm:px-4"> <div class="flex flex-col items-center gap-4 sm:gap-6 mb-4 sm:mb-6 px-3 sm:px-4">
<div class="w-full max-w-[600px] space-y-4 sm:space-y-6"> <div class="w-full max-w-[600px] space-y-4 sm:space-y-6">
<!-- Name Input --> <!-- Name Input -->
@ -59,9 +60,10 @@
<p class="text-orange-400/80 text-sm">Server is still starting up. Your identity will be saved once it's ready.</p> <p class="text-orange-400/80 text-sm">Server is still starting up. Your identity will be saved once it's ready.</p>
</div> </div>
<p v-else-if="errorMessage" class="text-red-400 text-sm text-center mb-4">{{ errorMessage }}</p> <p v-else-if="errorMessage" class="text-red-400 text-sm text-center mb-4">{{ errorMessage }}</p>
</div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6"> <div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6 pt-3">
<button <button
@click="createIdentity" @click="createIdentity"
:disabled="isCreating" :disabled="isCreating"

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[1200px] w-full relative z-10 path-glass-container onb-scroll-container"> <div class="max-w-[1200px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
<div class="text-center mb-4 sm:mb-6 flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6"> <div class="text-center mb-4 sm:mb-6 flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6">
<div class="logo-gradient-border inline-block mb-4 sm:mb-6"> <div class="logo-gradient-border inline-block mb-4 sm:mb-6">
<img <img
@ -68,8 +69,9 @@
<span class="text-xs text-white/50 mt-1 block">(Coming Soon)</span> <span class="text-xs text-white/50 mt-1 block">(Coming Soon)</span>
</div> </div>
</div> </div>
</div>
<div class="flex justify-center flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6 mt-4 sm:mt-8"> <div class="flex justify-center flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6 pt-4 sm:pt-6">
<button <button
@click="proceed" @click="proceed"
class="path-action-button path-action-button--continue" class="path-action-button path-action-button--continue"

View File

@ -1,7 +1,9 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6 relative"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6 relative">
<!-- Main Glass Container - Scrollable --> <!-- Main Glass Container - Scrollable -->
<div class="max-w-[1200px] w-full relative z-10 path-glass-container onb-scroll-container"> <div class="max-w-[1200px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
<!-- Header --> <!-- Header -->
<div class="text-center mb-4 md:mb-6 flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6"> <div class="text-center mb-4 md:mb-6 flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6">
<h1 class="text-xl md:text-[26px] font-semibold text-white/96 mb-2 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">Your Node, Your Possibilities</h1> <h1 class="text-xl md:text-[26px] font-semibold text-white/96 mb-2 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">Your Node, Your Possibilities</h1>
@ -78,9 +80,10 @@
</p> </p>
</div> </div>
</div> </div>
</div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6"> <div class="flex justify-center max-w-[600px] mx-auto flex-shrink-0 px-3 sm:px-4 pb-4 sm:pb-6 pt-4 sm:pt-6">
<button <button
ref="continueButton" ref="continueButton"
@click="proceed" @click="proceed"

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full relative z-10 path-glass-container onb-scroll-container flex flex-col" style="max-height: calc(100dvh - 2rem);"> <div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Header --> <!-- Header -->
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full relative z-10 path-glass-container onb-scroll-container flex flex-col" style="max-height: calc(100dvh - 2rem);"> <div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Header --> <!-- Header -->
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="min-h-full flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-full flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full relative z-10 path-glass-container onb-scroll-container flex flex-col" style="max-height: calc(100dvh - 2rem);"> <div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col">
<!-- Header (hidden after verification) --> <!-- Header (hidden after verification) -->
<div v-if="!verified" class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div v-if="!verified" class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
@ -66,13 +66,12 @@
</div> </div>
<!-- Word Input Fields --> <!-- Word Input Fields -->
<div v-if="!verified" class="w-full max-w-[600px] space-y-2 sm:space-y-3"> <div v-if="!verified" class="w-full max-w-[600px] space-y-3 sm:space-y-4">
<div <div
v-for="(idx, i) in challengeIndices" v-for="(idx, i) in challengeIndices"
:key="idx" :key="idx"
class="path-option-card cursor-default px-3 py-3 sm:px-5 sm:py-4"
> >
<label class="block text-xs font-semibold text-white/80 mb-1.5 sm:mb-2 uppercase tracking-wide"> <label class="block text-xs font-semibold text-white/80 mb-1.5 uppercase tracking-wide text-left">
Word #{{ idx + 1 }} Word #{{ idx + 1 }}
</label> </label>
<input <input

View File

@ -446,7 +446,10 @@ if [ "$BOTH" = true ]; then
if [ "$RO" = "true" ]; then if [ "$RO" = "true" ]; then
$DOCKER stop filebrowser 2>/dev/null; $DOCKER rm filebrowser 2>/dev/null $DOCKER stop filebrowser 2>/dev/null; $DOCKER rm filebrowser 2>/dev/null
sudo mkdir -p /var/lib/archipelago/filebrowser sudo mkdir -p /var/lib/archipelago/filebrowser
$DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>/dev/null $DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>/dev/null
fi fi
fi fi
' 2>/dev/null || true ' 2>/dev/null || true
@ -848,7 +851,10 @@ PYEOF
$DOCKER stop filebrowser 2>/dev/null $DOCKER stop filebrowser 2>/dev/null
$DOCKER rm filebrowser 2>/dev/null $DOCKER rm filebrowser 2>/dev/null
sudo mkdir -p /var/lib/archipelago/filebrowser sudo mkdir -p /var/lib/archipelago/filebrowser
$DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>&1 | tail -1 $DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>&1 | tail -1
echo " FileBrowser recreated" echo " FileBrowser recreated"
else else
echo " FileBrowser OK" echo " FileBrowser OK"
@ -856,7 +862,10 @@ PYEOF
else else
echo " Creating FileBrowser..." echo " Creating FileBrowser..."
sudo mkdir -p /var/lib/archipelago/filebrowser sudo mkdir -p /var/lib/archipelago/filebrowser
$DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>&1 | tail -1 $DOCKER run -d --name filebrowser --restart=unless-stopped --user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv "$FILEBROWSER_IMAGE" 2>&1 | tail -1
echo " FileBrowser created" echo " FileBrowser created"
fi fi
' 2>/dev/null || true ' 2>/dev/null || true
@ -1091,6 +1100,8 @@ MANIFEST_EOF
echo ' Creating mysql-mempool...' echo ' Creating mysql-mempool...'
sudo mkdir -p /var/lib/archipelago/mysql-mempool sudo mkdir -p /var/lib/archipelago/mysql-mempool
\$DOCKER run -d --name archy-mempool-db --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name archy-mempool-db --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \ -v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
-e MYSQL_DATABASE=mempool \ -e MYSQL_DATABASE=mempool \
-e MYSQL_USER=mempool \ -e MYSQL_USER=mempool \
@ -1118,6 +1129,8 @@ MANIFEST_EOF
echo ' Creating electrumx (indexer - may take days to sync, do not recreate)...' echo ' Creating electrumx (indexer - may take days to sync, do not recreate)...'
sudo mkdir -p /var/lib/archipelago/electrumx sudo mkdir -p /var/lib/archipelago/electrumx
\$DOCKER run -d --name electrumx --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name electrumx --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 50001:50001 \ -p 50001:50001 \
-v /var/lib/archipelago/electrumx:/data \ -v /var/lib/archipelago/electrumx:/data \
-e DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/ \ -e DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/ \
@ -1137,6 +1150,8 @@ MANIFEST_EOF
echo ' Creating mempool-api (backend)...' echo ' Creating mempool-api (backend)...'
sudo mkdir -p /var/lib/archipelago/mempool sudo mkdir -p /var/lib/archipelago/mempool
\$DOCKER run -d --name mempool-api --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name mempool-api --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8999:8999 \ -p 8999:8999 \
-v /var/lib/archipelago/mempool:/data \ -v /var/lib/archipelago/mempool:/data \
-e MEMPOOL_BACKEND=electrum \ -e MEMPOOL_BACKEND=electrum \
@ -1164,6 +1179,8 @@ MANIFEST_EOF
if ! \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-mempool-web; then if ! \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-mempool-web; then
echo ' Creating mempool frontend on 4080...' echo ' Creating mempool frontend on 4080...'
\$DOCKER run -d --name archy-mempool-web --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name archy-mempool-web --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 4080:8080 \ -p 4080:8080 \
-e FRONTEND_HTTP_PORT=8080 \ -e FRONTEND_HTTP_PORT=8080 \
-e BACKEND_MAINNET_HTTP_HOST=mempool-api \ -e BACKEND_MAINNET_HTTP_HOST=mempool-api \
@ -1187,6 +1204,8 @@ MANIFEST_EOF
echo ' Creating archy-btcpay-db (PostgreSQL)...' echo ' Creating archy-btcpay-db (PostgreSQL)...'
sudo mkdir -p /var/lib/archipelago/postgres-btcpay sudo mkdir -p /var/lib/archipelago/postgres-btcpay
\$DOCKER run -d --name archy-btcpay-db --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name archy-btcpay-db --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \ -v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
-e POSTGRES_DB=btcpay \ -e POSTGRES_DB=btcpay \
-e POSTGRES_USER=btcpay \ -e POSTGRES_USER=btcpay \
@ -1205,6 +1224,8 @@ MANIFEST_EOF
echo ' Creating archy-nbxplorer...' echo ' Creating archy-nbxplorer...'
sudo mkdir -p /var/lib/archipelago/nbxplorer sudo mkdir -p /var/lib/archipelago/nbxplorer
\$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \$NET_OPT \ \$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \$NET_OPT \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 32838:32838 \ -p 32838:32838 \
-v /var/lib/archipelago/nbxplorer:/data \ -v /var/lib/archipelago/nbxplorer:/data \
-e NBXPLORER_DATADIR=/data \ -e NBXPLORER_DATADIR=/data \

View File

@ -16,8 +16,6 @@
# DO NOT split until tested on the build server — this is critical infrastructure. # DO NOT split until tested on the build server — this is critical infrastructure.
# #
LOG="/var/log/archipelago-first-boot.log" LOG="/var/log/archipelago-first-boot.log"
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
# Source pinned image versions (single source of truth) # Source pinned image versions (single source of truth)
source /opt/archipelago/image-versions.sh 2>/dev/null || true source /opt/archipelago/image-versions.sh 2>/dev/null || true
@ -26,9 +24,17 @@ source /opt/archipelago/image-versions.sh 2>/dev/null || true
SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)"
[ -f "$SCRIPT_DIR_FBC/lib/common.sh" ] && source "$SCRIPT_DIR_FBC/lib/common.sh" || true [ -f "$SCRIPT_DIR_FBC/lib/common.sh" ] && source "$SCRIPT_DIR_FBC/lib/common.sh" || true
# Must run as root for podman # Must run as root for system setup (sysctl, loginctl, subuid, chown).
# Podman commands run as the archipelago user (rootless) so the backend
# (which also runs as archipelago) can see and manage the containers.
[ "$(id -u)" -eq 0 ] || { echo "Must run as root" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || { echo "Must run as root" >&2; exit 1; }
# Run podman as the archipelago user (rootless) — NOT as root.
# The backend service runs as User=archipelago and connects to the rootless
# podman socket at /run/user/1000/podman/podman.sock. If we create containers
# as root (rootful podman), the backend can't see them at all.
DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/1000 podman"
TARGET_IP=$(hostname -I 2>/dev/null | awk '{print $1}') TARGET_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$TARGET_IP" ] && TARGET_IP="127.0.0.1" [ -z "$TARGET_IP" ] && TARGET_IP="127.0.0.1"
@ -219,6 +225,14 @@ grep -q "^archipelago:" /etc/subuid 2>/dev/null || {
# Ensure /etc/hosts is readable (rootless podman needs it) # Ensure /etc/hosts is readable (rootless podman needs it)
chmod 644 /etc/hosts 2>/dev/null chmod 644 /etc/hosts 2>/dev/null
# Ensure XDG_RUNTIME_DIR exists for rootless podman
mkdir -p /run/user/1000
chown archipelago:archipelago /run/user/1000
chmod 700 /run/user/1000
# Start rootless podman socket (required before first podman command)
runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/1000 \
systemctl --user start podman.socket 2>/dev/null || true
# Ensure network exists (matches deploy) # Ensure network exists (matches deploy)
$DOCKER network create archy-net 2>/dev/null || true $DOCKER network create archy-net 2>/dev/null || true
@ -348,6 +362,8 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-d
$DOCKER run -d --name archy-mempool-db --restart unless-stopped \ $DOCKER run -d --name archy-mempool-db --restart unless-stopped \
--health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-mempool-db) --network archy-net \ --memory=$(mem_limit archy-mempool-db) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \ -v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e "MYSQL_PASSWORD=$MEMPOOL_DB_PASS" \ -e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e "MYSQL_PASSWORD=$MEMPOOL_DB_PASS" \
-e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS" \ -e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS" \
@ -368,6 +384,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then
$DOCKER run -d --name electrumx --restart unless-stopped \ $DOCKER run -d --name electrumx --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8000/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:8000/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit electrumx) --network archy-net \ --memory=$(mem_limit electrumx) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 50001:50001 -v /var/lib/archipelago/electrumx:/data \ -p 50001:50001 -v /var/lib/archipelago/electrumx:/data \
-e "DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/" \ -e "DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/" \
-e COIN=Bitcoin -e DB_DIRECTORY=/data \ -e COIN=Bitcoin -e DB_DIRECTORY=/data \
@ -383,6 +401,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
$DOCKER run -d --name mempool-api --restart unless-stopped \ $DOCKER run -d --name mempool-api --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8999/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:8999/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit mempool-api) --network archy-net \ --memory=$(mem_limit mempool-api) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8999:8999 -v /var/lib/archipelago/mempool:/data \ -p 8999:8999 -v /var/lib/archipelago/mempool:/data \
-e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \ -e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \ -e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
@ -398,6 +418,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-web|
$DOCKER run -d --name archy-mempool-web --restart unless-stopped \ $DOCKER run -d --name archy-mempool-web --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-mempool-web) --network archy-net \ --memory=$(mem_limit archy-mempool-web) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \ -p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \
"$MEMPOOL_WEB_IMAGE" 2>>"$LOG" || true "$MEMPOOL_WEB_IMAGE" 2>>"$LOG" || true
fi fi
@ -409,15 +431,21 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrs-ui; then
log "Starting ElectrumX UI from pre-built image..." log "Starting ElectrumX UI from pre-built image..."
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
localhost/electrs-ui:local 2>>"$LOG" || \ localhost/electrs-ui:local 2>>"$LOG" || \
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
electrs-ui:local 2>>"$LOG" || true electrs-ui:local 2>>"$LOG" || true
elif [ -d /opt/archipelago/docker/electrs-ui ]; then elif [ -d /opt/archipelago/docker/electrs-ui ]; then
log "Building and starting ElectrumX UI from source..." log "Building and starting ElectrumX UI from source..."
$DOCKER build -t electrs-ui:local /opt/archipelago/docker/electrs-ui 2>>"$LOG" && \ $DOCKER build -t electrs-ui:local /opt/archipelago/docker/electrs-ui 2>>"$LOG" && \
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
electrs-ui:local 2>>"$LOG" || true electrs-ui:local 2>>"$LOG" || true
else else
log "ElectrumX UI: no image or source found, skipping" log "ElectrumX UI: no image or source found, skipping"
@ -431,6 +459,8 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db
$DOCKER run -d --name archy-btcpay-db --restart unless-stopped \ $DOCKER run -d --name archy-btcpay-db --restart unless-stopped \
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="pg_isready -U postgres || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-btcpay-db) --network archy-net \ --memory=$(mem_limit archy-btcpay-db) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \ -v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e "POSTGRES_PASSWORD=$BTCPAY_DB_PASS" \ -e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e "POSTGRES_PASSWORD=$BTCPAY_DB_PASS" \
"$BTCPAY_POSTGRES_IMAGE" 2>>"$LOG" || true "$BTCPAY_POSTGRES_IMAGE" 2>>"$LOG" || true
@ -452,6 +482,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the
$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \ $DOCKER run -d --name archy-nbxplorer --restart unless-stopped \
--health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-nbxplorer) --network archy-net \ --memory=$(mem_limit archy-nbxplorer) --network archy-net \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \ -p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
-e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -e NBXPLORER_CHAINS=btc \ -e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -e NBXPLORER_CHAINS=btc \
-e NBXPLORER_BIND=0.0.0.0:32838 -e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \ -e NBXPLORER_BIND=0.0.0.0:32838 -e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \
@ -650,7 +682,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q jellyfin; then
$DOCKER run -d --name jellyfin --restart unless-stopped \ $DOCKER run -d --name jellyfin --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8096/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:8096/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit jellyfin) \ --memory=$(mem_limit jellyfin) \
--cap-drop ALL --security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
--tmpfs /tmp:rw,exec,size=256m \
-p 8096:8096 \ -p 8096:8096 \
-v /var/lib/archipelago/jellyfin/config:/config \ -v /var/lib/archipelago/jellyfin/config:/config \
-v /var/lib/archipelago/jellyfin/cache:/cache \ -v /var/lib/archipelago/jellyfin/cache:/cache \
@ -782,6 +816,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q tailscale; then
--cap-drop=ALL \ --cap-drop=ALL \
--cap-add=NET_ADMIN \ --cap-add=NET_ADMIN \
--cap-add=NET_RAW \ --cap-add=NET_RAW \
--security-opt no-new-privileges:true \
--device=/dev/net/tun:/dev/net/tun \ --device=/dev/net/tun:/dev/net/tun \
--read-only \ --read-only \
--tmpfs /tmp \ --tmpfs /tmp \
@ -802,6 +837,9 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'nos
$DOCKER run -d --name nostr-rs-relay --restart unless-stopped \ $DOCKER run -d --name nostr-rs-relay --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit nostr-rs-relay) \ --memory=$(mem_limit nostr-rs-relay) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=32m \
-p 7047:7047 -v /var/lib/archipelago/nostr-rs-relay:/data \ -p 7047:7047 -v /var/lib/archipelago/nostr-rs-relay:/data \
"${NOSTR_RS_RELAY_IMAGE}" 2>>"$LOG" || true "${NOSTR_RS_RELAY_IMAGE}" 2>>"$LOG" || true
fi fi
@ -813,6 +851,8 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'str
$DOCKER run -d --name strfry --restart unless-stopped \ $DOCKER run -d --name strfry --restart unless-stopped \
--health-cmd="curl -sf http://localhost:7777/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:7777/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit strfry) \ --memory=$(mem_limit strfry) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 7777:7777 -v /var/lib/archipelago/strfry:/data \ -p 7777:7777 -v /var/lib/archipelago/strfry:/data \
"${STRFRY_IMAGE}" 2>>"$LOG" || true "${STRFRY_IMAGE}" 2>>"$LOG" || true
fi fi
@ -880,16 +920,25 @@ for ui in bitcoin-ui lnd-ui; do
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
log "Starting $ui from pre-built image..." log "Starting $ui from pre-built image..."
IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1) IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1)
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$IMG" 2>>"$LOG" || true $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
"$IMG" 2>>"$LOG" || true
elif [ -d "/opt/archipelago/docker/$ui" ]; then elif [ -d "/opt/archipelago/docker/$ui" ]; then
log "Building $ui from source (/opt/archipelago/docker/$ui)..." log "Building $ui from source (/opt/archipelago/docker/$ui)..."
if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:local" 2>>"$LOG" || true $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
"$ui:local" 2>>"$LOG" || true
fi fi
elif [ -d "/home/archipelago/archy/docker/$ui" ]; then elif [ -d "/home/archipelago/archy/docker/$ui" ]; then
log "Building $ui from source (/home/archipelago/archy/docker/$ui)..." log "Building $ui from source (/home/archipelago/archy/docker/$ui)..."
if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:local" 2>>"$LOG" || true $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
"$ui:local" 2>>"$LOG" || true
fi fi
else else
log "$ui: no image or source found, skipping" log "$ui: no image or source found, skipping"

View File

@ -36,7 +36,7 @@ JELLYFIN_IMAGE="$ARCHY_REGISTRY/jellyfin:10.8.13"
PHOTOPRISM_IMAGE="$ARCHY_REGISTRY/photoprism:240915" PHOTOPRISM_IMAGE="$ARCHY_REGISTRY/photoprism:240915"
OLLAMA_IMAGE="$ARCHY_REGISTRY/ollama:latest" OLLAMA_IMAGE="$ARCHY_REGISTRY/ollama:latest"
VAULTWARDEN_IMAGE="$ARCHY_REGISTRY/vaultwarden:1.30.0-alpine" VAULTWARDEN_IMAGE="$ARCHY_REGISTRY/vaultwarden:1.30.0-alpine"
NEXTCLOUD_IMAGE="$ARCHY_REGISTRY/nextcloud:28" NEXTCLOUD_IMAGE="$ARCHY_REGISTRY/nextcloud:29"
SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest" SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest"
ONLYOFFICE_IMAGE="$ARCHY_REGISTRY/onlyoffice:latest" ONLYOFFICE_IMAGE="$ARCHY_REGISTRY/onlyoffice:latest"
FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.27.0" FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.27.0"
@ -82,11 +82,7 @@ PENPOT_EXPORTER_IMAGE="$ARCHY_REGISTRY/penpot-exporter:2.4"
PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4" PENPOT_FRONTEND_IMAGE="$ARCHY_REGISTRY/penpot-frontend:2.4"
# Custom UI containers (built from docker/ dirs, pushed to registry) # Custom UI containers (built from docker/ dirs, pushed to registry)
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:latest" # These use :latest because they're internally built and pushed — acceptable for self-hosted images
LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:latest"
ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest"
# Custom UI containers (companion dashboards for headless services)
BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:latest" BITCOIN_UI_IMAGE="$ARCHY_REGISTRY/bitcoin-ui:latest"
LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:latest" LND_UI_IMAGE="$ARCHY_REGISTRY/lnd-ui:latest"
ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest" ELECTRS_UI_IMAGE="$ARCHY_REGISTRY/electrs-ui:latest"