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:
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user