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);
@@ -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<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(
@@ -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,
),