Implement multi-container app installation for Immich and Penpot, enhance Docker package scanning, and update Nginx configuration for iframe support

- Added support for installing Immich and Penpot stacks, including necessary Docker images and network configurations.
- Updated DockerPackageScanner to exclude Immich and Penpot related containers from app listings.
- Enhanced Nginx configuration to support iframe embedding for Immich and Penpot applications, improving user experience.
- Modified deployment scripts to ensure proper setup of first-boot container creation services.
This commit is contained in:
Dorian 2026-02-25 18:04:41 +00:00
parent f0ef84e4a5
commit 4cb9ac1faa
12 changed files with 876 additions and 30 deletions

View File

@ -618,6 +618,14 @@ impl RpcHandler {
return Err(anyhow::anyhow!("Invalid Docker image format"));
}
// Multi-container apps: create full stack
if package_id == "immich" {
return self.install_immich_stack().await;
}
if package_id == "penpot" || package_id == "penpot-frontend" {
return self.install_penpot_stack().await;
}
// Check if container already exists
let check_output = tokio::process::Command::new("sudo")
.args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name=^{}$", package_id)])
@ -663,7 +671,8 @@ impl RpcHandler {
let is_tailscale = package_id == "tailscale";
let needs_archy_net = matches!(
package_id,
"mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web"
"bitcoin-knots" | "bitcoin" | "bitcoin-core"
| "mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web"
| "btcpay-server" | "btcpayserver" | "archy-btcpay-db"
);
@ -695,6 +704,13 @@ impl RpcHandler {
if let Err(e) = create_dir {
debug!("Failed to create directory {}: {}", host_path, e);
}
// Grafana runs as UID 472 - fix permissions so it can write
if package_id == "grafana" && host_path.contains("grafana") {
let _ = tokio::process::Command::new("sudo")
.args(["chown", "-R", "472:472", host_path])
.output()
.await;
}
}
}
}
@ -723,8 +739,10 @@ impl RpcHandler {
// run_args.push("--network=isolated"); // Future: per-app network
// Security: Resource limits (from manifest)
run_args.push("--memory=2g"); // TODO: from manifest
run_args.push("--cpus=2"); // TODO: from manifest
let memory_limit = if package_id == "ollama" { "4g" } else { "2g" };
let mem_arg = format!("--memory={}", memory_limit);
run_args.push(&mem_arg);
run_args.push("--cpus=2");
// Finally, the image
run_args.push(docker_image);
@ -762,6 +780,213 @@ impl RpcHandler {
}))
}
/// Install Immich stack (postgres + redis + server)
async fn install_immich_stack(&self) -> Result<serde_json::Value> {
let check = tokio::process::Command::new("sudo")
.args(["podman", "ps", "-a", "--format", "{{.Names}}"])
.output()
.await
.context("Failed to list containers")?;
let stdout = String::from_utf8_lossy(&check.stdout);
if stdout.contains("immich_server") {
return Err(anyhow::anyhow!("Immich already installed. Stop and remove it first."));
}
let images = [
"ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0",
"docker.io/valkey/valkey:7-alpine",
"ghcr.io/immich-app/immich-server:release",
];
for img in &images {
let _ = tokio::process::Command::new("sudo")
.args(["podman", "pull", img])
.output()
.await;
}
let _ = tokio::process::Command::new("sudo")
.args(["mkdir", "-p", "/var/lib/archipelago/immich", "/var/lib/archipelago/immich-db"])
.output()
.await;
let _ = tokio::process::Command::new("sudo")
.args(["podman", "network", "create", "immich-net"])
.output()
.await;
// Postgres
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "immich_postgres", "--restart", "unless-stopped",
"--network", "immich-net",
"-v", "/var/lib/archipelago/immich-db:/var/lib/postgresql/data",
"-e", "POSTGRES_PASSWORD=immichpass", "-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_DB=immich",
"ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Redis
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "immich_redis", "--restart", "unless-stopped",
"--network", "immich-net",
"docker.io/valkey/valkey:7-alpine",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Server
let run = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "immich_server", "--restart", "unless-stopped",
"--network", "immich-net", "-p", "2283:2283",
"-v", "/var/lib/archipelago/immich:/usr/src/app/upload",
"-e", "DB_HOSTNAME=immich_postgres", "-e", "DB_USERNAME=postgres",
"-e", "DB_PASSWORD=immichpass", "-e", "DB_DATABASE_NAME=immich",
"-e", "REDIS_HOSTNAME=immich_redis", "-e", "UPLOAD_LOCATION=/usr/src/app/upload",
"ghcr.io/immich-app/immich-server:release",
])
.output()
.await
.context("Failed to start immich_server")?;
if !run.status.success() {
let stderr = String::from_utf8_lossy(&run.stderr);
return Err(anyhow::anyhow!("Failed to start Immich server: {}", stderr));
}
Ok(serde_json::json!({
"success": true,
"package_id": "immich",
"message": "Immich stack installed and started"
}))
}
/// Install Penpot stack (postgres + valkey + backend + exporter + frontend)
async fn install_penpot_stack(&self) -> Result<serde_json::Value> {
let check = tokio::process::Command::new("sudo")
.args(["podman", "ps", "-a", "--format", "{{.Names}}"])
.output()
.await
.context("Failed to list containers")?;
let stdout = String::from_utf8_lossy(&check.stdout);
if stdout.contains("penpot-frontend") {
return Err(anyhow::anyhow!("Penpot already installed. Stop and remove it first."));
}
let images = [
"docker.io/postgres:15",
"docker.io/valkey/valkey:8.1",
"docker.io/penpotapp/backend:latest",
"docker.io/penpotapp/exporter:latest",
"docker.io/penpotapp/frontend:latest",
];
for img in &images {
let _ = tokio::process::Command::new("sudo")
.args(["podman", "pull", img])
.output()
.await;
}
let _ = tokio::process::Command::new("sudo")
.args(["mkdir", "-p", "/var/lib/archipelago/penpot-assets"])
.output()
.await;
let _ = tokio::process::Command::new("sudo")
.args(["podman", "network", "create", "penpot-net"])
.output()
.await;
let secret = "archipelago-penpot-secret-key-change-in-production";
let host_ip = &self.config.host_ip;
// Postgres
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "penpot-postgres", "--restart", "unless-stopped",
"--network", "penpot-net",
"-v", "/var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data",
"-e", "POSTGRES_DB=penpot", "-e", "POSTGRES_USER=penpot", "-e", "POSTGRES_PASSWORD=penpot",
"docker.io/postgres:15",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Valkey
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "penpot-valkey", "--restart", "unless-stopped",
"--network", "penpot-net",
"-e", "VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu",
"docker.io/valkey/valkey:8.1",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
// Backend
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "penpot-backend", "--restart", "unless-stopped",
"--network", "penpot-net",
"-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets",
"-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
"-e", &format!("PENPOT_SECRET_KEY={}", secret),
"-e", "PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot",
"-e", "PENPOT_DATABASE_USERNAME=penpot", "-e", "PENPOT_DATABASE_PASSWORD=penpot",
"-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0",
"-e", "PENPOT_OBJECTS_STORAGE_BACKEND=fs",
"-e", "PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets",
"-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
"docker.io/penpotapp/backend:latest",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// Exporter
let _ = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "penpot-exporter", "--restart", "unless-stopped",
"--network", "penpot-net",
"-e", &format!("PENPOT_SECRET_KEY={}", secret),
"-e", "PENPOT_PUBLIC_URI=http://penpot-frontend:8080",
"-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0",
"docker.io/penpotapp/exporter:latest",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Frontend
let run = tokio::process::Command::new("sudo")
.args([
"podman", "run", "-d", "--name", "penpot-frontend", "--restart", "unless-stopped",
"--network", "penpot-net", "-p", "9001:8080",
"-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets",
"-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
"-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
"docker.io/penpotapp/frontend:latest",
])
.output()
.await
.context("Failed to start penpot-frontend")?;
if !run.status.success() {
let stderr = String::from_utf8_lossy(&run.stderr);
return Err(anyhow::anyhow!("Failed to start Penpot frontend: {}", stderr));
}
Ok(serde_json::json!({
"success": true,
"package_id": "penpot",
"message": "Penpot stack installed and started"
}))
}
// Package management methods for docker-compose containers
async fn handle_package_start(
&self,
@ -777,8 +1002,12 @@ impl RpcHandler {
let to_start: Vec<String> = if containers.is_empty() {
vec![format!("archy-{}", package_id)]
} else {
// Start order for mempool: db first, then api, then web
let order = ["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"];
let order: &[&str] = match package_id {
"mempool" | "mempool-web" => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"],
"immich" => &["immich_postgres", "immich_redis", "immich_server"],
"penpot" | "penpot-frontend" => &["penpot-postgres", "penpot-valkey", "penpot-backend", "penpot-exporter", "penpot-frontend"],
_ => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"],
};
let mut sorted = containers;
sorted.sort_by_key(|c| order.iter().position(|o| *o == c).unwrap_or(99));
sorted
@ -1131,6 +1360,18 @@ async fn get_containers_for_app(package_id: &str) -> Result<Vec<String>> {
]
}
"fedimint" => vec!["fedimint".into(), "fedimint-ui".into(), "archy-fedimint".into()],
"immich" => vec![
"immich_postgres".into(),
"immich_redis".into(),
"immich_server".into(),
],
"penpot" | "penpot-frontend" => vec![
"penpot-postgres".into(),
"penpot-valkey".into(),
"penpot-backend".into(),
"penpot-exporter".into(),
"penpot-frontend".into(),
],
_ => vec![package_id.to_string(), format!("archy-{}", package_id)],
};
@ -1156,6 +1397,14 @@ fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
format!("{}/mempool-electrs", base),
],
"fedimint" => vec![format!("{}/fedimint", base)],
"immich" => vec![
format!("{}/immich", base),
format!("{}/immich-db", base),
],
"penpot" | "penpot-frontend" => vec![
format!("{}/penpot-assets", base),
format!("{}/penpot-postgres", base),
],
_ => vec![format!("{}/{}", base, package_id)],
}
}
@ -1203,9 +1452,9 @@ fn get_app_config(
None,
None,
),
"bitcoin" | "bitcoin-core" => (
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => (
vec!["8332:8332".to_string(), "8333:8333".to_string()],
vec!["/var/lib/archipelago/bitcoin:/bitcoin/.bitcoin".to_string()],
vec!["/var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin".to_string()],
vec![],
None,
None,
@ -1295,7 +1544,7 @@ fn get_app_config(
"grafana" => (
vec!["3000:3000".to_string()],
vec!["/var/lib/archipelago/grafana:/var/lib/grafana".to_string()],
vec![],
vec!["GF_PATHS_DATA=/var/lib/grafana".to_string(), "GF_USERS_ALLOW_SIGN_UP=false".to_string()],
None,
None,
),
@ -1361,14 +1610,21 @@ fn get_app_config(
"photoprism" => (
vec!["2342:2342".to_string()],
vec!["/var/lib/archipelago/photoprism:/photoprism/storage".to_string()],
vec![],
vec!["PHOTOPRISM_ADMIN_PASSWORD=archipelago".to_string(), "PHOTOPRISM_DEFAULT_LOCALE=en".to_string()],
None,
None,
),
"immich" => (
vec!["2283:3001".to_string()],
vec!["2283:2283".to_string()],
vec!["/var/lib/archipelago/immich:/usr/src/app/upload".to_string()],
vec![],
vec![
"DB_HOSTNAME=immich_postgres".to_string(),
"DB_USERNAME=postgres".to_string(),
"DB_PASSWORD=immichpass".to_string(),
"DB_DATABASE_NAME=immich".to_string(),
"REDIS_HOSTNAME=immich_redis".to_string(),
"UPLOAD_LOCATION=/usr/src/app/upload".to_string(),
],
None,
None,
),
@ -1404,7 +1660,7 @@ fn get_app_config(
"uptime-kuma" => (
vec!["3001:3001".to_string()],
vec!["/var/lib/archipelago/uptime-kuma:/app/data".to_string()],
vec![],
vec!["TZ=UTC".to_string()],
None,
None,
),

View File

@ -38,6 +38,7 @@ impl DockerPackageScanner {
// Backend services that should not appear as apps
let excluded_services = [
"btcpay-db",
"nbxplorer",
"mempool-db",
"mempool-api",
"penpot-postgres",
@ -45,6 +46,8 @@ impl DockerPackageScanner {
"penpot-exporter",
"penpot-valkey",
"penpot-mailcatch",
"immich_postgres",
"immich_redis",
"endurain-db",
"nextcloud-db",
];
@ -224,7 +227,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
icon: "/assets/img/app-icons/btcpay-server.png".to_string(),
repo: "https://github.com/btcpayserver/btcpayserver".to_string(),
},
"homeassistant" => AppMetadata {
"homeassistant" | "home-assistant" => AppMetadata {
title: "Home Assistant".to_string(),
description: "Open source home automation platform".to_string(),
icon: "/assets/img/app-icons/homeassistant.png".to_string(),
@ -320,7 +323,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
icon: "/assets/img/favico.png".to_string(), // Placeholder, no icon available
repo: "https://github.com/photoprism/photoprism".to_string(),
},
"immich" => AppMetadata {
"immich" | "immich_server" => AppMetadata {
title: "Immich".to_string(),
description: "High-performance self-hosted photo and video backup".to_string(),
icon: "/assets/img/favico.png".to_string(), // Placeholder, no icon available

View File

@ -689,7 +689,31 @@ 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/"
echo " ✅ Container images bundled (including Tor)"
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
echo " Creating first-boot container creation service..."
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'
[Unit]
Description=Create core Archipelago containers on first boot
After=archipelago-setup-tor.service network-online.target podman.service
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
[Service]
Type=oneshot
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/"
fi
echo " ✅ Container images bundled (including Tor + first-boot)"
# =============================================================================
# STEP 4: Create auto-installer script
@ -930,6 +954,13 @@ if [ -d "$BOOT_MEDIA/archipelago/container-images" ]; then
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
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
echo " ✅ Container images staged for first-boot loading"
fi
@ -1025,6 +1056,7 @@ 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
chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true
chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true
# Cleanup
sync

View File

@ -27,9 +27,57 @@ server {
proxy_set_header X-Real-IP $remote_addr;
# Increase timeout for long-running operations (e.g., Docker image pulls)
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
# Proxy apps that set X-Frame-Options - strip header so iframe works
location /app/nextcloud/ {
proxy_pass http://127.0.0.1:8085/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/vaultwarden/ {
proxy_pass http://127.0.0.1:8082/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
location /app/immich/ {
proxy_pass http://127.0.0.1:2283/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/penpot/ {
proxy_pass http://127.0.0.1:9001/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# Proxy WebSocket

View File

@ -1,16 +1,35 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
/** BTCPay and Home Assistant set X-Frame-Options and don't support subpath proxy - open in new tab */
/** Apps that set X-Frame-Options and/or don't support subpath proxy - open in new tab for correct display */
function mustOpenInNewTab(url: string): boolean {
try {
const u = new URL(url)
return u.port === '23000' || u.port === '8123'
return (
u.port === '23000' || // BTCPay
u.port === '8123' || // Home Assistant
u.port === '8085' || // Nextcloud (subpath breaks CSS/assets)
u.port === '2283' // Immich (subpath breaks SPA)
)
} catch {
return false
}
}
/** Rewrite to same-origin proxy so iframe can embed (nginx strips X-Frame-Options) */
function toEmbeddableUrl(url: string): string {
try {
const u = new URL(url)
const origin = window.location.origin
// Only Vaultwarden and Penpot support subpath proxy; Nextcloud/Immich open in new tab
if (u.port === '8082') return `${origin}/app/vaultwarden/`
if (u.port === '9001') return `${origin}/app/penpot/`
} catch {
/* ignore */
}
return url
}
export const useAppLauncherStore = defineStore('appLauncher', () => {
const isOpen = ref(false)
const url = ref('')
@ -23,7 +42,7 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
return
}
previousActiveElement = (document.activeElement as HTMLElement) || null
url.value = payload.url
url.value = toEmbeddableUrl(payload.url)
title.value = payload.title
isOpen.value = true
}

View File

@ -456,6 +456,23 @@ const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
'lnd-ui': 'lnd',
bitcoin: 'bitcoin-knots',
'bitcoin-knots': 'bitcoin-knots',
homeassistant: 'homeassistant',
'home-assistant': 'homeassistant',
grafana: 'grafana',
searxng: 'searxng',
ollama: 'ollama',
onlyoffice: 'onlyoffice',
penpot: 'penpot',
nextcloud: 'nextcloud',
vaultwarden: 'vaultwarden',
jellyfin: 'jellyfin',
photoprism: 'photoprism',
immich: 'immich',
filebrowser: 'filebrowser',
'nginx-proxy-manager': 'nginx-proxy-manager',
portainer: 'portainer',
'uptime-kuma': 'uptime-kuma',
tailscale: 'tailscale',
}
function resolvePackageKey(routeId: string): string {
@ -671,17 +688,29 @@ function launchApp() {
prod: 'http://localhost:11434'
},
'searxng': {
dev: 'http://localhost:8082',
prod: 'http://localhost:8082'
dev: 'http://localhost:8888',
prod: 'http://localhost:8888'
},
'onlyoffice': {
dev: 'http://localhost:8083',
prod: 'http://localhost:8083'
dev: 'http://localhost:9980',
prod: 'http://localhost:9980'
},
'penpot': {
dev: 'http://localhost:9001',
prod: 'http://localhost:9001'
}
},
'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' },
'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' },
'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' },
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' },
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
'uptime-kuma': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
'tailscale': { dev: 'http://localhost:8240', prod: 'http://localhost:8240' },
'lnd': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
'bitcoin-knots': { dev: 'http://localhost:8334', prod: 'http://localhost:8334' }
}
if (appUrls[id]) {

View File

@ -587,8 +587,16 @@ const filteredCommunityApps = computed(() => {
return communityApps.value
})
/** Marketplace app ID -> backend package keys (for "Already Installed" when first-boot/deploy created them) */
const INSTALLED_ALIASES: Record<string, string[]> = {
mempool: ['mempool-web'],
bitcoin: ['bitcoin-knots'],
btcpay: ['btcpay-server'],
}
function isInstalled(appId: string): boolean {
return appId in installedPackages.value
if (appId in installedPackages.value) return true
const aliases = INSTALLED_ALIASES[appId]
return aliases ? aliases.some((a) => a in installedPackages.value) : false
}
// Load community marketplace on mount
@ -818,7 +826,7 @@ function getCuratedAppList() {
description: 'Self-hosted monitoring tool. Monitor uptime for HTTP(s), TCP, DNS, and more.',
icon: '/assets/img/app-icons/uptime-kuma.webp',
author: 'Uptime Kuma',
dockerImage: 'docker.io/louislam/uptime-kuma:1.23.11',
dockerImage: 'docker.io/louislam/uptime-kuma:1',
manifestUrl: null,
repoUrl: 'https://github.com/louislam/uptime-kuma'
},

View File

@ -132,6 +132,29 @@ if [ "$LIVE" = true ]; then
rm -f /tmp/archipelago-nginx-patch.conf
' 2>/dev/null || true
fi
# Add /app/nextcloud/, /app/vaultwarden/, /app/immich/ proxy for iframe embedding (strip X-Frame-Options)
if [ -f "$SCRIPT_DIR/nginx-app-iframe-patch.conf" ]; then
sshpass -p "$ARCHIPELAGO_PASSWORD" scp $SSH_OPTS "$SCRIPT_DIR/nginx-app-iframe-patch.conf" "$TARGET_HOST:/tmp/nginx-app-iframe-patch.conf" 2>/dev/null || true
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" '
CFG=/etc/nginx/sites-available/archipelago
if [ -f "$CFG" ] && [ -f /tmp/nginx-app-iframe-patch.conf ] && ! grep -q "location /app/nextcloud/" "$CFG"; then
echo " Adding /app/nextcloud/, /app/vaultwarden/, /app/immich/, /app/penpot/ proxy to nginx..."
sudo sed -i "/# Proxy WebSocket/r /tmp/nginx-app-iframe-patch.conf" "$CFG"
fi
rm -f /tmp/nginx-app-iframe-patch.conf
' 2>/dev/null || true
fi
if [ -f "$SCRIPT_DIR/nginx-penpot-iframe-patch.conf" ]; then
sshpass -p "$ARCHIPELAGO_PASSWORD" scp $SSH_OPTS "$SCRIPT_DIR/nginx-penpot-iframe-patch.conf" "$TARGET_HOST:/tmp/nginx-penpot-patch.conf" 2>/dev/null || true
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" '
CFG=/etc/nginx/sites-available/archipelago
if [ -f "$CFG" ] && [ -f /tmp/nginx-penpot-patch.conf ] && ! grep -q "location /app/penpot/" "$CFG"; then
echo " Adding /app/penpot/ proxy to nginx..."
sudo sed -i "/# Proxy WebSocket/r /tmp/nginx-penpot-patch.conf" "$CFG"
fi
rm -f /tmp/nginx-penpot-patch.conf
' 2>/dev/null || true
fi
# Restart services
echo " Restarting services..."
@ -373,6 +396,41 @@ if [ "$LIVE" = true ]; then
fi
" 2>&1 | sed 's/^/ /' || true
# Ensure Immich stack (postgres + redis + server) - creates if missing
echo " Ensuring Immich stack (port 2283)..."
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
echo ' Creating Immich stack...'
sudo mkdir -p /var/lib/archipelago/immich /var/lib/archipelago/immich-db
sudo \$DOCKER network create immich-net 2>/dev/null || true
if ! sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q immich_postgres; then
sudo \$DOCKER run -d --name immich_postgres --restart unless-stopped --network immich-net \
-v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=immichpass -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>/dev/null || true
sleep 5
fi
if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_redis; then
sudo \$DOCKER run -d --name immich_redis --restart unless-stopped --network immich-net \
docker.io/valkey/valkey:7-alpine 2>/dev/null || true
sleep 2
fi
if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
sudo \$DOCKER run -d --name immich_server --restart unless-stopped --network immich-net \
-p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=immichpass \
-e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \
-e UPLOAD_LOCATION=/usr/src/app/upload \
ghcr.io/immich-app/immich-server:release 2>/dev/null || true
fi
echo ' Immich stack created (may take 1-2 min to become ready)'
else
echo ' Immich already running'
fi
" 2>&1 | sed 's/^/ /' || true
# Tor: global hidden services - each service gets its own .onion address
echo " Setting up Tor (hidden services for each app)..."
TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)"

View File

@ -0,0 +1,333 @@
#!/bin/bash
#
# First-boot container creation for Archipelago autoinstaller
# Creates core containers so My Apps works out of the box after ISO install
# Runs after archipelago-load-images.service and archipelago-setup-tor.service
#
# Based on scripts/deploy-to-target.sh (--live) container logic - do not diverge.
# No set -e: each section continues even if one fails (idempotent, best-effort).
#
LOG="/var/log/archipelago-first-boot.log"
DOCKER=podman
command -v podman >/dev/null 2>&1 || DOCKER=docker
# Must run as root for podman
[ "$(id -u)" -eq 0 ] || { echo "Must run as root" >&2; exit 1; }
TARGET_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$TARGET_IP" ] && TARGET_IP="127.0.0.1"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; }
log "First-boot container creation starting (host=$TARGET_IP)"
# Ensure network exists (matches deploy)
$DOCKER network create archy-net 2>/dev/null || true
# 1. Bitcoin Knots (matches deploy exactly)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|archy-bitcoin-knots'; then
log "Creating Bitcoin Knots..."
mkdir -p /var/lib/archipelago/bitcoin
if $DOCKER run -d --name bitcoin-knots --restart unless-stopped --network archy-net \
-p 8332:8332 -p 8333:8333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
docker.io/bitcoinknots/bitcoin:latest \
-server=1 -txindex=1 \
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
-rpcuser=archipelago -rpcpassword=archipelago123 \
-dbcache=4096 2>>"$LOG"; then
log "Bitcoin Knots started"
else
log "Bitcoin Knots failed (may already exist)"
fi
else
$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true
log "Bitcoin Knots already running"
fi
# 2. Mempool stack (matches deploy)
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-db|mysql-mempool'; then
log "Creating mysql-mempool..."
mkdir -p /var/lib/archipelago/mysql-mempool
$DOCKER run -d --name archy-mempool-db --restart unless-stopped --network archy-net \
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e MYSQL_PASSWORD=mempoolpass \
-e MYSQL_ROOT_PASSWORD=rootpass \
docker.io/mariadb:10.11 2>>"$LOG" || true
sleep 3
fi
MYSQL_CNT=$($DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'mysql-mempool|archy-mempool-db' | head -1)
MYSQL_CNT=${MYSQL_CNT:-archy-mempool-db}
$DOCKER network connect archy-net "$MYSQL_CNT" 2>/dev/null || true
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-electrs; then
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q mempool-electrs; then
$DOCKER start mempool-electrs 2>/dev/null || true
else
log "Creating mempool-electrs..."
mkdir -p /var/lib/archipelago/mempool-electrs
$DOCKER run -d --name mempool-electrs --restart unless-stopped --network archy-net \
-p 50001:50001 -v /var/lib/archipelago/mempool-electrs:/data \
docker.io/mempool/electrs:latest \
--daemon-rpc-addr bitcoin-knots:8332 --cookie archipelago:archipelago123 \
--jsonrpc-import --electrum-rpc-addr 0.0.0.0:50001 --db-dir /data --lightmode 2>>"$LOG" || true
fi
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
log "Creating mempool-api..."
mkdir -p /var/lib/archipelago/mempool
$DOCKER run -d --name mempool-api --restart unless-stopped --network archy-net \
-p 8999:8999 -v /var/lib/archipelago/mempool:/data \
-e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=mempool-electrs -e ELECTRUM_PORT=50001 \
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
-e CORE_RPC_USERNAME=archipelago -e CORE_RPC_PASSWORD=archipelago123 \
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
-e DATABASE_USERNAME=mempool -e DATABASE_PASSWORD=mempoolpass \
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-web|mempool-web'; then
log "Creating mempool frontend..."
$DOCKER run -d --name archy-mempool-web --restart unless-stopped --network archy-net \
-p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \
docker.io/mempool/frontend:v2.5.0 2>>"$LOG" || true
fi
# 3. BTCPay stack (matches deploy)
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
log "Creating PostgreSQL for BTCPay..."
mkdir -p /var/lib/archipelago/postgres-btcpay
$DOCKER run -d --name archy-btcpay-db --restart unless-stopped --network archy-net \
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e POSTGRES_PASSWORD=btcpaypass \
docker.io/postgres:15-alpine 2>>"$LOG" || true
sleep 3
fi
# Create nbxplorer DB only if postgres is running
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
$DOCKER exec archy-btcpay-db psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \
$DOCKER exec -e PGPASSWORD=btcpaypass archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
$DOCKER start archy-nbxplorer 2>/dev/null || true
else
log "Creating NBXplorer..."
mkdir -p /var/lib/archipelago/nbxplorer
$DOCKER run -d --name archy-nbxplorer --restart unless-stopped --network archy-net \
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
-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_BTCRPCUSER=archipelago -e NBXPLORER_BTCRPCPASSWORD=archipelago123 \
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
docker.io/nicolasdorier/nbxplorer:2.6.0 2>>"$LOG" && sleep 5 || true
fi
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
log "Creating BTCPay Server..."
mkdir -p /var/lib/archipelago/btcpay
$DOCKER run -d --name btcpay-server --restart unless-stopped --network archy-net \
-p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \
-e ASPNETCORE_URLS=http://0.0.0.0:49392 -e BTCPAY_PROTOCOL=http \
-e BTCPAY_HOST="$TARGET_IP:23000" -e BTCPAY_CHAINS=btc \
-e BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 \
-e BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 \
-e BTCPAY_BTCRPCUSER=archipelago -e BTCPAY_BTCRPCPASSWORD=archipelago123 \
-e BTCPAY_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \
docker.io/btcpayserver/btcpayserver:1.13.5 2>>"$LOG" || true
fi
# 4. LND
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE '^lnd$'; then
log "Creating LND..."
mkdir -p /var/lib/archipelago/lnd
$DOCKER run -d --name lnd --restart unless-stopped --network archy-net \
-p 9735:9735 -p 10009:10009 -p 8080:8080 \
-v /var/lib/archipelago/lnd:/root/.lnd \
-e BITCOIN_ACTIVE=1 \
docker.io/lightninglabs/lnd:v0.18.4-beta 2>>"$LOG" || true
fi
# 5. Fedimint
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
log "Creating Fedimint..."
mkdir -p /var/lib/archipelago/fedimint
$DOCKER run -d --name fedimint --restart unless-stopped --network archy-net \
-p 8173:8173 -p 8174:8174 -p 8175:8175 \
-v /var/lib/archipelago/fedimint:/data \
-e FM_DATA_DIR=/data -e FM_BITCOIND_USERNAME=archipelago -e FM_BITCOIND_PASSWORD=archipelago123 \
-e FM_BITCOIN_NETWORK=bitcoin -e FM_BIND_P2P=0.0.0.0:8173 \
-e FM_BIND_API=0.0.0.0:8174 -e FM_BIND_UI=0.0.0.0:8175 \
-e FM_P2P_URL=fedimint://"$TARGET_IP":8173 -e FM_API_URL=ws://"$TARGET_IP":8174 \
-e FM_BITCOIND_URL=http://"$TARGET_IP":8332 \
docker.io/fedimint/fedimintd:v0.10.0 2>>"$LOG" || true
fi
# 6. Home Assistant
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'homeassistant|home-assistant'; then
log "Creating Home Assistant..."
mkdir -p /var/lib/archipelago/home-assistant
$DOCKER run -d --name homeassistant --restart unless-stopped \
-p 8123:8123 -v /var/lib/archipelago/home-assistant:/config \
-e TZ=UTC \
docker.io/homeassistant/home-assistant:2024.1 2>>"$LOG" || true
fi
# 7. Single-container apps (Grafana, Uptime Kuma, Jellyfin, PhotoPrism, Ollama, Vaultwarden)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then
log "Creating Grafana..."
mkdir -p /var/lib/archipelago/grafana
chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
$DOCKER run -d --name grafana --restart unless-stopped \
-p 3000:3000 -v /var/lib/archipelago/grafana:/var/lib/grafana \
-e GF_PATHS_DATA=/var/lib/grafana -e GF_USERS_ALLOW_SIGN_UP=false \
docker.io/grafana/grafana:10.2.0 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q uptime-kuma; then
log "Creating Uptime Kuma..."
mkdir -p /var/lib/archipelago/uptime-kuma
$DOCKER run -d --name uptime-kuma --restart unless-stopped \
-p 3001:3001 -v /var/lib/archipelago/uptime-kuma:/app/data \
-e TZ=UTC \
docker.io/louislam/uptime-kuma:1 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q jellyfin; then
log "Creating Jellyfin..."
mkdir -p /var/lib/archipelago/jellyfin/config /var/lib/archipelago/jellyfin/cache
$DOCKER run -d --name jellyfin --restart unless-stopped \
-p 8096:8096 \
-v /var/lib/archipelago/jellyfin/config:/config \
-v /var/lib/archipelago/jellyfin/cache:/cache \
docker.io/jellyfin/jellyfin:10.8.13 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q photoprism; then
log "Creating PhotoPrism..."
mkdir -p /var/lib/archipelago/photoprism
$DOCKER run -d --name photoprism --restart unless-stopped \
-p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \
-e PHOTOPRISM_ADMIN_PASSWORD=archipelago -e PHOTOPRISM_DEFAULT_LOCALE=en \
docker.io/photoprism/photoprism:latest 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then
log "Creating Ollama..."
mkdir -p /var/lib/archipelago/ollama
$DOCKER run -d --name ollama --restart unless-stopped \
-p 11434:11434 -v /var/lib/archipelago/ollama:/root/.ollama \
docker.io/ollama/ollama:latest 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q vaultwarden; then
log "Creating Vaultwarden..."
mkdir -p /var/lib/archipelago/vaultwarden
$DOCKER run -d --name vaultwarden --restart unless-stopped \
-p 8082:80 -v /var/lib/archipelago/vaultwarden:/data \
docker.io/vaultwarden/server:1.30.0-alpine 2>>"$LOG" || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nextcloud; then
log "Creating Nextcloud..."
mkdir -p /var/lib/archipelago/nextcloud
$DOCKER run -d --name nextcloud --restart unless-stopped \
-p 8085:80 -v /var/lib/archipelago/nextcloud:/var/www/html \
docker.io/library/nextcloud:28 2>>"$LOG" || true
fi
# Immich stack (postgres + redis + server - ML optional)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
log "Creating Immich stack..."
mkdir -p /var/lib/archipelago/immich /var/lib/archipelago/immich-db
$DOCKER network create immich-net 2>/dev/null || true
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q immich_postgres; then
$DOCKER run -d --name immich_postgres --restart unless-stopped --network immich-net \
-v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=immichpass -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>>"$LOG" || true
sleep 5
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_redis; then
$DOCKER run -d --name immich_redis --restart unless-stopped --network immich-net \
docker.io/valkey/valkey:7-alpine 2>>"$LOG" || true
sleep 2
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
$DOCKER run -d --name immich_server --restart unless-stopped --network immich-net \
-p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=immichpass \
-e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \
-e UPLOAD_LOCATION=/usr/src/app/upload \
ghcr.io/immich-app/immich-server:release 2>>"$LOG" || true
fi
fi
# Penpot stack (postgres + valkey + backend + exporter + frontend)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; then
log "Creating Penpot stack..."
mkdir -p /var/lib/archipelago/penpot-assets /var/lib/archipelago/penpot-postgres
$DOCKER network create penpot-net 2>/dev/null || true
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q penpot-postgres; then
$DOCKER run -d --name penpot-postgres --restart unless-stopped --network penpot-net \
-v /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data \
-e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e POSTGRES_PASSWORD=penpot \
docker.io/postgres:15 2>>"$LOG" || true
sleep 5
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-valkey; then
$DOCKER run -d --name penpot-valkey --restart unless-stopped --network penpot-net \
-e VALKEY_EXTRA_FLAGS="--maxmemory 128mb --maxmemory-policy volatile-lfu" \
docker.io/valkey/valkey:8.1 2>>"$LOG" || true
sleep 3
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-backend; then
$DOCKER run -d --name penpot-backend --restart unless-stopped --network penpot-net \
-v /var/lib/archipelago/penpot-assets:/opt/data/assets \
-e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \
-e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \
-e PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot \
-e PENPOT_DATABASE_USERNAME=penpot -e PENPOT_DATABASE_PASSWORD=penpot \
-e PENPOT_REDIS_URI=redis://penpot-valkey/0 \
-e PENPOT_OBJECTS_STORAGE_BACKEND=fs \
-e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \
-e PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies \
docker.io/penpotapp/backend:latest 2>>"$LOG" || true
sleep 5
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-exporter; then
$DOCKER run -d --name penpot-exporter --restart unless-stopped --network penpot-net \
-e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \
-e PENPOT_PUBLIC_URI=http://penpot-frontend:8080 \
-e PENPOT_REDIS_URI=redis://penpot-valkey/0 \
docker.io/penpotapp/exporter:latest 2>>"$LOG" || true
sleep 2
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; then
$DOCKER run -d --name penpot-frontend --restart unless-stopped --network penpot-net \
-p 9001:8080 -v /var/lib/archipelago/penpot-assets:/opt/data/assets \
-e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \
-e PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies \
docker.io/penpotapp/frontend:latest 2>>"$LOG" || true
fi
fi
# 8. Nostr relays (optional - only if images were loaded; deploy does not create these on first boot)
# nostr-rs-relay and strfry are in ISO image bundle; create if image exists
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'nostr-rs-relay'; then
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nostr-rs-relay; then
log "Creating nostr-rs-relay..."
mkdir -p /var/lib/archipelago/nostr-rs-relay
$DOCKER run -d --name nostr-rs-relay --restart unless-stopped \
-p 7047:7047 -v /var/lib/archipelago/nostr-rs-relay:/data \
scsibug/nostr-rs-relay:latest 2>>"$LOG" || true
fi
fi
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'strfry'; then
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q strfry; then
log "Creating strfry..."
mkdir -p /var/lib/archipelago/strfry
$DOCKER run -d --name strfry --restart unless-stopped \
-p 7777:7777 -v /var/lib/archipelago/strfry:/data \
hoytech/strfry:latest 2>>"$LOG" || true
fi
fi
log "First-boot container creation complete"

View File

@ -0,0 +1,48 @@
# Proxy apps that set X-Frame-Options - strip header so iframe works (Nextcloud, Vaultwarden, Immich)
location /app/nextcloud/ {
proxy_pass http://127.0.0.1:8085/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/vaultwarden/ {
proxy_pass http://127.0.0.1:8082/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
location /app/immich/ {
proxy_pass http://127.0.0.1:2283/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/penpot/ {
proxy_pass http://127.0.0.1:9001/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}

View File

@ -0,0 +1,12 @@
location /app/penpot/ {
proxy_pass http://127.0.0.1:9001/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}

View File

@ -97,9 +97,9 @@ server {
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;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
location /ws {