From 4cb9ac1faaff59ad8fd32c56ce391dcd3b7678d7 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 25 Feb 2026 18:04:41 +0000 Subject: [PATCH] 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. --- core/archipelago/src/api/rpc.rs | 280 ++++++++++++++- .../src/container/docker_packages.rs | 7 +- image-recipe/build-auto-installer-iso.sh | 34 +- image-recipe/configs/nginx-archipelago.conf | 52 ++- neode-ui/src/stores/appLauncher.ts | 25 +- neode-ui/src/views/AppDetails.vue | 39 +- neode-ui/src/views/Marketplace.vue | 12 +- scripts/deploy-to-target.sh | 58 +++ scripts/first-boot-containers.sh | 333 ++++++++++++++++++ scripts/nginx-app-iframe-patch.conf | 48 +++ scripts/nginx-penpot-iframe-patch.conf | 12 + scripts/setup-https-dev.sh | 6 +- 12 files changed, 876 insertions(+), 30 deletions(-) create mode 100644 scripts/first-boot-containers.sh create mode 100644 scripts/nginx-app-iframe-patch.conf create mode 100644 scripts/nginx-penpot-iframe-patch.conf diff --git a/core/archipelago/src/api/rpc.rs b/core/archipelago/src/api/rpc.rs index c321354a..1b089297 100644 --- a/core/archipelago/src/api/rpc.rs +++ b/core/archipelago/src/api/rpc.rs @@ -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); @@ -761,6 +779,213 @@ impl RpcHandler { "message": format!("Package {} installed and started", package_id) })) } + + /// Install Immich stack (postgres + redis + server) + async fn install_immich_stack(&self) -> Result { + 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 { + 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( @@ -777,8 +1002,12 @@ impl RpcHandler { let to_start: Vec = 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> { ] } "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 { 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, ), diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index ac11d40b..aa540af4 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -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 diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 5a4d2e55..97c63d2b 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -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 diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 9e2a6f13..ca6caa36 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -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 diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index 98a38645..7efa47fc 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -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 } diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 847ee50e..c86b06cb 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -456,6 +456,23 @@ const ROUTE_TO_PACKAGE_KEY: Record = { '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]) { diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index c76d0adb..be5cccba 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -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 = { + 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' }, diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index c267bc70..bd43a768 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -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)" diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh new file mode 100644 index 00000000..01dc489c --- /dev/null +++ b/scripts/first-boot-containers.sh @@ -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" diff --git a/scripts/nginx-app-iframe-patch.conf b/scripts/nginx-app-iframe-patch.conf new file mode 100644 index 00000000..d75384c4 --- /dev/null +++ b/scripts/nginx-app-iframe-patch.conf @@ -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; + } + diff --git a/scripts/nginx-penpot-iframe-patch.conf b/scripts/nginx-penpot-iframe-patch.conf new file mode 100644 index 00000000..64769a49 --- /dev/null +++ b/scripts/nginx-penpot-iframe-patch.conf @@ -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; + } diff --git a/scripts/setup-https-dev.sh b/scripts/setup-https-dev.sh index e07ed154..210852a0 100644 --- a/scripts/setup-https-dev.sh +++ b/scripts/setup-https-dev.sh @@ -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 {