From a578834462d94596d41e54663b8ec66477425418 Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 21 May 2026 21:33:51 -0400 Subject: [PATCH] fix(apps): repair saleor storefront startup --- CHANGELOG.md | 7 + .../archipelago/src/api/rpc/package/stacks.rs | 215 +++++++++++------- 2 files changed, 142 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afa25e66..5f4acebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v1.7.81-alpha (2026-05-21) + +- Saleor storefront installs now use the prebuilt registry image instead of building the Next.js app on-device, avoiding Podman build failures during stack installation. +- Existing Saleor stacks are repaired on adoption by recreating missing storefront containers, forcing the storefront app to bind `0.0.0.0:3000`, and resolving nginx upstreams dynamically after container restarts. +- The shipped Saleor storefront image now includes public assets and omits Vercel-only Speed Insights injection, fixing broken static asset responses and the local `/_vercel/speed-insights/script.js` browser warning. +- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for `9011` storefront, static assets, and proxied GraphQL. + ## v1.7.80-alpha (2026-05-21) - Saleor storefront proxying now falls back to the direct request scheme when no forwarded protocol header is present, fixing direct `http://node:9011` launches that could generate an invalid same-origin GraphQL URL. diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 26a4ba61..f81ade17 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -76,6 +76,10 @@ async fn adopt_stack_if_exists( async fn repair_stack_before_adopt(stack_name: &str) { match stack_name { + "saleor" => { + repair_saleor_network_aliases().await; + let _ = start_saleor_storefront_containers().await; + } "btcpay" | "btcpay-server" => { let _ = tokio::process::Command::new("sudo") .args([ @@ -99,7 +103,6 @@ async fn repair_stack_before_adopt(stack_name: &str) { } "indeedhub" => repair_indeedhub_network_aliases().await, "netbird" => repair_netbird_unified_origin().await, - "saleor" => repair_saleor_network_aliases().await, _ => {} } } @@ -275,6 +278,53 @@ async fn repair_saleor_network_aliases() { } } +async fn start_saleor_storefront_containers() -> Result<()> { + let names = podman_container_names().await?; + + if !names.iter().any(|name| name == "saleor-storefront-app") { + pull_image_with_retry(SALEOR_STOREFRONT_IMAGE).await?; + let mut storefront_cmd = saleor_storefront_app_command(); + run_required_stack_command("saleor", "create storefront app", &mut storefront_cmd).await?; + } else { + let _ = tokio::process::Command::new("podman") + .args(["start", "saleor-storefront-app"]) + .output() + .await; + } + + write_saleor_storefront_proxy_config().await?; + if !names.iter().any(|name| name == "saleor-storefront") { + let mut proxy_cmd = saleor_storefront_proxy_command(); + run_required_stack_command("saleor", "create storefront proxy", &mut proxy_cmd).await?; + } else { + let _ = tokio::process::Command::new("podman") + .args(["start", "saleor-storefront"]) + .output() + .await; + } + + wait_for_stack_containers( + "saleor", + &["saleor-storefront-app", "saleor-storefront"], + 60, + ) + .await +} + +async fn podman_container_names() -> Result> { + let output = tokio::process::Command::new("podman") + .args(["ps", "-a", "--format", "{{.Names}}"]) + .output() + .await + .context("Failed to list containers")?; + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|name| !name.is_empty()) + .map(ToOwned::to_owned) + .collect()) +} + async fn run_required_stack_command( stack_name: &str, label: &str, @@ -447,9 +497,7 @@ const NETBIRD_SERVER_IMAGE: &str = "docker.io/netbirdio/netbird-server:0.71.2"; const NETBIRD_PROXY_IMAGE: &str = "docker.io/library/nginx:1.27-alpine"; const SALEOR_API_IMAGE: &str = "ghcr.io/saleor/saleor:3.23"; const SALEOR_DASHBOARD_IMAGE: &str = "ghcr.io/saleor/saleor-dashboard:3.23"; -const SALEOR_STOREFRONT_IMAGE: &str = "localhost/archipelago/saleor-storefront:6eb0b97"; -const SALEOR_STOREFRONT_CONTEXT: &str = - "https://github.com/saleor/storefront.git#6eb0b97b25bd4344d8139515a1cabf763d703b39"; +const SALEOR_STOREFRONT_IMAGE: &str = "146.59.87.168:3000/lfg2025/saleor-storefront:6eb0b97"; const SALEOR_POSTGRES_IMAGE: &str = "docker.io/library/postgres:15-alpine"; const SALEOR_VALKEY_IMAGE: &str = "docker.io/valkey/valkey:8.1-alpine"; const SALEOR_JAEGER_IMAGE: &str = "docker.io/jaegertracing/jaeger:latest"; @@ -457,6 +505,14 @@ const SALEOR_MAILPIT_IMAGE: &str = "docker.io/axllent/mailpit:latest"; /// Pull an image with retry and exponential backoff (3 attempts). async fn pull_image_with_retry(image: &str) -> Result<()> { + let exists = tokio::process::Command::new("podman") + .args(["image", "exists", image]) + .status() + .await; + if matches!(exists, Ok(status) if status.success()) { + return Ok(()); + } + const MAX_ATTEMPTS: u32 = 3; const BACKOFF_SECS: [u64; 3] = [5, 15, 45]; @@ -501,6 +557,73 @@ async fn pull_image_with_retry(image: &str) -> Result<()> { unreachable!() } +fn saleor_storefront_app_command() -> tokio::process::Command { + let mut cmd = tokio::process::Command::new("podman"); + cmd.args([ + "run", + "-d", + "--name", + "saleor-storefront-app", + "--network", + "saleor-net", + "--network-alias", + "storefront-app", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=512m", + "--pids-limit=2048", + "-e", + "NEXT_PUBLIC_SALEOR_API_URL=http://api:8000/graphql/", + "-e", + "NEXT_PUBLIC_STOREFRONT_URL=http://localhost:9011", + "-e", + "NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel", + "-e", + "HOSTNAME=0.0.0.0", + "-e", + "PORT=3000", + SALEOR_STOREFRONT_IMAGE, + ]); + cmd +} + +fn saleor_storefront_proxy_command() -> tokio::process::Command { + let mut cmd = tokio::process::Command::new("podman"); + cmd.args([ + "run", + "-d", + "--name", + "saleor-storefront", + "--network", + "saleor-net", + "--network-alias", + "storefront", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=NET_BIND_SERVICE", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=128m", + "--pids-limit=1024", + "-p", + "9011:80", + "-v", + "/var/lib/archipelago/saleor-storefront/nginx.conf:/etc/nginx/conf.d/default.conf:ro", + NETBIRD_PROXY_IMAGE, + ]); + cmd +} + impl RpcHandler { /// Install Immich stack (postgres + redis + server). pub(super) async fn install_immich_stack(&self) -> Result { @@ -1678,6 +1801,7 @@ impl RpcHandler { SALEOR_VALKEY_IMAGE, SALEOR_API_IMAGE, SALEOR_DASHBOARD_IMAGE, + SALEOR_STOREFRONT_IMAGE, SALEOR_JAEGER_IMAGE, SALEOR_MAILPIT_IMAGE, ]; @@ -1754,7 +1878,6 @@ impl RpcHandler { let dashboard_origin = format!("http://{}:9010", host_ip); let dashboard_url = format!("{}/", dashboard_origin); let api_url = format!("http://{}:8000/graphql/", host_ip); - let internal_api_url = "http://api:8000/graphql/"; let storefront_origin = format!("http://{}:9011", host_ip); let allowed_hosts = format!("localhost,127.0.0.1,api,saleor-api,{}", host_ip); let allowed_client_hosts = format!( @@ -2079,82 +2202,11 @@ user.save() ]); run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?; - let mut storefront_build_cmd = tokio::process::Command::new("podman"); - storefront_build_cmd.args([ - "build", - "--network", - "saleor-net", - "--pull=always", - "-t", - SALEOR_STOREFRONT_IMAGE, - "--build-arg", - &format!("NEXT_PUBLIC_SALEOR_API_URL={}", internal_api_url), - "--build-arg", - &format!("NEXT_PUBLIC_STOREFRONT_URL={}", storefront_origin), - "--build-arg", - "NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel", - SALEOR_STOREFRONT_CONTEXT, - ]); - run_required_stack_command("saleor", "build storefront", &mut storefront_build_cmd).await?; - - let mut storefront_cmd = tokio::process::Command::new("podman"); - storefront_cmd.args([ - "run", - "-d", - "--name", - "saleor-storefront-app", - "--network", - "saleor-net", - "--network-alias", - "storefront-app", - "--restart=unless-stopped", - "--cap-drop=ALL", - "--cap-add=CHOWN", - "--cap-add=DAC_OVERRIDE", - "--cap-add=FOWNER", - "--cap-add=SETGID", - "--cap-add=SETUID", - "--security-opt=no-new-privileges:true", - "--memory=512m", - "--pids-limit=2048", - "-e", - &format!("NEXT_PUBLIC_SALEOR_API_URL={}", internal_api_url), - "-e", - &format!("NEXT_PUBLIC_STOREFRONT_URL={}", storefront_origin), - "-e", - "NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel", - SALEOR_STOREFRONT_IMAGE, - ]); + let mut storefront_cmd = saleor_storefront_app_command(); run_required_stack_command("saleor", "create storefront app", &mut storefront_cmd).await?; write_saleor_storefront_proxy_config().await?; - let mut storefront_proxy_cmd = tokio::process::Command::new("podman"); - storefront_proxy_cmd.args([ - "run", - "-d", - "--name", - "saleor-storefront", - "--network", - "saleor-net", - "--network-alias", - "storefront", - "--restart=unless-stopped", - "--cap-drop=ALL", - "--cap-add=CHOWN", - "--cap-add=DAC_OVERRIDE", - "--cap-add=FOWNER", - "--cap-add=NET_BIND_SERVICE", - "--cap-add=SETGID", - "--cap-add=SETUID", - "--security-opt=no-new-privileges:true", - "--memory=128m", - "--pids-limit=1024", - "-p", - "9011:80", - "-v", - "/var/lib/archipelago/saleor-storefront/nginx.conf:/etc/nginx/conf.d/default.conf:ro", - NETBIRD_PROXY_IMAGE, - ]); + let mut storefront_proxy_cmd = saleor_storefront_proxy_command(); run_required_stack_command( "saleor", "create storefront proxy", @@ -2242,6 +2294,7 @@ async fn write_saleor_storefront_proxy_config() -> Result<()> { server { listen 80; server_name _; + resolver 10.89.4.1 valid=10s ipv6=off; proxy_http_version 1.1; proxy_set_header Host $host; @@ -2250,13 +2303,15 @@ server { proxy_set_header X-Forwarded-Proto $scheme; location ^~ /graphql/ { - proxy_pass http://api:8000/graphql/; + set $saleor_api http://api:8000/graphql/; + proxy_pass $saleor_api; proxy_set_header Host api; proxy_set_header Origin ""; } location / { - proxy_pass http://storefront-app:3000; + set $saleor_storefront_app http://storefront-app:3000; + proxy_pass $saleor_storefront_app; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Accept-Encoding "";