From e429738ece000ca5cdf969782de23bb89463fc46 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 001/153] feat: generate dynamic proxy dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-dashboard.sh | 202 +++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100755 .docker/scripts/proxy-dashboard.sh diff --git a/.docker/scripts/proxy-dashboard.sh b/.docker/scripts/proxy-dashboard.sh new file mode 100755 index 00000000..8258c065 --- /dev/null +++ b/.docker/scripts/proxy-dashboard.sh @@ -0,0 +1,202 @@ +#!/bin/sh + +set -eu + +html_dir="${HTML_DIR:-/html}" +output="$html_dir/index.html" + +html_escape() { + printf '%s' "$1" | + sed \ + -e 's/&/\&/g' \ + -e 's//\>/g' \ + -e 's/"/\"/g' \ + -e "s/'/\'/g" +} + +service_title() { + case "$1" in + nginx) + printf 'Nextcloud' + ;; + mailpit) + printf 'Mailpit' + ;; + playwright) + printf 'Playwright' + ;; + eurooffice) + printf 'EuroOffice' + ;; + signal-gateway) + printf 'Signal Gateway' + ;; + *) + printf '%s' "$1" + ;; + esac +} + +active_hosts() { + docker ps --filter network=librecode-dev-proxy --format '{{.ID}}' | + while IFS= read -r container; do + [ -n "$container" ] || continue + + virtual_hosts="$(docker inspect \ + --format '{{range .Config.Env}}{{println .}}{{end}}' \ + "$container" | + sed -n 's/^VIRTUAL_HOST=//p' | + head -n 1)" + + [ -n "$virtual_hosts" ] || continue + + project="$(docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ + "$container" 2>/dev/null || true)" + service="$(docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.service" }}' \ + "$container" 2>/dev/null || true)" + + [ -n "$project" ] || project="Docker" + [ -n "$service" ] || service="Service" + + printf '%s\n' "$virtual_hosts" | + tr ',' '\n' | + while IFS= read -r host; do + host="$(printf '%s' "$host" | tr -d '[:space:]')" + case "$host" in + *.localhost) + printf '%s|%s|%s\n' "$project" "$service" "$host" + ;; + esac + done + done | + sort -u +} + +generate_dashboard() { + tmp="${output}.tmp" + hosts_file="$(mktemp)" + trap 'rm -f "$hosts_file" "$tmp"' EXIT INT TERM + + active_hosts > "$hosts_file" + + cat > "$tmp" <<'EOF' + + + + + + LibreCode Development Proxy + + + +
+

πŸ’™ LibreCode Development Proxy

+ +

The shared development proxy is running.

+

Active environments

+EOF + + if [ ! -s "$hosts_file" ]; then + printf '

No development environments are active.

\n' >> "$tmp" + else + current_project="" + while IFS='|' read -r project service host; do + project_html="$(html_escape "$project")" + service_html="$(html_escape "$(service_title "$service")")" + host_html="$(html_escape "$host")" + + if [ "$project" != "$current_project" ]; then + if [ -n "$current_project" ]; then + printf ' \n' >> "$tmp" + fi + printf '

%s

\n \n' >> "$tmp" + fi + + cat >> "$tmp" <<'EOF' +

This page is generated from the services currently connected to the shared development proxy.

+
+ + + +EOF + + mv "$tmp" "$output" + rm -f "$hosts_file" + trap - EXIT INT TERM +} + +mkdir -p "$html_dir" +generate_dashboard + +while true; do + docker events \ + --filter type=container \ + --filter event=start \ + --filter event=stop \ + --filter event=die \ + --filter event=destroy \ + --format '{{.Status}}' | + while IFS= read -r _event; do + generate_dashboard + done + + sleep 1 +done From abf38757a5a6b6c47d022f75552f5de4597843eb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 002/153] feat: serve proxy dashboard and friendly not found page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .docker/nginx-proxy/dashboard.conf diff --git a/.docker/nginx-proxy/dashboard.conf b/.docker/nginx-proxy/dashboard.conf new file mode 100644 index 00000000..37fe983c --- /dev/null +++ b/.docker/nginx-proxy/dashboard.conf @@ -0,0 +1,23 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /index.html { + internal; + } + + location / { + if ($host != localhost) { + return 404; + } + + try_files /index.html =404; + } + + error_page 404 =404 /index.html; +} From dbfcbd063ee34d55a83389df7fd015ac889da6fe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 003/153] refactor: retire static localhost virtual host Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/localhost.conf | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.docker/nginx-proxy/localhost.conf b/.docker/nginx-proxy/localhost.conf index fef7b775..bf2a2824 100644 --- a/.docker/nginx-proxy/localhost.conf +++ b/.docker/nginx-proxy/localhost.conf @@ -1,13 +1,3 @@ -server { - listen 80; - listen [::]:80; - - server_name localhost; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files /index.html =404; - } -} +# localhost is now provided by the proxy-dashboard service. +# This file remains temporarily so existing coordinator installations can +# replace the previous static server block in the shared configuration volume. From 98734e526880e65b6fc6e0012ed35b7ab7e8d78d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 004/153] feat: add dynamic proxy dashboard services Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index d0767fa7..9bdd4353 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -17,6 +17,37 @@ services: - librecode-dev-proxy-html:/usr/share/nginx/html environment: - ENABLE_IPV6=true + - DEFAULT_HOST=localhost + networks: + - proxy + + proxy-dashboard: + image: nginx:alpine + container_name: librecode-dev-proxy-dashboard + restart: unless-stopped + labels: + coop.librecode.dev-proxy-dashboard: "true" + volumes: + - librecode-dev-proxy-html:/usr/share/nginx/html:ro + - ./.docker/nginx-proxy/dashboard.conf:/etc/nginx/conf.d/default.conf:ro + environment: + - VIRTUAL_HOST=localhost + - VIRTUAL_PORT=80 + - SELF_SIGNED_HOST=localhost + networks: + - proxy + + proxy-dashboard-updater: + image: docker:29-cli + container_name: librecode-dev-proxy-dashboard-updater + restart: unless-stopped + labels: + coop.librecode.dev-proxy-dashboard-updater: "true" + volumes: + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro + - librecode-dev-proxy-html:/html + - ./.docker/scripts/proxy-dashboard.sh:/usr/local/bin/proxy-dashboard.sh:ro + command: /bin/sh /usr/local/bin/proxy-dashboard.sh networks: - proxy From 2e2a6719b06b4014bfb5b50461d22c1f7718e59e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 005/153] fix: reconcile shared proxy services when reused Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 100644 => 100755 .docker/scripts/proxy-coordinator.sh diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh old mode 100644 new mode 100755 index 8ac711c8..d6b9d519 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -234,6 +234,7 @@ compose config --quiet install_proxy_assets if proxy_is_ready; then + proxy_compose up --detach proxy_state=reused else ensure_ports_available @@ -251,4 +252,4 @@ if ! report_environment_ready; then echo 'Could not print environment banner.' >&2 fi -success "$proxy_state" \ No newline at end of file +success "$proxy_state" From 672d7b51d13eede6aae6f2f840d51f70a46cfba0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 006/153] fix: route unknown localhost hosts to dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index 9bdd4353..4f815bd8 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -17,7 +17,6 @@ services: - librecode-dev-proxy-html:/usr/share/nginx/html environment: - ENABLE_IPV6=true - - DEFAULT_HOST=localhost networks: - proxy @@ -31,8 +30,9 @@ services: - librecode-dev-proxy-html:/usr/share/nginx/html:ro - ./.docker/nginx-proxy/dashboard.conf:/etc/nginx/conf.d/default.conf:ro environment: - - VIRTUAL_HOST=localhost + - VIRTUAL_HOST=localhost,~^.+[.]localhost$$ - VIRTUAL_PORT=80 + - CERT_NAME=localhost - SELF_SIGNED_HOST=localhost networks: - proxy From 06b2b41acdbc2148498a0632cbe802cda508174e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 007/153] fix: use wildcard host for proxy dashboard fallback Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index 4f815bd8..e013d730 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -30,10 +30,9 @@ services: - librecode-dev-proxy-html:/usr/share/nginx/html:ro - ./.docker/nginx-proxy/dashboard.conf:/etc/nginx/conf.d/default.conf:ro environment: - - VIRTUAL_HOST=localhost,~^.+[.]localhost$$ + - VIRTUAL_HOST=localhost,*.localhost - VIRTUAL_PORT=80 - - CERT_NAME=localhost - - SELF_SIGNED_HOST=localhost + - SELF_SIGNED_HOST=localhost,*.localhost networks: - proxy From f379db0fd5af468d176d9766f9fb4007e79b3e33 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 008/153] fix: keep proxy dashboard routes in sync Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-dashboard.sh | 55 +++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/.docker/scripts/proxy-dashboard.sh b/.docker/scripts/proxy-dashboard.sh index 8258c065..c22bb91d 100755 --- a/.docker/scripts/proxy-dashboard.sh +++ b/.docker/scripts/proxy-dashboard.sh @@ -4,6 +4,8 @@ set -eu html_dir="${HTML_DIR:-/html}" output="$html_dir/index.html" +proxy_network="${PROXY_NETWORK:-librecode-dev-proxy}" +proxy_project="${PROXY_PROJECT:-librecode-dev-proxy}" html_escape() { printf '%s' "$1" | @@ -39,18 +41,10 @@ service_title() { } active_hosts() { - docker ps --filter network=librecode-dev-proxy --format '{{.ID}}' | + docker ps --filter "network=$proxy_network" --format '{{.ID}}' | while IFS= read -r container; do [ -n "$container" ] || continue - virtual_hosts="$(docker inspect \ - --format '{{range .Config.Env}}{{println .}}{{end}}' \ - "$container" | - sed -n 's/^VIRTUAL_HOST=//p' | - head -n 1)" - - [ -n "$virtual_hosts" ] || continue - project="$(docker inspect \ --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ "$container" 2>/dev/null || true)" @@ -58,6 +52,17 @@ active_hosts() { --format '{{ index .Config.Labels "com.docker.compose.service" }}' \ "$container" 2>/dev/null || true)" + # The shared proxy infrastructure exposes its own routing metadata, but it + # is not a development environment and must not appear in the dashboard. + [ "$project" != "$proxy_project" ] || continue + + virtual_hosts="$(docker inspect \ + --format '{{range .Config.Env}}{{println .}}{{end}}' \ + "$container" | + sed -n 's/^VIRTUAL_HOST=//p' | + head -n 1)" + + [ -n "$virtual_hosts" ] || continue [ -n "$project" ] || project="Docker" [ -n "$service" ] || service="Service" @@ -67,6 +72,11 @@ active_hosts() { host="$(printf '%s' "$host" | tr -d '[:space:]')" case "$host" in *.localhost) + case "$host" in + '*.'*|'~'*) + continue + ;; + esac printf '%s|%s|%s\n' "$project" "$service" "$host" ;; esac @@ -187,13 +197,26 @@ mkdir -p "$html_dir" generate_dashboard while true; do - docker events \ - --filter type=container \ - --filter event=start \ - --filter event=stop \ - --filter event=die \ - --filter event=destroy \ - --format '{{.Status}}' | + { + docker events \ + --filter type=container \ + --filter event=start \ + --filter event=stop \ + --filter event=die \ + --filter event=destroy \ + --format '{{.Status}}' & + container_events_pid=$! + + docker events \ + --filter type=network \ + --filter "network=$proxy_network" \ + --filter event=connect \ + --filter event=disconnect \ + --format '{{.Status}}' & + network_events_pid=$! + + wait "$container_events_pid" "$network_events_pid" + } | while IFS= read -r _event; do generate_dashboard done From 78b0588b016f80cdb920c024aeb9ffe5b9998bfb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 009/153] feat: extend nginx-proxy for dynamic dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .docker/nginx-proxy/Dockerfile diff --git a/.docker/nginx-proxy/Dockerfile b/.docker/nginx-proxy/Dockerfile new file mode 100644 index 00000000..6941b92b --- /dev/null +++ b/.docker/nginx-proxy/Dockerfile @@ -0,0 +1,5 @@ +FROM nginxproxy/nginx-proxy:1.11-alpine + +COPY Procfile /app/Procfile +COPY dashboard.tmpl /app/dashboard.tmpl +COPY dashboard.conf /etc/nginx/conf.d/dashboard.conf From 22a12ccd334475cc4593b9310d3cef14dc24ed20 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 010/153] feat: watch Docker state for dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/Procfile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .docker/nginx-proxy/Procfile diff --git a/.docker/nginx-proxy/Procfile b/.docker/nginx-proxy/Procfile new file mode 100644 index 00000000..dff02fed --- /dev/null +++ b/.docker/nginx-proxy/Procfile @@ -0,0 +1,3 @@ +dockergen: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf +dashboard: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect /app/dashboard.tmpl /usr/share/nginx/html/index.html +nginx: nginx -g "daemon off;" From 2f6e956e23f38fe9f0f54e0455783b5853c9ba30 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 011/153] feat: render dashboard from Docker metadata Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 147 +++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 .docker/nginx-proxy/dashboard.tmpl diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl new file mode 100644 index 00000000..b8c31fb0 --- /dev/null +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -0,0 +1,147 @@ +{{- $routes := list -}} +{{- range $hostname, $containers := groupByMulti . "Env.VIRTUAL_HOST" "," -}} + {{- $hostname = trim $hostname -}} + {{- if or (eq $hostname "") (eq $hostname "localhost") (eq $hostname "*.localhost") -}} + {{- continue -}} + {{- end -}} + + {{- range $container := $containers -}} + {{- $onProxyNetwork := false -}} + {{- range $network := $container.Networks -}} + {{- if eq $network.Name "librecode-dev-proxy" -}} + {{- $onProxyNetwork = true -}} + {{- end -}} + {{- end -}} + {{- if not $onProxyNetwork -}} + {{- continue -}} + {{- end -}} + + {{- $project := index $container.Labels "com.docker.compose.project" -}} + {{- $service := index $container.Labels "com.docker.compose.service" -}} + {{- if not $project -}} + {{- $project = "Docker" -}} + {{- end -}} + {{- if not $service -}} + {{- $service = "Service" -}} + {{- end -}} + + {{- $routes = append $routes (dict "host" $hostname "project" $project "service" $service) -}} + {{- break -}} + {{- end -}} +{{- end -}} + + + + + + LibreCode Development Proxy + + + +
+

πŸ’™ LibreCode Development Proxy

+ + +

The shared development proxy is running.

+

Active environments

+
+

This page is generated from the routes currently connected to the shared development proxy.

+
+ + + + + From 73b927244fd31b285e85811393010b469efffb4b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 012/153] refactor: serve dashboard inside nginx-proxy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/nginx-proxy/dashboard.conf b/.docker/nginx-proxy/dashboard.conf index 37fe983c..52e31c3f 100644 --- a/.docker/nginx-proxy/dashboard.conf +++ b/.docker/nginx-proxy/dashboard.conf @@ -1,6 +1,6 @@ server { - listen 80 default_server; - listen [::]:80 default_server; + listen 8080; + listen [::]:8080; server_name _; From db330e16975d69be953dc654f45868983e4f08ec Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 013/153] refactor: keep dashboard inside proxy service Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 33 ++++---------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index e013d730..1262fcf5 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -1,6 +1,8 @@ services: nginx-proxy: - image: nginxproxy/nginx-proxy:1.11-alpine + build: + context: ./.docker/nginx-proxy + dockerfile: Dockerfile container_name: librecode-dev-proxy restart: unless-stopped labels: @@ -17,39 +19,12 @@ services: - librecode-dev-proxy-html:/usr/share/nginx/html environment: - ENABLE_IPV6=true - networks: - - proxy - - proxy-dashboard: - image: nginx:alpine - container_name: librecode-dev-proxy-dashboard - restart: unless-stopped - labels: - coop.librecode.dev-proxy-dashboard: "true" - volumes: - - librecode-dev-proxy-html:/usr/share/nginx/html:ro - - ./.docker/nginx-proxy/dashboard.conf:/etc/nginx/conf.d/default.conf:ro - environment: - VIRTUAL_HOST=localhost,*.localhost - - VIRTUAL_PORT=80 + - VIRTUAL_PORT=8080 - SELF_SIGNED_HOST=localhost,*.localhost networks: - proxy - proxy-dashboard-updater: - image: docker:29-cli - container_name: librecode-dev-proxy-dashboard-updater - restart: unless-stopped - labels: - coop.librecode.dev-proxy-dashboard-updater: "true" - volumes: - - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro - - librecode-dev-proxy-html:/html - - ./.docker/scripts/proxy-dashboard.sh:/usr/local/bin/proxy-dashboard.sh:ro - command: /bin/sh /usr/local/bin/proxy-dashboard.sh - networks: - - proxy - ssl-companion: image: sebastienheyd/self-signed-proxy-companion:latest container_name: librecode-dev-proxy-ssl-companion From 912d5a52a05be32f2686cd75c53a773afa8d1e6f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 014/153] refactor: let proxy own dashboard assets Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index d6b9d519..d9a1c9dc 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -206,17 +206,6 @@ report_environment_ready() { nextcloud sh /var/www/scripts/report-environment-ready } -install_proxy_assets() { - docker run --rm \ - -v librecode-dev-proxy-conf:/conf \ - -v librecode-dev-proxy-html:/html \ - -v "$PROJECT_DIR/.docker/nginx-proxy:/source:ro" \ - alpine sh -c ' - cp /source/localhost.conf /conf/librecode-localhost.conf - cp /source/index.html /html/index.html - ' -} - success() { case "$1" in reused) @@ -231,8 +220,6 @@ success() { echo "Validating Compose project ${project} at ${PROJECT_DIR}." compose config --quiet -install_proxy_assets - if proxy_is_ready; then proxy_compose up --detach proxy_state=reused From a4e1d6d2500b76adff12d385450ec2f132a0f579 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 015/153] refactor: remove dashboard updater service script Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-dashboard.sh | 225 ----------------------------- 1 file changed, 225 deletions(-) delete mode 100755 .docker/scripts/proxy-dashboard.sh diff --git a/.docker/scripts/proxy-dashboard.sh b/.docker/scripts/proxy-dashboard.sh deleted file mode 100755 index c22bb91d..00000000 --- a/.docker/scripts/proxy-dashboard.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/sh - -set -eu - -html_dir="${HTML_DIR:-/html}" -output="$html_dir/index.html" -proxy_network="${PROXY_NETWORK:-librecode-dev-proxy}" -proxy_project="${PROXY_PROJECT:-librecode-dev-proxy}" - -html_escape() { - printf '%s' "$1" | - sed \ - -e 's/&/\&/g' \ - -e 's//\>/g' \ - -e 's/"/\"/g' \ - -e "s/'/\'/g" -} - -service_title() { - case "$1" in - nginx) - printf 'Nextcloud' - ;; - mailpit) - printf 'Mailpit' - ;; - playwright) - printf 'Playwright' - ;; - eurooffice) - printf 'EuroOffice' - ;; - signal-gateway) - printf 'Signal Gateway' - ;; - *) - printf '%s' "$1" - ;; - esac -} - -active_hosts() { - docker ps --filter "network=$proxy_network" --format '{{.ID}}' | - while IFS= read -r container; do - [ -n "$container" ] || continue - - project="$(docker inspect \ - --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ - "$container" 2>/dev/null || true)" - service="$(docker inspect \ - --format '{{ index .Config.Labels "com.docker.compose.service" }}' \ - "$container" 2>/dev/null || true)" - - # The shared proxy infrastructure exposes its own routing metadata, but it - # is not a development environment and must not appear in the dashboard. - [ "$project" != "$proxy_project" ] || continue - - virtual_hosts="$(docker inspect \ - --format '{{range .Config.Env}}{{println .}}{{end}}' \ - "$container" | - sed -n 's/^VIRTUAL_HOST=//p' | - head -n 1)" - - [ -n "$virtual_hosts" ] || continue - [ -n "$project" ] || project="Docker" - [ -n "$service" ] || service="Service" - - printf '%s\n' "$virtual_hosts" | - tr ',' '\n' | - while IFS= read -r host; do - host="$(printf '%s' "$host" | tr -d '[:space:]')" - case "$host" in - *.localhost) - case "$host" in - '*.'*|'~'*) - continue - ;; - esac - printf '%s|%s|%s\n' "$project" "$service" "$host" - ;; - esac - done - done | - sort -u -} - -generate_dashboard() { - tmp="${output}.tmp" - hosts_file="$(mktemp)" - trap 'rm -f "$hosts_file" "$tmp"' EXIT INT TERM - - active_hosts > "$hosts_file" - - cat > "$tmp" <<'EOF' - - - - - - LibreCode Development Proxy - - - -
-

πŸ’™ LibreCode Development Proxy

- -

The shared development proxy is running.

-

Active environments

-EOF - - if [ ! -s "$hosts_file" ]; then - printf '

No development environments are active.

\n' >> "$tmp" - else - current_project="" - while IFS='|' read -r project service host; do - project_html="$(html_escape "$project")" - service_html="$(html_escape "$(service_title "$service")")" - host_html="$(html_escape "$host")" - - if [ "$project" != "$current_project" ]; then - if [ -n "$current_project" ]; then - printf ' \n' >> "$tmp" - fi - printf '

%s

\n \n' >> "$tmp" - fi - - cat >> "$tmp" <<'EOF' -

This page is generated from the services currently connected to the shared development proxy.

-
- - - -EOF - - mv "$tmp" "$output" - rm -f "$hosts_file" - trap - EXIT INT TERM -} - -mkdir -p "$html_dir" -generate_dashboard - -while true; do - { - docker events \ - --filter type=container \ - --filter event=start \ - --filter event=stop \ - --filter event=die \ - --filter event=destroy \ - --format '{{.Status}}' & - container_events_pid=$! - - docker events \ - --filter type=network \ - --filter "network=$proxy_network" \ - --filter event=connect \ - --filter event=disconnect \ - --format '{{.Status}}' & - network_events_pid=$! - - wait "$container_events_pid" "$network_events_pid" - } | - while IFS= read -r _event; do - generate_dashboard - done - - sleep 1 -done From 0c64f90f35e1f92ed036c6fea83e33c620d47f7f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 016/153] refactor: remove legacy localhost proxy config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/localhost.conf | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .docker/nginx-proxy/localhost.conf diff --git a/.docker/nginx-proxy/localhost.conf b/.docker/nginx-proxy/localhost.conf deleted file mode 100644 index bf2a2824..00000000 --- a/.docker/nginx-proxy/localhost.conf +++ /dev/null @@ -1,3 +0,0 @@ -# localhost is now provided by the proxy-dashboard service. -# This file remains temporarily so existing coordinator installations can -# replace the previous static server block in the shared configuration volume. From 288bdb197e98266fe953689f47a35c95358087b0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 017/153] refactor: remove static proxy dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/index.html | 72 ---------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 .docker/nginx-proxy/index.html diff --git a/.docker/nginx-proxy/index.html b/.docker/nginx-proxy/index.html deleted file mode 100644 index ea292373..00000000 --- a/.docker/nginx-proxy/index.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - LibreCode Development Proxy - - - -
-

πŸ’™ LibreCode Development Proxy

- -

The shared development proxy is running.

- -

- Open your environment using the URL shown by - docker compose up. -

- -

- The hostname is usually based on the project directory name: -

- -

- php83-master/ - β†’ https://php83-master.localhost -

- -

- Requests to *.localhost are routed automatically. -

-
- - From 84dba5d5be13c63098c805e94190505d45a08666 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 018/153] refactor: keep generated proxy files ephemeral Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index 1262fcf5..fe4f1ad3 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -13,10 +13,8 @@ services: volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/tmp/docker.sock:ro - librecode-dev-proxy-vhost:/etc/nginx/vhost.d - - librecode-dev-proxy-conf:/etc/nginx/conf.d - librecode-dev-proxy-log:/etc/nginx/log - librecode-dev-proxy-certs:/etc/nginx/certs:ro - - librecode-dev-proxy-html:/usr/share/nginx/html environment: - ENABLE_IPV6=true - VIRTUAL_HOST=localhost,*.localhost @@ -46,11 +44,7 @@ networks: volumes: librecode-dev-proxy-vhost: name: librecode-dev-proxy-vhost - librecode-dev-proxy-conf: - name: librecode-dev-proxy-conf librecode-dev-proxy-log: name: librecode-dev-proxy-log librecode-dev-proxy-certs: name: librecode-dev-proxy-certs - librecode-dev-proxy-html: - name: librecode-dev-proxy-html From 3d724a0757764fed93fb7625c05c00f1e5f47238 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 019/153] feat: generate proxy config and dashboard together Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/docker-gen.cfg | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .docker/nginx-proxy/docker-gen.cfg diff --git a/.docker/nginx-proxy/docker-gen.cfg b/.docker/nginx-proxy/docker-gen.cfg new file mode 100644 index 00000000..484d5801 --- /dev/null +++ b/.docker/nginx-proxy/docker-gen.cfg @@ -0,0 +1,19 @@ +[[config]] +template = "/app/nginx.tmpl" +dest = "/etc/nginx/conf.d/default.conf" +watch = true +wait = "100ms:500ms" +notifycmd = "nginx -s reload" + +[[config]] +template = "/dashboard/dashboard.conf.tmpl" +dest = "/etc/nginx/conf.d/dashboard.conf" +watch = true +wait = "100ms:500ms" +notifycmd = "nginx -s reload" + +[[config]] +template = "/dashboard/dashboard.tmpl" +dest = "/usr/share/nginx/html/index.html" +watch = true +wait = "100ms:500ms" From 06ee3cb218ac45c93eef3cc7ba5c81c8f652ee59 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:07 -0300 Subject: [PATCH 020/153] feat: serve dashboard from localhost vhost Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/localhost_location_override | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .docker/nginx-proxy/localhost_location_override diff --git a/.docker/nginx-proxy/localhost_location_override b/.docker/nginx-proxy/localhost_location_override new file mode 100644 index 00000000..a831b0aa --- /dev/null +++ b/.docker/nginx-proxy/localhost_location_override @@ -0,0 +1,2 @@ +root /usr/share/nginx/html; +try_files /index.html =404; From 3e6e45bf87de3738bf3a7aa1f09da78ec08ff878 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 021/153] feat: render dashboard for unknown localhost hosts Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/*.localhost | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .docker/nginx-proxy/*.localhost diff --git a/.docker/nginx-proxy/*.localhost b/.docker/nginx-proxy/*.localhost new file mode 100644 index 00000000..a2450af0 --- /dev/null +++ b/.docker/nginx-proxy/*.localhost @@ -0,0 +1,6 @@ +root /usr/share/nginx/html; +error_page 404 =404 /index.html; + +location = /index.html { + internal; +} From 1d2224f3676979189c932674cff154c012912864 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 022/153] feat: return 404 for unknown localhost hosts Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/*.localhost_location_override | 1 + 1 file changed, 1 insertion(+) create mode 100644 .docker/nginx-proxy/*.localhost_location_override diff --git a/.docker/nginx-proxy/*.localhost_location_override b/.docker/nginx-proxy/*.localhost_location_override new file mode 100644 index 00000000..421c127f --- /dev/null +++ b/.docker/nginx-proxy/*.localhost_location_override @@ -0,0 +1 @@ +return 404; From 14f796bc41797be90cf4a6ae9f553a89c1a59091 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 023/153] refactor: run dashboard generator from stock proxy image Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/nginx-proxy/Procfile b/.docker/nginx-proxy/Procfile index dff02fed..69b47b8e 100644 --- a/.docker/nginx-proxy/Procfile +++ b/.docker/nginx-proxy/Procfile @@ -1,3 +1,3 @@ dockergen: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf -dashboard: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect /app/dashboard.tmpl /usr/share/nginx/html/index.html +dashboard: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect /dashboard/dashboard.tmpl /usr/share/nginx/html/index.html nginx: nginx -g "daemon off;" From 3ae587287a3dc8aabdc9a6cd2f864296c559f57b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 024/153] refactor: run shared proxy without local build Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index fe4f1ad3..0abd44c5 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -1,24 +1,24 @@ services: nginx-proxy: - build: - context: ./.docker/nginx-proxy - dockerfile: Dockerfile + image: nginxproxy/nginx-proxy:1.11-alpine container_name: librecode-dev-proxy - restart: unless-stopped labels: coop.librecode.dev-proxy: "true" + working_dir: /dashboard + command: ["forego", "start", "-r"] ports: - "80:80" - "443:443" volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/tmp/docker.sock:ro - librecode-dev-proxy-vhost:/etc/nginx/vhost.d + - librecode-dev-proxy-assets:/dashboard:ro - librecode-dev-proxy-log:/etc/nginx/log - librecode-dev-proxy-certs:/etc/nginx/certs:ro environment: - ENABLE_IPV6=true - VIRTUAL_HOST=localhost,*.localhost - - VIRTUAL_PORT=8080 + - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=localhost,*.localhost networks: - proxy @@ -26,7 +26,6 @@ services: ssl-companion: image: sebastienheyd/self-signed-proxy-companion:latest container_name: librecode-dev-proxy-ssl-companion - restart: unless-stopped labels: coop.librecode.dev-proxy-companion: "true" volumes: @@ -44,6 +43,8 @@ networks: volumes: librecode-dev-proxy-vhost: name: librecode-dev-proxy-vhost + librecode-dev-proxy-assets: + name: librecode-dev-proxy-assets librecode-dev-proxy-log: name: librecode-dev-proxy-log librecode-dev-proxy-certs: From f8a49d3d2b6514c17c1c9c27b9a27cc79784f88d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 025/153] refactor: run proxy coordinator without local build Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c44c5947..c8c5a29d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,11 +64,13 @@ services: - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: - build: - context: .docker/ - dockerfile: Dockerfile.proxy-coordinator + image: docker:29-cli + entrypoint: ["/bin/sh", "/usr/local/bin/proxy-coordinator.sh"] + labels: + coop.librecode.dev-proxy-client: "true" volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + - ./.docker/scripts/proxy-coordinator.sh:/usr/local/bin/proxy-coordinator.sh:ro - .:${PWD}:ro working_dir: ${PWD} environment: @@ -77,6 +79,7 @@ services: - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} restart: "no" + stop_grace_period: 15s depends_on: - nextcloud mailpit: From fb95f2c757991debfd8802d67210c82f7419ba15 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 026/153] feat: release shared proxy with last environment Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 105 +++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 6 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index d9a1c9dc..96c673c1 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -5,6 +5,9 @@ set -eu proxy_project=librecode-dev-proxy proxy_network=librecode-dev-proxy proxy_label=coop.librecode.dev-proxy=true +proxy_client_label=coop.librecode.dev-proxy-client=true +proxy_assets_volume=librecode-dev-proxy-assets +proxy_vhost_volume=librecode-dev-proxy-vhost compose_project() { docker inspect \ @@ -124,7 +127,6 @@ start_proxy() { return 0 fi - # Another checkout may have created the shared proxy concurrently. if proxy_is_ready; then return 0 fi @@ -140,6 +142,55 @@ start_proxy() { exit 1 } +copy_to_named_volume() { + volume="$1" + source="$2" + destination="$3" + + docker run --rm -i \ + -v "$volume:/target" \ + docker:29-cli \ + sh -c 'cat > "/target/$1"' sh "$destination" \ + < "$source" +} + +install_proxy_assets() { + docker volume create "$proxy_assets_volume" >/dev/null + docker volume create "$proxy_vhost_volume" >/dev/null + + docker run --rm \ + -v "$proxy_assets_volume:/target" \ + docker:29-cli \ + sh -c 'rm -f /target/Procfile /target/dashboard.tmpl' + + copy_to_named_volume \ + "$proxy_assets_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/Procfile" \ + Procfile + copy_to_named_volume \ + "$proxy_assets_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/dashboard.tmpl" \ + dashboard.tmpl + + docker run --rm \ + -v "$proxy_vhost_volume:/target" \ + docker:29-cli \ + sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' + + copy_to_named_volume \ + "$proxy_vhost_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/localhost_location_override" \ + localhost_location_override + copy_to_named_volume \ + "$proxy_vhost_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/*.localhost" \ + '*.localhost' + copy_to_named_volume \ + "$proxy_vhost_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/*.localhost_location_override" \ + '*.localhost_location_override' +} + running_services="$(compose ps --status running --services)" service_is_running() { @@ -206,20 +257,55 @@ report_environment_ready() { nextcloud sh /var/www/scripts/report-environment-ready } +other_proxy_client_is_running() { + docker ps \ + --filter "label=$proxy_client_label" \ + --format '{{.Label "com.docker.compose.project"}}' | + grep -v -x "$project" | + grep -q . +} + +release_proxy_if_unused() { + if other_proxy_client_is_running; then + echo 'βœ… Shared development proxy is still used by another environment.' + return 0 + fi + + sleep 1 + + if other_proxy_client_is_running; then + echo 'βœ… Shared development proxy is still used by another environment.' + return 0 + fi + + echo 'Stopping unused shared development proxy.' + if ! proxy_compose down --remove-orphans; then + echo 'Could not stop the unused shared development proxy.' >&2 + fi +} + +shutdown() { + trap - INT TERM HUP + release_proxy_if_unused + exit 0 +} + success() { case "$1" in reused) - echo 'βœ… Existing LibreCode development proxy reused. Coordinator exiting normally.' - ;; - started) - echo 'βœ… Development proxy started successfully. Coordinator exiting normally.' - ;; + echo 'βœ… Existing LibreCode development proxy reused. Coordinator lease is active.' + ;; + started) + echo 'βœ… Development proxy started successfully. Coordinator lease is active.' + ;; esac } echo "Validating Compose project ${project} at ${PROJECT_DIR}." compose config --quiet +install_proxy_assets + if proxy_is_ready; then proxy_compose up --detach proxy_state=reused @@ -240,3 +326,10 @@ if ! report_environment_ready; then fi success "$proxy_state" + +trap shutdown INT TERM HUP + +while :; do + sleep 3600 & + wait "$!" || true +done From 6e35c04790780c01bdd89577318a16affda7323f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 027/153] refactor: remove local proxy image build Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/Dockerfile | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .docker/nginx-proxy/Dockerfile diff --git a/.docker/nginx-proxy/Dockerfile b/.docker/nginx-proxy/Dockerfile deleted file mode 100644 index 6941b92b..00000000 --- a/.docker/nginx-proxy/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM nginxproxy/nginx-proxy:1.11-alpine - -COPY Procfile /app/Procfile -COPY dashboard.tmpl /app/dashboard.tmpl -COPY dashboard.conf /etc/nginx/conf.d/dashboard.conf From bac090a47091ce5698871bfec04f50b3376607e9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 028/153] refactor: remove unused docker-gen config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/docker-gen.cfg | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .docker/nginx-proxy/docker-gen.cfg diff --git a/.docker/nginx-proxy/docker-gen.cfg b/.docker/nginx-proxy/docker-gen.cfg deleted file mode 100644 index 484d5801..00000000 --- a/.docker/nginx-proxy/docker-gen.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[[config]] -template = "/app/nginx.tmpl" -dest = "/etc/nginx/conf.d/default.conf" -watch = true -wait = "100ms:500ms" -notifycmd = "nginx -s reload" - -[[config]] -template = "/dashboard/dashboard.conf.tmpl" -dest = "/etc/nginx/conf.d/dashboard.conf" -watch = true -wait = "100ms:500ms" -notifycmd = "nginx -s reload" - -[[config]] -template = "/dashboard/dashboard.tmpl" -dest = "/usr/share/nginx/html/index.html" -watch = true -wait = "100ms:500ms" From ed58a8f682c46a237d5bdf4b85934aaf657a0934 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 029/153] refactor: remove separate dashboard server config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .docker/nginx-proxy/dashboard.conf diff --git a/.docker/nginx-proxy/dashboard.conf b/.docker/nginx-proxy/dashboard.conf deleted file mode 100644 index 52e31c3f..00000000 --- a/.docker/nginx-proxy/dashboard.conf +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 8080; - listen [::]:8080; - - server_name _; - - root /usr/share/nginx/html; - index index.html; - - location = /index.html { - internal; - } - - location / { - if ($host != localhost) { - return 404; - } - - try_files /index.html =404; - } - - error_page 404 =404 /index.html; -} From 245f9daf695290c74de8659a96edd82c8f14d6b6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 030/153] refactor: remove local coordinator image build Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/Dockerfile.proxy-coordinator | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .docker/Dockerfile.proxy-coordinator diff --git a/.docker/Dockerfile.proxy-coordinator b/.docker/Dockerfile.proxy-coordinator deleted file mode 100644 index fcde7fca..00000000 --- a/.docker/Dockerfile.proxy-coordinator +++ /dev/null @@ -1,9 +0,0 @@ -FROM docker:29-cli - -RUN apk add --no-cache docker-cli-compose - -COPY scripts/proxy-coordinator.sh /usr/local/bin/proxy-coordinator.sh - -RUN chmod +x /usr/local/bin/proxy-coordinator.sh - -ENTRYPOINT ["/usr/local/bin/proxy-coordinator.sh"] From 3a0aead66442db71134c420bdd734ee88be696e0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 031/153] refactor: keep shared proxy network external Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index 0abd44c5..a98bf158 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -39,6 +39,7 @@ services: networks: proxy: name: librecode-dev-proxy + external: true volumes: librecode-dev-proxy-vhost: From 221533439a6b8863a8579ec51c323f842c88de40 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 032/153] fix: ensure shared proxy network exists Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 96c673c1..d9725f20 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -122,6 +122,19 @@ ensure_ports_available() { done } +ensure_proxy_network() { + if docker network inspect "$proxy_network" >/dev/null 2>&1; then + return 0 + fi + + if docker network create "$proxy_network" >/dev/null 2>&1; then + return 0 + fi + + # Another checkout may have created it concurrently. + docker network inspect "$proxy_network" >/dev/null +} + start_proxy() { if proxy_compose up --detach; then return 0 @@ -304,6 +317,7 @@ success() { echo "Validating Compose project ${project} at ${PROJECT_DIR}." compose config --quiet +ensure_proxy_network install_proxy_assets if proxy_is_ready; then From d4fa66fd75c7e0a38fb09ec4890e539331866030 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 033/153] feat: generate proxy config and dashboard together Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/docker-gen.cfg | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .docker/nginx-proxy/docker-gen.cfg diff --git a/.docker/nginx-proxy/docker-gen.cfg b/.docker/nginx-proxy/docker-gen.cfg new file mode 100644 index 00000000..484d5801 --- /dev/null +++ b/.docker/nginx-proxy/docker-gen.cfg @@ -0,0 +1,19 @@ +[[config]] +template = "/app/nginx.tmpl" +dest = "/etc/nginx/conf.d/default.conf" +watch = true +wait = "100ms:500ms" +notifycmd = "nginx -s reload" + +[[config]] +template = "/dashboard/dashboard.conf.tmpl" +dest = "/etc/nginx/conf.d/dashboard.conf" +watch = true +wait = "100ms:500ms" +notifycmd = "nginx -s reload" + +[[config]] +template = "/dashboard/dashboard.tmpl" +dest = "/usr/share/nginx/html/index.html" +watch = true +wait = "100ms:500ms" From fe7fc4fdc96b0b803505853e26ac7c409e7f20ac Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 034/153] refactor: use one docker-gen watcher Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/Procfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.docker/nginx-proxy/Procfile b/.docker/nginx-proxy/Procfile index 69b47b8e..c0f1ad20 100644 --- a/.docker/nginx-proxy/Procfile +++ b/.docker/nginx-proxy/Procfile @@ -1,3 +1,2 @@ -dockergen: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf -dashboard: docker-gen -watch -wait 100ms:500ms -event-filter event=connect -event-filter event=disconnect /dashboard/dashboard.tmpl /usr/share/nginx/html/index.html +dockergen: docker-gen -config /dashboard/docker-gen.cfg -event-filter event=connect -event-filter event=disconnect nginx: nginx -g "daemon off;" From ffbd3683f1986131fbaf5190885dc4eb3a9a8ea7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 035/153] refactor: generate dashboard nginx config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf.tmpl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .docker/nginx-proxy/dashboard.conf.tmpl diff --git a/.docker/nginx-proxy/dashboard.conf.tmpl b/.docker/nginx-proxy/dashboard.conf.tmpl new file mode 100644 index 00000000..52e31c3f --- /dev/null +++ b/.docker/nginx-proxy/dashboard.conf.tmpl @@ -0,0 +1,23 @@ +server { + listen 8080; + listen [::]:8080; + + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /index.html { + internal; + } + + location / { + if ($host != localhost) { + return 404; + } + + try_files /index.html =404; + } + + error_page 404 =404 /index.html; +} From b13e5ba417e9014dbaa9c897d39c4fd0012d4ba7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 036/153] refactor: keep dashboard generation in one watcher Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/docker-gen.cfg | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.docker/nginx-proxy/docker-gen.cfg b/.docker/nginx-proxy/docker-gen.cfg index 484d5801..15161780 100644 --- a/.docker/nginx-proxy/docker-gen.cfg +++ b/.docker/nginx-proxy/docker-gen.cfg @@ -5,13 +5,6 @@ watch = true wait = "100ms:500ms" notifycmd = "nginx -s reload" -[[config]] -template = "/dashboard/dashboard.conf.tmpl" -dest = "/etc/nginx/conf.d/dashboard.conf" -watch = true -wait = "100ms:500ms" -notifycmd = "nginx -s reload" - [[config]] template = "/dashboard/dashboard.tmpl" dest = "/usr/share/nginx/html/index.html" From b01798360ec430d47d9de6f1d5068b4a1b3552e2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 037/153] refactor: remove redundant dashboard nginx server Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf.tmpl | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .docker/nginx-proxy/dashboard.conf.tmpl diff --git a/.docker/nginx-proxy/dashboard.conf.tmpl b/.docker/nginx-proxy/dashboard.conf.tmpl deleted file mode 100644 index 52e31c3f..00000000 --- a/.docker/nginx-proxy/dashboard.conf.tmpl +++ /dev/null @@ -1,23 +0,0 @@ -server { - listen 8080; - listen [::]:8080; - - server_name _; - - root /usr/share/nginx/html; - index index.html; - - location = /index.html { - internal; - } - - location / { - if ($host != localhost) { - return 404; - } - - try_files /index.html =404; - } - - error_page 404 =404 /index.html; -} From 3bc6480f31a8379fd8640781b59588f3a2fd52d8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 038/153] fix: release shared proxy with coordinator leases Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 72 ++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index d9725f20..20ac8ac4 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -8,11 +8,13 @@ proxy_label=coop.librecode.dev-proxy=true proxy_client_label=coop.librecode.dev-proxy-client=true proxy_assets_volume=librecode-dev-proxy-assets proxy_vhost_volume=librecode-dev-proxy-vhost +proxy_lease_acquired=false +coordinator_container="$(hostname)" compose_project() { docker inspect \ --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ - "$(hostname)" + "$coordinator_container" } project="$(compose_project)" @@ -174,12 +176,16 @@ install_proxy_assets() { docker run --rm \ -v "$proxy_assets_volume:/target" \ docker:29-cli \ - sh -c 'rm -f /target/Procfile /target/dashboard.tmpl' + sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' copy_to_named_volume \ "$proxy_assets_volume" \ "$PROJECT_DIR/.docker/nginx-proxy/Procfile" \ Procfile + copy_to_named_volume \ + "$proxy_assets_volume" \ + "$PROJECT_DIR/.docker/nginx-proxy/docker-gen.cfg" \ + docker-gen.cfg copy_to_named_volume \ "$proxy_assets_volume" \ "$PROJECT_DIR/.docker/nginx-proxy/dashboard.tmpl" \ @@ -270,23 +276,72 @@ report_environment_ready() { nextcloud sh /var/www/scripts/report-environment-ready } +acquire_proxy_lease() { + if ! docker inspect \ + --format '{{ json .NetworkSettings.Networks }}' \ + "$coordinator_container" | + grep -q "\"$proxy_network\""; then + docker network connect "$proxy_network" "$coordinator_container" + fi + + proxy_lease_acquired=true +} + +release_proxy_lease() { + [ "$proxy_lease_acquired" = true ] || return 0 + + docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1 || true + proxy_lease_acquired=false +} + other_proxy_client_is_running() { docker ps \ --filter "label=$proxy_client_label" \ - --format '{{.Label "com.docker.compose.project"}}' | - grep -v -x "$project" | + --filter "network=$proxy_network" \ + --format '{{.ID}}' | grep -q . } +other_proxy_route_is_running() { + for container in $(docker ps --filter "network=$proxy_network" --format '{{.ID}}'); do + container_project="$(docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ + "$container" 2>/dev/null || true)" + + case "$container_project" in + "$project"|"$proxy_project") + continue + ;; + esac + + virtual_host="$(docker inspect \ + --format '{{range .Config.Env}}{{println .}}{{end}}' \ + "$container" 2>/dev/null | + sed -n 's/^VIRTUAL_HOST=//p' | + head -n 1)" + + [ -z "$virtual_host" ] || return 0 + done + + return 1 +} + +proxy_is_used_by_another_environment() { + other_proxy_client_is_running || other_proxy_route_is_running +} + release_proxy_if_unused() { - if other_proxy_client_is_running; then + release_proxy_lease + + if proxy_is_used_by_another_environment; then echo 'βœ… Shared development proxy is still used by another environment.' return 0 fi + # Give an environment starting concurrently time to acquire its lease. sleep 1 - if other_proxy_client_is_running; then + if proxy_is_used_by_another_environment; then echo 'βœ… Shared development proxy is still used by another environment.' return 0 fi @@ -329,6 +384,9 @@ else proxy_state=started fi +acquire_proxy_lease +trap shutdown INT TERM HUP + connect_running_service_to_proxy_network nginx connect_running_service_to_proxy_network mailpit connect_running_service_to_proxy_network eurooffice @@ -341,8 +399,6 @@ fi success "$proxy_state" -trap shutdown INT TERM HUP - while :; do sleep 3600 & wait "$!" || true From 14430963021f6caaa55510529f9b8331f8b54047 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:08 -0300 Subject: [PATCH 039/153] fix: serve dashboard for unknown localhost hosts Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/*.localhost_location_override | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.docker/nginx-proxy/*.localhost_location_override b/.docker/nginx-proxy/*.localhost_location_override index 421c127f..a831b0aa 100644 --- a/.docker/nginx-proxy/*.localhost_location_override +++ b/.docker/nginx-proxy/*.localhost_location_override @@ -1 +1,2 @@ -return 404; +root /usr/share/nginx/html; +try_files /index.html =404; From eaad008bab4298db42ed8fa3feb22428c677e7d6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 040/153] fix: handle localhost dashboard directly in nginx Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .docker/nginx-proxy/dashboard.conf diff --git a/.docker/nginx-proxy/dashboard.conf b/.docker/nginx-proxy/dashboard.conf new file mode 100644 index 00000000..f847c0d1 --- /dev/null +++ b/.docker/nginx-proxy/dashboard.conf @@ -0,0 +1,63 @@ +server { + listen 80; + listen [::]:80; + server_name localhost; + + root /usr/share/nginx/html; + + location / { + try_files /index.html =404; + } +} + +server { + listen 80; + listen [::]:80; + server_name *.localhost; + + root /usr/share/nginx/html; + + location = /index.html { + internal; + } + + location / { + error_page 404 =404 /index.html; + return 404; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/localhost.crt; + ssl_certificate_key /etc/nginx/certs/localhost.key; + + root /usr/share/nginx/html; + + location / { + try_files /index.html =404; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name *.localhost; + + ssl_certificate /etc/nginx/certs/*.localhost.crt; + ssl_certificate_key /etc/nginx/certs/*.localhost.key; + + root /usr/share/nginx/html; + + location = /index.html { + internal; + } + + location / { + error_page 404 =404 /index.html; + return 404; + } +} From 4f82dee2550a57df1fc788f9ca6b18a6831bde70 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 041/153] refactor: serve dashboard from proxy config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index a98bf158..23c846ee 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -11,14 +11,12 @@ services: - "443:443" volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/tmp/docker.sock:ro - - librecode-dev-proxy-vhost:/etc/nginx/vhost.d - librecode-dev-proxy-assets:/dashboard:ro + - librecode-dev-proxy-conf:/etc/nginx/conf.d - librecode-dev-proxy-log:/etc/nginx/log - librecode-dev-proxy-certs:/etc/nginx/certs:ro environment: - ENABLE_IPV6=true - - VIRTUAL_HOST=localhost,*.localhost - - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=localhost,*.localhost networks: - proxy @@ -42,10 +40,10 @@ networks: external: true volumes: - librecode-dev-proxy-vhost: - name: librecode-dev-proxy-vhost librecode-dev-proxy-assets: name: librecode-dev-proxy-assets + librecode-dev-proxy-conf: + name: librecode-dev-proxy-conf librecode-dev-proxy-log: name: librecode-dev-proxy-log librecode-dev-proxy-certs: From e3ad06ee0f1828024dbeefee0a30d1a997f245b3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 042/153] revert: keep wildcard host managed by nginx-proxy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index 23c846ee..a98bf158 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -11,12 +11,14 @@ services: - "443:443" volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/tmp/docker.sock:ro + - librecode-dev-proxy-vhost:/etc/nginx/vhost.d - librecode-dev-proxy-assets:/dashboard:ro - - librecode-dev-proxy-conf:/etc/nginx/conf.d - librecode-dev-proxy-log:/etc/nginx/log - librecode-dev-proxy-certs:/etc/nginx/certs:ro environment: - ENABLE_IPV6=true + - VIRTUAL_HOST=localhost,*.localhost + - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=localhost,*.localhost networks: - proxy @@ -40,10 +42,10 @@ networks: external: true volumes: + librecode-dev-proxy-vhost: + name: librecode-dev-proxy-vhost librecode-dev-proxy-assets: name: librecode-dev-proxy-assets - librecode-dev-proxy-conf: - name: librecode-dev-proxy-conf librecode-dev-proxy-log: name: librecode-dev-proxy-log librecode-dev-proxy-certs: From 95d910e9715b5547762bca8415bdb794d2904f83 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 043/153] revert: remove direct dashboard server Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.conf | 63 ------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 .docker/nginx-proxy/dashboard.conf diff --git a/.docker/nginx-proxy/dashboard.conf b/.docker/nginx-proxy/dashboard.conf deleted file mode 100644 index f847c0d1..00000000 --- a/.docker/nginx-proxy/dashboard.conf +++ /dev/null @@ -1,63 +0,0 @@ -server { - listen 80; - listen [::]:80; - server_name localhost; - - root /usr/share/nginx/html; - - location / { - try_files /index.html =404; - } -} - -server { - listen 80; - listen [::]:80; - server_name *.localhost; - - root /usr/share/nginx/html; - - location = /index.html { - internal; - } - - location / { - error_page 404 =404 /index.html; - return 404; - } -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - server_name localhost; - - ssl_certificate /etc/nginx/certs/localhost.crt; - ssl_certificate_key /etc/nginx/certs/localhost.key; - - root /usr/share/nginx/html; - - location / { - try_files /index.html =404; - } -} - -server { - listen 443 ssl; - listen [::]:443 ssl; - server_name *.localhost; - - ssl_certificate /etc/nginx/certs/*.localhost.crt; - ssl_certificate_key /etc/nginx/certs/*.localhost.key; - - root /usr/share/nginx/html; - - location = /index.html { - internal; - } - - location / { - error_page 404 =404 /index.html; - return 404; - } -} From 9403f3d9fbd0e7c70920176a46261ccc00a5da49 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 044/153] fix: render friendly wildcard 404 page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/*.localhost | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.docker/nginx-proxy/*.localhost b/.docker/nginx-proxy/*.localhost index a2450af0..b66d860e 100644 --- a/.docker/nginx-proxy/*.localhost +++ b/.docker/nginx-proxy/*.localhost @@ -1,6 +1,6 @@ root /usr/share/nginx/html; -error_page 404 =404 /index.html; +error_page 404 =404 @librecode_dashboard_404; -location = /index.html { - internal; +location @librecode_dashboard_404 { + try_files /index.html =404; } From aeb643013482ab5e6da030a95c2decfcff016f09 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 045/153] fix: preserve 404 status for wildcard dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/*.localhost_location_override | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.docker/nginx-proxy/*.localhost_location_override b/.docker/nginx-proxy/*.localhost_location_override index a831b0aa..421c127f 100644 --- a/.docker/nginx-proxy/*.localhost_location_override +++ b/.docker/nginx-proxy/*.localhost_location_override @@ -1,2 +1 @@ -root /usr/share/nginx/html; -try_files /index.html =404; +return 404; From a29ee72ef169b358cc0cb10516dd24f5d364cd9e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 046/153] fix: ignore current proxy lease during shutdown Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 20ac8ac4..8dd161b8 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -290,16 +290,25 @@ acquire_proxy_lease() { release_proxy_lease() { [ "$proxy_lease_acquired" = true ] || return 0 - docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1 || true + if ! docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1; then + echo 'Could not disconnect this coordinator lease from the shared proxy network; continuing with project-based lease detection.' >&2 + fi proxy_lease_acquired=false } other_proxy_client_is_running() { - docker ps \ + for container in $(docker ps \ --filter "label=$proxy_client_label" \ --filter "network=$proxy_network" \ - --format '{{.ID}}' | - grep -q . + --format '{{.ID}}'); do + container_project="$(docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ + "$container" 2>/dev/null || true)" + + [ "$container_project" = "$project" ] || return 0 + done + + return 1 } other_proxy_route_is_running() { @@ -354,6 +363,7 @@ release_proxy_if_unused() { shutdown() { trap - INT TERM HUP + echo 'Releasing shared development proxy lease.' release_proxy_if_unused exit 0 } From da1116702b959b869f10f337393e9424c38221de Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 047/153] refactor: extract proxy coordinator common helpers Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/common.sh | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .docker/scripts/proxy/common.sh diff --git a/.docker/scripts/proxy/common.sh b/.docker/scripts/proxy/common.sh new file mode 100644 index 00000000..1fc56e4d --- /dev/null +++ b/.docker/scripts/proxy/common.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +proxy_project=${PROXY_PROJECT:-librecode-dev-proxy} +proxy_network=${PROXY_NETWORK:-librecode-dev-proxy} +proxy_label=${PROXY_LABEL:-coop.librecode.dev-proxy=true} +proxy_client_label=${PROXY_CLIENT_LABEL:-coop.librecode.dev-proxy-client=true} +proxy_assets_volume=${PROXY_ASSETS_VOLUME:-librecode-dev-proxy-assets} +proxy_vhost_volume=${PROXY_VHOST_VOLUME:-librecode-dev-proxy-vhost} + +Docker() { + docker "$@" +} + +container_project() { + Docker inspect \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ + "$1" 2>/dev/null || true +} + +container_virtual_host() { + Docker inspect \ + --format '{{range .Config.Env}}{{println .}}{{end}}' \ + "$1" 2>/dev/null | + sed -n 's/^VIRTUAL_HOST=//p' | + head -n 1 +} + +container_networks() { + Docker inspect \ + --format '{{ json .NetworkSettings.Networks }}' \ + "$1" 2>/dev/null || true +} + +compose() { + Docker compose \ + --project-name "$project" \ + --project-directory "$PROJECT_DIR" \ + --file "$PROJECT_DIR/docker-compose.yml" \ + "$@" +} + +proxy_compose() { + Docker compose \ + --project-name "$proxy_project" \ + --project-directory "$PROJECT_DIR" \ + --file "$PROJECT_DIR/.docker/docker-compose.proxy.yml" \ + "$@" +} From e403b5fb650c35e96e2644608c662a1547562053 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 048/153] refactor: extract shared proxy infrastructure logic Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/infrastructure.sh | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .docker/scripts/proxy/infrastructure.sh diff --git a/.docker/scripts/proxy/infrastructure.sh b/.docker/scripts/proxy/infrastructure.sh new file mode 100644 index 00000000..9c013dde --- /dev/null +++ b/.docker/scripts/proxy/infrastructure.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +container_for_published_port() { + port="$1" + + Docker ps \ + --filter "publish=$port" \ + --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Label "coop.librecode.dev-proxy"}}' +} + +port_is_in_use() { + container_for_published_port "$1" | grep -q . +} + +compatible_proxy_container() { + Docker ps \ + --filter "label=$proxy_label" \ + --format '{{.ID}}' | + head -n 1 +} + +is_compatible_proxy_port_owner() { + port="$1" + info="$(container_for_published_port "$port" | head -n 1)" + + [ -n "$info" ] || return 1 + [ "$(printf '%s\n' "$info" | cut -f4)" = "true" ] +} + +proxy_is_ready() { + [ -n "$(compatible_proxy_container || true)" ] && + is_compatible_proxy_port_owner 80 && + is_compatible_proxy_port_owner 443 +} + +show_conflict() { + port="$1" + container_info="$(container_for_published_port "$port" | head -n 1)" + + printf 'β”Œβ”€ β›” Development proxy cannot start ─────────────────────\n' >&2 + printf 'β”‚\n' >&2 + printf 'β”‚ Port 80 or 443 is already in use by another service.\n' >&2 + printf 'β”‚\n' >&2 + printf 'β”‚ This development environment requires:\n' >&2 + printf 'β”‚\n' >&2 + printf 'β”‚ HTTP localhost:80\n' >&2 + printf 'β”‚ HTTPS localhost:443\n' >&2 + printf 'β”‚\n' >&2 + printf 'β”‚ Stop the conflicting service and run:\n' >&2 + printf 'β”‚\n' >&2 + printf 'β”‚ docker compose up\n' >&2 + printf 'β”‚\n' >&2 + + if [ -n "$container_info" ]; then + printf 'β”‚ Conflicting container\n' >&2 + printf 'β”‚ Name %s\n' "$(printf '%s\n' "$container_info" | cut -f2)" >&2 + printf 'β”‚ Image %s\n' "$(printf '%s\n' "$container_info" | cut -f3)" >&2 + printf 'β”‚ Port %s\n' "$port" >&2 + else + printf 'β”‚ Port %s is already in use by a process outside Docker.\n' "$port" >&2 + fi + + printf 'β”‚\n' >&2 + printf '└────────────────────────────────────────────────────────\n' >&2 +} + +ensure_ports_available() { + for port in 80 443; do + if port_is_in_use "$port"; then + show_conflict "$port" + return 1 + fi + done +} + +ensure_proxy_network() { + if Docker network inspect "$proxy_network" >/dev/null 2>&1; then + return 0 + fi + + if Docker network create "$proxy_network" >/dev/null 2>&1; then + return 0 + fi + + # Another checkout may have created it concurrently. + Docker network inspect "$proxy_network" >/dev/null +} + +start_proxy() { + if proxy_compose up --detach; then + return 0 + fi + + # Another checkout may have started the shared proxy concurrently. + if proxy_is_ready; then + return 0 + fi + + ensure_ports_available || return 1 + + echo 'Could not start the LibreCode development proxy.' >&2 + return 1 +} + +ensure_proxy_running() { + if proxy_is_ready; then + proxy_compose up --detach + printf 'reused\n' + return 0 + fi + + ensure_ports_available || return 1 + start_proxy || return 1 + printf 'started\n' +} From 2bf03bf6ebb55504fe123da15a1209ef72e99ef4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 049/153] refactor: extract shared proxy asset installation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .docker/scripts/proxy/assets.sh diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh new file mode 100644 index 00000000..93a1fe5c --- /dev/null +++ b/.docker/scripts/proxy/assets.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +copy_to_named_volume() { + volume="$1" + source="$2" + destination="$3" + + Docker run --rm -i \ + -v "$volume:/target" \ + docker:29-cli \ + sh -c 'cat > "/target/$1"' sh "$destination" \ + < "$source" +} + +install_proxy_assets() { + Docker volume create "$proxy_assets_volume" >/dev/null + Docker volume create "$proxy_vhost_volume" >/dev/null + + Docker run --rm \ + -v "$proxy_assets_volume:/target" \ + docker:29-cli \ + sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' + + copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/Procfile" Procfile + copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/docker-gen.cfg" docker-gen.cfg + copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/dashboard.tmpl" dashboard.tmpl + + Docker run --rm \ + -v "$proxy_vhost_volume:/target" \ + docker:29-cli \ + sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' + + copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/localhost_location_override" localhost_location_override + copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/*.localhost" '*.localhost' + copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/*.localhost_location_override" '*.localhost_location_override' +} From dd523b9a759c66288c1565e053181d3737692104 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 050/153] refactor: extract proxied service coordination Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/services.sh | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .docker/scripts/proxy/services.sh diff --git a/.docker/scripts/proxy/services.sh b/.docker/scripts/proxy/services.sh new file mode 100644 index 00000000..d35996f9 --- /dev/null +++ b/.docker/scripts/proxy/services.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +service_is_running() { + compose ps --status running --services | + grep -qx "$1" +} + +container_for_service() { + compose ps -q "$1" 2>/dev/null || true +} + +connect_to_proxy_network() { + service="$1" + container="$(container_for_service "$service")" + + [ -n "$container" ] || return 0 + + if container_networks "$container" | grep -q "\"$proxy_network\""; then + return 0 + fi + + Docker network connect "$proxy_network" "$container" +} + +connect_running_service_to_proxy_network() { + service="$1" + + service_is_running "$service" || return 0 + connect_to_proxy_network "$service" +} + +connect_project_services() { + for service in nginx mailpit eurooffice playwright signal-gateway; do + connect_running_service_to_proxy_network "$service" + done +} + +report_environment_ready() { + set -- \ + -e ENV_NEXTCLOUD_URL="https://${project}.localhost" \ + -e ENV_ADMIN_USER="$NEXTCLOUD_ADMIN_USER" \ + -e ENV_ADMIN_PASSWORD="$NEXTCLOUD_ADMIN_PASSWORD" \ + -e ENV_NEXTCLOUD_BRANCH="$VERSION_NEXTCLOUD" + + if service_is_running mailpit; then + set -- "$@" -e ENV_MAILPIT_URL="https://${project}-mailpit.localhost" + fi + + if service_is_running eurooffice; then + set -- "$@" -e ENV_EUROOFFICE_URL="https://${project}-eurooffice.localhost" + fi + + if service_is_running playwright; then + set -- "$@" -e ENV_PLAYWRIGHT_URL="https://${project}-playwright.localhost" + fi + + if service_is_running signal-gateway; then + set -- "$@" -e ENV_SIGNAL_URL="https://${project}-signal.localhost" + fi + + compose exec -T \ + "$@" \ + nextcloud sh /var/www/scripts/report-environment-ready +} From 3dca7dab71e73684e17db4cb9dcf25ac60bedce1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 051/153] refactor: extract shared proxy lease lifecycle Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/lease.sh | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .docker/scripts/proxy/lease.sh diff --git a/.docker/scripts/proxy/lease.sh b/.docker/scripts/proxy/lease.sh new file mode 100644 index 00000000..6e080c25 --- /dev/null +++ b/.docker/scripts/proxy/lease.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +proxy_lease_acquired=false + +acquire_proxy_lease() { + if ! container_networks "$coordinator_container" | grep -q "\"$proxy_network\""; then + Docker network connect "$proxy_network" "$coordinator_container" + fi + + proxy_lease_acquired=true +} + +release_proxy_lease() { + [ "$proxy_lease_acquired" = true ] || return 0 + + if ! Docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1; then + echo 'Could not disconnect this coordinator lease from the shared proxy network; continuing with project-based lease detection.' >&2 + fi + + proxy_lease_acquired=false +} + +other_proxy_client_is_running() { + for container in $(Docker ps \ + --filter "label=$proxy_client_label" \ + --filter "network=$proxy_network" \ + --format '{{.ID}}'); do + [ "$(container_project "$container")" = "$project" ] || return 0 + done + + return 1 +} + +other_proxy_route_is_running() { + for container in $(Docker ps --filter "network=$proxy_network" --format '{{.ID}}'); do + container_project_name="$(container_project "$container")" + + case "$container_project_name" in + "$project"|"$proxy_project") + continue + ;; + esac + + [ -z "$(container_virtual_host "$container")" ] || return 0 + done + + return 1 +} + +proxy_is_used_by_another_environment() { + other_proxy_client_is_running || other_proxy_route_is_running +} + +wait_for_concurrent_lease() { + sleep "${PROXY_LEASE_GRACE_SECONDS:-1}" +} + +release_proxy_if_unused() { + release_proxy_lease + + if proxy_is_used_by_another_environment; then + echo 'βœ… Shared development proxy is still used by another environment.' + return 0 + fi + + wait_for_concurrent_lease + + if proxy_is_used_by_another_environment; then + echo 'βœ… Shared development proxy is still used by another environment.' + return 0 + fi + + echo 'Stopping unused shared development proxy.' + if ! proxy_compose down --remove-orphans; then + echo 'Could not stop the unused shared development proxy.' >&2 + return 1 + fi +} From 11a3f340292774110eb2a26716ed6dd60713d955 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 052/153] refactor: reduce proxy coordinator to orchestration Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 443 ++++----------------------- 1 file changed, 57 insertions(+), 386 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 8dd161b8..b3b5a227 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -2,414 +2,85 @@ set -eu -proxy_project=librecode-dev-proxy -proxy_network=librecode-dev-proxy -proxy_label=coop.librecode.dev-proxy=true -proxy_client_label=coop.librecode.dev-proxy-client=true -proxy_assets_volume=librecode-dev-proxy-assets -proxy_vhost_volume=librecode-dev-proxy-vhost -proxy_lease_acquired=false -coordinator_container="$(hostname)" - -compose_project() { - docker inspect \ - --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ - "$coordinator_container" -} - -project="$(compose_project)" - -if [ -z "$project" ]; then - echo 'Could not determine the Compose project from the coordinator container.' >&2 - exit 1 -fi - -if [ -z "${PROJECT_DIR:-}" ]; then - echo 'The host project directory was not provided to the coordinator.' >&2 - exit 1 -fi - -compose() { - docker compose \ - --project-name "$project" \ - --project-directory "$PROJECT_DIR" \ - --file "$PROJECT_DIR/docker-compose.yml" \ - "$@" -} - -proxy_compose() { - docker compose \ - --project-name "$proxy_project" \ - --project-directory "$PROJECT_DIR" \ - --file "$PROJECT_DIR/.docker/docker-compose.proxy.yml" \ - "$@" -} - -container_for_published_port() { - port="$1" - - docker ps \ - --filter "publish=$port" \ - --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Label "coop.librecode.dev-proxy"}}' -} - -port_is_in_use() { - container_for_published_port "$1" | grep -q . -} - -compatible_proxy_container() { - docker ps \ - --filter "label=$proxy_label" \ - --format '{{.ID}}' | - head -n 1 -} - -is_compatible_proxy_port_owner() { - port="$1" - info="$(container_for_published_port "$port" | head -n 1)" - - [ -n "$info" ] || return 1 - - compatible="$(printf '%s\n' "$info" | cut -f4)" - - [ "$compatible" = "true" ] -} - -proxy_is_ready() { - [ -n "$(compatible_proxy_container || true)" ] && - is_compatible_proxy_port_owner 80 && - is_compatible_proxy_port_owner 443 -} - -show_conflict() { - port="$1" - container_info="$(container_for_published_port "$port" | head -n 1)" - - printf 'β”Œβ”€ β›” Development proxy cannot start ─────────────────────\n' >&2 - printf 'β”‚\n' >&2 - printf 'β”‚ Port 80 or 443 is already in use by another service.\n' >&2 - printf 'β”‚\n' >&2 - printf 'β”‚ This development environment requires:\n' >&2 - printf 'β”‚\n' >&2 - printf 'β”‚ HTTP localhost:80\n' >&2 - printf 'β”‚ HTTPS localhost:443\n' >&2 - printf 'β”‚\n' >&2 - printf 'β”‚ Stop the conflicting service and run:\n' >&2 - printf 'β”‚\n' >&2 - printf 'β”‚ docker compose up\n' >&2 - printf 'β”‚\n' >&2 - - if [ -n "$container_info" ]; then - name="$(printf '%s\n' "$container_info" | cut -f2)" - image="$(printf '%s\n' "$container_info" | cut -f3)" - - printf 'β”‚ Conflicting container\n' >&2 - printf 'β”‚ Name %s\n' "$name" >&2 - printf 'β”‚ Image %s\n' "$image" >&2 - printf 'β”‚ Port %s\n' "$port" >&2 - else - printf 'β”‚ Port %s is already in use by a process outside Docker.\n' "$port" >&2 - fi - - printf 'β”‚\n' >&2 - printf '└────────────────────────────────────────────────────────\n' >&2 -} - -ensure_ports_available() { - for port in 80 443; do - if port_is_in_use "$port"; then - show_conflict "$port" - exit 1 - fi - done -} - -ensure_proxy_network() { - if docker network inspect "$proxy_network" >/dev/null 2>&1; then - return 0 - fi - - if docker network create "$proxy_network" >/dev/null 2>&1; then - return 0 - fi - - # Another checkout may have created it concurrently. - docker network inspect "$proxy_network" >/dev/null -} - -start_proxy() { - if proxy_compose up --detach; then - return 0 - fi - - if proxy_is_ready; then - return 0 - fi - - for port in 80 443; do - if port_is_in_use "$port"; then - show_conflict "$port" - exit 1 - fi - done - - echo 'Could not start the LibreCode development proxy.' >&2 - exit 1 -} - -copy_to_named_volume() { - volume="$1" - source="$2" - destination="$3" - - docker run --rm -i \ - -v "$volume:/target" \ - docker:29-cli \ - sh -c 'cat > "/target/$1"' sh "$destination" \ - < "$source" -} - -install_proxy_assets() { - docker volume create "$proxy_assets_volume" >/dev/null - docker volume create "$proxy_vhost_volume" >/dev/null - - docker run --rm \ - -v "$proxy_assets_volume:/target" \ - docker:29-cli \ - sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' - - copy_to_named_volume \ - "$proxy_assets_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/Procfile" \ - Procfile - copy_to_named_volume \ - "$proxy_assets_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/docker-gen.cfg" \ - docker-gen.cfg - copy_to_named_volume \ - "$proxy_assets_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/dashboard.tmpl" \ - dashboard.tmpl - - docker run --rm \ - -v "$proxy_vhost_volume:/target" \ - docker:29-cli \ - sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' - - copy_to_named_volume \ - "$proxy_vhost_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/localhost_location_override" \ - localhost_location_override - copy_to_named_volume \ - "$proxy_vhost_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/*.localhost" \ - '*.localhost' - copy_to_named_volume \ - "$proxy_vhost_volume" \ - "$PROJECT_DIR/.docker/nginx-proxy/*.localhost_location_override" \ - '*.localhost_location_override' -} - -running_services="$(compose ps --status running --services)" - -service_is_running() { - printf '%s\n' "$running_services" | - grep -qx "$1" -} - -container_for_service() { - compose ps -q "$1" 2>/dev/null || true -} +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +proxy_lib_dir="${PROXY_LIB_DIR:-$script_dir/proxy}" + +# shellcheck source=.docker/scripts/proxy/common.sh +. "$proxy_lib_dir/common.sh" +# shellcheck source=.docker/scripts/proxy/infrastructure.sh +. "$proxy_lib_dir/infrastructure.sh" +# shellcheck source=.docker/scripts/proxy/assets.sh +. "$proxy_lib_dir/assets.sh" +# shellcheck source=.docker/scripts/proxy/services.sh +. "$proxy_lib_dir/services.sh" +# shellcheck source=.docker/scripts/proxy/lease.sh +. "$proxy_lib_dir/lease.sh" -connect_to_proxy_network() { - service="$1" - container="$(container_for_service "$service")" - - [ -n "$container" ] || return 0 - - if docker inspect \ - --format '{{ json .NetworkSettings.Networks }}' \ - "$container" | - grep -q "\"$proxy_network\""; then - return 0 - fi - - docker network connect "$proxy_network" "$container" -} - -connect_running_service_to_proxy_network() { - service="$1" - - service_is_running "$service" || return 0 - connect_to_proxy_network "$service" -} - -report_environment_ready() { - set -- \ - -e ENV_NEXTCLOUD_URL="https://${project}.localhost" \ - -e ENV_ADMIN_USER="$NEXTCLOUD_ADMIN_USER" \ - -e ENV_ADMIN_PASSWORD="$NEXTCLOUD_ADMIN_PASSWORD" \ - -e ENV_NEXTCLOUD_BRANCH="$VERSION_NEXTCLOUD" - - if service_is_running mailpit; then - set -- "$@" \ - -e ENV_MAILPIT_URL="https://${project}-mailpit.localhost" - fi - - if service_is_running eurooffice; then - set -- "$@" \ - -e ENV_EUROOFFICE_URL="https://${project}-eurooffice.localhost" - fi - - if service_is_running playwright; then - set -- "$@" \ - -e ENV_PLAYWRIGHT_URL="https://${project}-playwright.localhost" - fi +coordinator_container="$(hostname)" +project="$(container_project "$coordinator_container")" - if service_is_running signal-gateway; then - set -- "$@" \ - -e ENV_SIGNAL_URL="https://${project}-signal.localhost" +validate_environment() { + if [ -z "$project" ]; then + echo 'Could not determine the Compose project from the coordinator container.' >&2 + return 1 fi - compose exec -T \ - "$@" \ - nextcloud sh /var/www/scripts/report-environment-ready -} - -acquire_proxy_lease() { - if ! docker inspect \ - --format '{{ json .NetworkSettings.Networks }}' \ - "$coordinator_container" | - grep -q "\"$proxy_network\""; then - docker network connect "$proxy_network" "$coordinator_container" + if [ -z "${PROJECT_DIR:-}" ]; then + echo 'The host project directory was not provided to the coordinator.' >&2 + return 1 fi - proxy_lease_acquired=true + echo "Validating Compose project ${project} at ${PROJECT_DIR}." + compose config --quiet } -release_proxy_lease() { - [ "$proxy_lease_acquired" = true ] || return 0 - - if ! docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1; then - echo 'Could not disconnect this coordinator lease from the shared proxy network; continuing with project-based lease detection.' >&2 - fi - proxy_lease_acquired=false +success() { + case "$1" in + reused) + echo 'βœ… Existing LibreCode development proxy reused. Coordinator lease is active.' + ;; + started) + echo 'βœ… Development proxy started successfully. Coordinator lease is active.' + ;; + esac } -other_proxy_client_is_running() { - for container in $(docker ps \ - --filter "label=$proxy_client_label" \ - --filter "network=$proxy_network" \ - --format '{{.ID}}'); do - container_project="$(docker inspect \ - --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ - "$container" 2>/dev/null || true)" - - [ "$container_project" = "$project" ] || return 0 - done - - return 1 +shutdown() { + trap - INT TERM HUP + echo 'Releasing shared development proxy lease.' + release_proxy_if_unused || true + exit 0 } -other_proxy_route_is_running() { - for container in $(docker ps --filter "network=$proxy_network" --format '{{.ID}}'); do - container_project="$(docker inspect \ - --format '{{ index .Config.Labels "com.docker.compose.project" }}' \ - "$container" 2>/dev/null || true)" - - case "$container_project" in - "$project"|"$proxy_project") - continue - ;; - esac - - virtual_host="$(docker inspect \ - --format '{{range .Config.Env}}{{println .}}{{end}}' \ - "$container" 2>/dev/null | - sed -n 's/^VIRTUAL_HOST=//p' | - head -n 1)" +wait_for_shutdown() { + trap shutdown INT TERM HUP - [ -z "$virtual_host" ] || return 0 + while :; do + sleep 3600 & + wait "$!" || true done - - return 1 -} - -proxy_is_used_by_another_environment() { - other_proxy_client_is_running || other_proxy_route_is_running } -release_proxy_if_unused() { - release_proxy_lease +main() { + validate_environment + ensure_proxy_network + install_proxy_assets - if proxy_is_used_by_another_environment; then - echo 'βœ… Shared development proxy is still used by another environment.' - return 0 - fi + proxy_state="$(ensure_proxy_running)" - # Give an environment starting concurrently time to acquire its lease. - sleep 1 + acquire_proxy_lease + trap shutdown INT TERM HUP - if proxy_is_used_by_another_environment; then - echo 'βœ… Shared development proxy is still used by another environment.' - return 0 - fi + connect_project_services - echo 'Stopping unused shared development proxy.' - if ! proxy_compose down --remove-orphans; then - echo 'Could not stop the unused shared development proxy.' >&2 + if ! report_environment_ready; then + echo 'Could not print environment banner.' >&2 fi -} -shutdown() { - trap - INT TERM HUP - echo 'Releasing shared development proxy lease.' - release_proxy_if_unused - exit 0 + success "$proxy_state" + wait_for_shutdown } -success() { - case "$1" in - reused) - echo 'βœ… Existing LibreCode development proxy reused. Coordinator lease is active.' - ;; - started) - echo 'βœ… Development proxy started successfully. Coordinator lease is active.' - ;; - esac -} - -echo "Validating Compose project ${project} at ${PROJECT_DIR}." -compose config --quiet - -ensure_proxy_network -install_proxy_assets - -if proxy_is_ready; then - proxy_compose up --detach - proxy_state=reused -else - ensure_ports_available - start_proxy - proxy_state=started +if [ "${PROXY_COORDINATOR_SOURCE_ONLY:-false}" != "true" ]; then + main "$@" fi - -acquire_proxy_lease -trap shutdown INT TERM HUP - -connect_running_service_to_proxy_network nginx -connect_running_service_to_proxy_network mailpit -connect_running_service_to_proxy_network eurooffice -connect_running_service_to_proxy_network playwright -connect_running_service_to_proxy_network signal-gateway - -if ! report_environment_ready; then - echo 'Could not print environment banner.' >&2 -fi - -success "$proxy_state" - -while :; do - sleep 3600 & - wait "$!" || true -done From 13fbd34609eb5d4d14ddbc911a7a8802e42854cd Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 053/153] refactor: mount proxy coordinator modules Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c8c5a29d..0554df69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,12 +65,12 @@ services: - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: image: docker:29-cli - entrypoint: ["/bin/sh", "/usr/local/bin/proxy-coordinator.sh"] + entrypoint: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" volumes: - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock - - ./.docker/scripts/proxy-coordinator.sh:/usr/local/bin/proxy-coordinator.sh:ro + - ./.docker/scripts:/usr/local/lib/librecode:ro - .:${PWD}:ro working_dir: ${PWD} environment: From b86d87b55707b198d77279035a80d2e34edc4afd Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 054/153] refactor: make compose files injectable for tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/common.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/scripts/proxy/common.sh b/.docker/scripts/proxy/common.sh index 1fc56e4d..fb17b6be 100644 --- a/.docker/scripts/proxy/common.sh +++ b/.docker/scripts/proxy/common.sh @@ -35,7 +35,7 @@ compose() { Docker compose \ --project-name "$project" \ --project-directory "$PROJECT_DIR" \ - --file "$PROJECT_DIR/docker-compose.yml" \ + --file "${PROJECT_COMPOSE_FILE:-$PROJECT_DIR/docker-compose.yml}" \ "$@" } @@ -43,6 +43,6 @@ proxy_compose() { Docker compose \ --project-name "$proxy_project" \ --project-directory "$PROJECT_DIR" \ - --file "$PROJECT_DIR/.docker/docker-compose.proxy.yml" \ + --file "${PROXY_COMPOSE_FILE:-$PROJECT_DIR/.docker/docker-compose.proxy.yml}" \ "$@" } From 2aac7913369ed055249dccbc007733ec18a59fd3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 055/153] test: cover shared proxy lease decisions Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/lease.bats | 115 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/proxy/lease.bats diff --git a/tests/proxy/lease.bats b/tests/proxy/lease.bats new file mode 100644 index 00000000..78158a02 --- /dev/null +++ b/tests/proxy/lease.bats @@ -0,0 +1,115 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + TEST_LOG="$BATS_TEST_TMPDIR/docker.log" + : > "$TEST_LOG" + + # shellcheck source=.docker/scripts/proxy/common.sh + source "$REPO_ROOT/.docker/scripts/proxy/common.sh" + # shellcheck source=.docker/scripts/proxy/lease.sh + source "$REPO_ROOT/.docker/scripts/proxy/lease.sh" + + project=current + coordinator_container=current-coordinator + PROXY_LEASE_GRACE_SECONDS=0 +} + +@test "current project coordinator is not another proxy client" { + Docker() { + printf '%s\n' current-coordinator + } + container_project() { + printf '%s\n' current + } + + run other_proxy_client_is_running + + [ "$status" -eq 1 ] +} + +@test "coordinator from another project keeps the proxy leased" { + Docker() { + printf '%s\n' current-coordinator other-coordinator + } + container_project() { + case "$1" in + current-coordinator) printf '%s\n' current ;; + other-coordinator) printf '%s\n' other ;; + esac + } + + run other_proxy_client_is_running + + [ "$status" -eq 0 ] +} + +@test "route from another project keeps the proxy leased" { + Docker() { + printf '%s\n' proxy current-route other-route + } + container_project() { + case "$1" in + proxy) printf '%s\n' "$proxy_project" ;; + current-route) printf '%s\n' current ;; + other-route) printf '%s\n' other ;; + esac + } + container_virtual_host() { + [ "$1" = other-route ] && printf '%s\n' other.localhost + } + + run other_proxy_route_is_running + + [ "$status" -eq 0 ] +} + +@test "last lease stops the shared proxy" { + proxy_lease_acquired=true + proxy_is_used_by_another_environment() { + return 1 + } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + proxy_compose() { + printf 'proxy-compose %s\n' "$*" >> "$TEST_LOG" + } + + run release_proxy_if_unused + + [ "$status" -eq 0 ] + grep -q '^proxy-compose down --remove-orphans$' "$TEST_LOG" +} + +@test "another lease prevents proxy shutdown" { + proxy_lease_acquired=true + proxy_is_used_by_another_environment() { + return 0 + } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + proxy_compose() { + printf 'proxy-compose %s\n' "$*" >> "$TEST_LOG" + } + + run release_proxy_if_unused + + [ "$status" -eq 0 ] + ! grep -q '^proxy-compose down' "$TEST_LOG" +} + +@test "acquiring a lease connects the coordinator only when needed" { + container_networks() { + printf '{}\n' + } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + + acquire_proxy_lease + + grep -q "^docker network connect $proxy_network $coordinator_container$" "$TEST_LOG" + [ "$proxy_lease_acquired" = true ] +} From 2754c1ac58c9ec560ce30b53e813bf9c13bdc2b0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 056/153] test: cover shared proxy infrastructure decisions Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/infrastructure.bats | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/proxy/infrastructure.bats diff --git a/tests/proxy/infrastructure.bats b/tests/proxy/infrastructure.bats new file mode 100644 index 00000000..0890e9d7 --- /dev/null +++ b/tests/proxy/infrastructure.bats @@ -0,0 +1,69 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + TEST_LOG="$BATS_TEST_TMPDIR/proxy.log" + : > "$TEST_LOG" + + # shellcheck source=.docker/scripts/proxy/common.sh + source "$REPO_ROOT/.docker/scripts/proxy/common.sh" + # shellcheck source=.docker/scripts/proxy/infrastructure.sh + source "$REPO_ROOT/.docker/scripts/proxy/infrastructure.sh" +} + +@test "ready proxy is reconciled and reused" { + proxy_is_ready() { return 0; } + proxy_compose() { + printf 'proxy-compose %s\n' "$*" >> "$TEST_LOG" + } + + run ensure_proxy_running + + [ "$status" -eq 0 ] + [ "$output" = reused ] + grep -q '^proxy-compose up --detach$' "$TEST_LOG" +} + +@test "missing proxy is started" { + proxy_is_ready() { return 1; } + ensure_ports_available() { return 0; } + start_proxy() { + printf 'start-proxy\n' >> "$TEST_LOG" + } + + run ensure_proxy_running + + [ "$status" -eq 0 ] + [ "$output" = started ] + grep -q '^start-proxy$' "$TEST_LOG" +} + +@test "occupied required port prevents startup" { + port_is_in_use() { + [ "$1" = 80 ] + } + show_conflict() { + printf 'conflict %s\n' "$1" >> "$TEST_LOG" + } + + run ensure_ports_available + + [ "$status" -eq 1 ] + grep -q '^conflict 80$' "$TEST_LOG" +} + +@test "existing proxy network is reused" { + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + case "$*" in + "network inspect $proxy_network") return 0 ;; + esac + return 1 + } + + run ensure_proxy_network + + [ "$status" -eq 0 ] + grep -q "^docker network inspect $proxy_network$" "$TEST_LOG" + ! grep -q '^docker network create' "$TEST_LOG" +} From e7f65326d7065c2f78a68a1d83412dd2434f1a12 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 057/153] test: cover proxied service coordination Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/services.bats | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/proxy/services.bats diff --git a/tests/proxy/services.bats b/tests/proxy/services.bats new file mode 100644 index 00000000..4fcf639c --- /dev/null +++ b/tests/proxy/services.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + TEST_LOG="$BATS_TEST_TMPDIR/services.log" + : > "$TEST_LOG" + + # shellcheck source=.docker/scripts/proxy/common.sh + source "$REPO_ROOT/.docker/scripts/proxy/common.sh" + # shellcheck source=.docker/scripts/proxy/services.sh + source "$REPO_ROOT/.docker/scripts/proxy/services.sh" + + project=current +} + +@test "running service is connected to proxy network" { + service_is_running() { return 0; } + container_for_service() { printf '%s\n' service-container; } + container_networks() { printf '{}\n'; } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + + connect_running_service_to_proxy_network nginx + + grep -q "^docker network connect $proxy_network service-container$" "$TEST_LOG" +} + +@test "service already on proxy network is not connected twice" { + service_is_running() { return 0; } + container_for_service() { printf '%s\n' service-container; } + container_networks() { printf '{\"%s\":{}}\n' "$proxy_network"; } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + + connect_running_service_to_proxy_network nginx + + ! grep -q '^docker network connect' "$TEST_LOG" +} + +@test "stopped service is ignored" { + service_is_running() { return 1; } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + + connect_running_service_to_proxy_network nginx + + [ ! -s "$TEST_LOG" ] +} From a734a2394e8e4cdddb70dddce5eccc7c02e6ec02 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 058/153] test: add minimal proxy integration fixture Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/proxy/fixtures/compose.yml diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml new file mode 100644 index 00000000..e7aa35e1 --- /dev/null +++ b/tests/proxy/fixtures/compose.yml @@ -0,0 +1,28 @@ +services: + nginx: + image: nginx:alpine + environment: + - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost + - VIRTUAL_PORT=80 + - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost + + proxy-coordinator: + image: docker:29-cli + entrypoint: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh"] + labels: + coop.librecode.dev-proxy-client: "true" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${REPO_ROOT}:/project:ro + working_dir: /project + environment: + - PROJECT_DIR=/project + - PROJECT_COMPOSE_FILE=/project/tests/proxy/fixtures/compose.yml + - PROXY_LEASE_GRACE_SECONDS=0 + - NEXTCLOUD_ADMIN_USER=admin + - NEXTCLOUD_ADMIN_PASSWORD=admin + - VERSION_NEXTCLOUD=master + restart: "no" + stop_grace_period: 10s + depends_on: + - nginx From 6ab5227dffee7531655cd913d4be665ef4ec3fc2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:09 -0300 Subject: [PATCH 059/153] test: cover shared proxy lifecycle and routing Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 111 +++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/proxy/integration.bats diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats new file mode 100644 index 00000000..f7011388 --- /dev/null +++ b/tests/proxy/integration.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + FIXTURE="$REPO_ROOT/tests/proxy/fixtures/compose.yml" + BODY="$BATS_TEST_TMPDIR/body.html" + cleanup_proxy_tests +} + +teardown() { + cleanup_proxy_tests +} + +compose_test() { + project="$1" + shift + COMPOSE_PROJECT_NAME="$project" REPO_ROOT="$REPO_ROOT" \ + docker compose --project-name "$project" --file "$FIXTURE" "$@" +} + +cleanup_proxy_tests() { + for project in proxytesta proxytestb; do + COMPOSE_PROJECT_NAME="$project" REPO_ROOT="$REPO_ROOT" \ + docker compose --project-name "$project" --file "$FIXTURE" down --volumes --remove-orphans >/dev/null 2>&1 || true + done + + docker compose \ + --project-name librecode-dev-proxy \ + --project-directory "$REPO_ROOT" \ + --file "$REPO_ROOT/.docker/docker-compose.proxy.yml" \ + down --remove-orphans >/dev/null 2>&1 || true + + docker network rm librecode-dev-proxy >/dev/null 2>&1 || true +} + +container_is_running() { + docker ps --format '{{.Names}}' | grep -qx "$1" +} + +wait_for_running() { + name="$1" + for _ in $(seq 1 60); do + container_is_running "$name" && return 0 + sleep 0.5 + done + return 1 +} + +wait_for_absent() { + name="$1" + for _ in $(seq 1 60); do + container_is_running "$name" || return 0 + sleep 0.5 + done + return 1 +} + +wait_for_https_status() { + host="$1" + expected="$2" + + for _ in $(seq 1 60); do + status="$(curl --silent --show-error --insecure \ + --resolve "$host:443:127.0.0.1" \ + --output "$BODY" \ + --write-out '%{http_code}' \ + "https://$host/" 2>/dev/null || true)" + [ "$status" = "$expected" ] && return 0 + sleep 0.5 + done + return 1 +} + +@test "single project starts routing and releases the shared proxy" { + compose_test proxytesta up --detach + + wait_for_running librecode-dev-proxy + wait_for_running librecode-dev-proxy-ssl-companion + wait_for_https_status localhost 200 + grep -q 'LibreCode Development Proxy' "$BODY" + + wait_for_https_status proxytesta.localhost 200 + grep -q 'Welcome to nginx' "$BODY" + + wait_for_https_status something-wrong.localhost 404 + grep -q 'Environment not found' "$BODY" + + compose_test proxytesta stop + + wait_for_absent librecode-dev-proxy + wait_for_absent librecode-dev-proxy-ssl-companion +} + +@test "shared proxy stays alive until the last project stops" { + compose_test proxytesta up --detach + compose_test proxytestb up --detach + + wait_for_running librecode-dev-proxy + wait_for_https_status proxytesta.localhost 200 + wait_for_https_status proxytestb.localhost 200 + + compose_test proxytesta stop + + wait_for_running librecode-dev-proxy + wait_for_https_status proxytestb.localhost 200 + + compose_test proxytestb stop + + wait_for_absent librecode-dev-proxy + wait_for_absent librecode-dev-proxy-ssl-companion +} From 1b4a856da0e51f980d5a54b42fd729db9fe8257a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 060/153] ci: test shared proxy lifecycle with Bats Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/proxy-tests.yml | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/proxy-tests.yml diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml new file mode 100644 index 00000000..411c943a --- /dev/null +++ b/.github/workflows/proxy-tests.yml @@ -0,0 +1,46 @@ +name: Proxy tests + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + unit: + name: Proxy unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Setup Bats-core + uses: bats-core/bats-action@4.0.0 + with: + support-install: false + assert-install: false + detik-install: false + file-install: false + - name: Run unit tests + run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats + + integration: + name: Proxy Docker integration tests + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - name: Setup Bats-core + uses: bats-core/bats-action@4.0.0 + with: + support-install: false + assert-install: false + detik-install: false + file-install: false + - name: Validate Compose files + run: | + docker compose config --quiet + REPO_ROOT="$GITHUB_WORKSPACE" COMPOSE_PROJECT_NAME=proxytesta docker compose --file tests/proxy/fixtures/compose.yml config --quiet + - name: Run Docker integration tests + run: bats tests/proxy/integration.bats From 5be1aa50d3a62052041f00cd809055dd97e86dff Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 061/153] fix: make proxy coordinator shellcheck clean Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index b3b5a227..eee5ecf6 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -2,18 +2,19 @@ set -eu -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +script_dir="$(cd -- "$(dirname -- "$0")" && pwd)" proxy_lib_dir="${PROXY_LIB_DIR:-$script_dir/proxy}" -# shellcheck source=.docker/scripts/proxy/common.sh +# The modules are loaded from a runtime path in the coordinator container. +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/common.sh" -# shellcheck source=.docker/scripts/proxy/infrastructure.sh +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/infrastructure.sh" -# shellcheck source=.docker/scripts/proxy/assets.sh +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/assets.sh" -# shellcheck source=.docker/scripts/proxy/services.sh +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/services.sh" -# shellcheck source=.docker/scripts/proxy/lease.sh +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/lease.sh" coordinator_container="$(hostname)" From 59bf8af1d9b3311150e0e475dea9a25b1c602fd2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 062/153] fix: document sourced proxy globals for shellcheck Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/common.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.docker/scripts/proxy/common.sh b/.docker/scripts/proxy/common.sh index fb17b6be..59415123 100644 --- a/.docker/scripts/proxy/common.sh +++ b/.docker/scripts/proxy/common.sh @@ -1,5 +1,8 @@ #!/bin/sh +# This module defines and consumes globals shared by the sourced proxy modules. +# shellcheck disable=SC2034,SC2154 + proxy_project=${PROXY_PROJECT:-librecode-dev-proxy} proxy_network=${PROXY_NETWORK:-librecode-dev-proxy} proxy_label=${PROXY_LABEL:-coop.librecode.dev-proxy=true} From 511c31629aa66cdb0c0b7323ca65dfc86eb389ed Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 063/153] fix: mark sourced proxy globals for infrastructure module Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/infrastructure.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.docker/scripts/proxy/infrastructure.sh b/.docker/scripts/proxy/infrastructure.sh index 9c013dde..0e3fe71d 100644 --- a/.docker/scripts/proxy/infrastructure.sh +++ b/.docker/scripts/proxy/infrastructure.sh @@ -1,5 +1,8 @@ #!/bin/sh +# Globals are provided by common.sh before this module is sourced. +# shellcheck disable=SC2154 + container_for_published_port() { port="$1" From 4bca3fec90d94612727c9bc695df86de87b9691d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 064/153] fix: mark sourced proxy globals for asset module Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index 93a1fe5c..affaf05d 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -1,5 +1,9 @@ #!/bin/sh +# Globals are provided by common.sh before this module is sourced. +# The single-quoted shell snippets are intentionally evaluated in helper containers. +# shellcheck disable=SC2154,SC2016 + copy_to_named_volume() { volume="$1" source="$2" From 093187e87360ba4eece350a43d88213be26dbafd Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 065/153] fix: mark sourced proxy globals for service module Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/services.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.docker/scripts/proxy/services.sh b/.docker/scripts/proxy/services.sh index d35996f9..e219f7e2 100644 --- a/.docker/scripts/proxy/services.sh +++ b/.docker/scripts/proxy/services.sh @@ -1,5 +1,8 @@ #!/bin/sh +# Globals are provided by common.sh before this module is sourced. +# shellcheck disable=SC2154 + service_is_running() { compose ps --status running --services | grep -qx "$1" From 943da4f80ad773d5993360394a847d3780affb6f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 066/153] fix: mark sourced proxy globals for lease module Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/lease.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.docker/scripts/proxy/lease.sh b/.docker/scripts/proxy/lease.sh index 6e080c25..d7c300a8 100644 --- a/.docker/scripts/proxy/lease.sh +++ b/.docker/scripts/proxy/lease.sh @@ -1,5 +1,8 @@ #!/bin/sh +# Globals are provided by common.sh and proxy-coordinator.sh before use. +# shellcheck disable=SC2154 + proxy_lease_acquired=false acquire_proxy_lease() { From db23c2d2b20fdbaa78163cfaa913fad21675d32b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 067/153] fix: keep proxy test fixture valid inside coordinator Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index e7aa35e1..0e91d81d 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -18,6 +18,7 @@ services: environment: - PROJECT_DIR=/project - PROJECT_COMPOSE_FILE=/project/tests/proxy/fixtures/compose.yml + - REPO_ROOT=/project - PROXY_LEASE_GRACE_SECONDS=0 - NEXTCLOUD_ADMIN_USER=admin - NEXTCLOUD_ADMIN_PASSWORD=admin From 9d5c1db955605b0edd7dab7c5196b2a390d76aa9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 068/153] fix: bound shared proxy shutdown time Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/lease.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.docker/scripts/proxy/lease.sh b/.docker/scripts/proxy/lease.sh index d7c300a8..72d54870 100644 --- a/.docker/scripts/proxy/lease.sh +++ b/.docker/scripts/proxy/lease.sh @@ -74,7 +74,9 @@ release_proxy_if_unused() { fi echo 'Stopping unused shared development proxy.' - if ! proxy_compose down --remove-orphans; then + if ! proxy_compose down \ + --timeout "${PROXY_STOP_TIMEOUT_SECONDS:-3}" \ + --remove-orphans; then echo 'Could not stop the unused shared development proxy.' >&2 return 1 fi From d1fa931be686f09732d122f43a913edf3137f292 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 069/153] test: cover Ctrl+C proxy shutdown Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats index f7011388..07f872c9 100644 --- a/tests/proxy/integration.bats +++ b/tests/proxy/integration.bats @@ -91,6 +91,27 @@ wait_for_https_status() { wait_for_absent librecode-dev-proxy-ssl-companion } +@test "Ctrl+C on attached compose stops the last shared proxy" { + log="$BATS_TEST_TMPDIR/compose-up.log" + + COMPOSE_PROJECT_NAME=proxytesta REPO_ROOT="$REPO_ROOT" \ + docker compose \ + --project-name proxytesta \ + --file "$FIXTURE" \ + up >"$log" 2>&1 & + compose_pid=$! + + wait_for_running librecode-dev-proxy + wait_for_running librecode-dev-proxy-ssl-companion + wait_for_https_status proxytesta.localhost 200 + + kill -INT "$compose_pid" + wait "$compose_pid" || true + + wait_for_absent librecode-dev-proxy + wait_for_absent librecode-dev-proxy-ssl-companion +} + @test "shared proxy stays alive until the last project stops" { compose_test proxytesta up --detach compose_test proxytestb up --detach From eae012f7faba602d25fcd2fd0e0d892215d7cc0b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 070/153] test: expect bounded proxy shutdown Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/lease.bats | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/proxy/lease.bats b/tests/proxy/lease.bats index 78158a02..8ebc5deb 100644 --- a/tests/proxy/lease.bats +++ b/tests/proxy/lease.bats @@ -64,8 +64,9 @@ setup() { [ "$status" -eq 0 ] } -@test "last lease stops the shared proxy" { +@test "last lease stops the shared proxy with bounded timeout" { proxy_lease_acquired=true + PROXY_STOP_TIMEOUT_SECONDS=7 proxy_is_used_by_another_environment() { return 1 } @@ -79,7 +80,7 @@ setup() { run release_proxy_if_unused [ "$status" -eq 0 ] - grep -q '^proxy-compose down --remove-orphans$' "$TEST_LOG" + grep -q '^proxy-compose down --timeout 7 --remove-orphans$' "$TEST_LOG" } @test "another lease prevents proxy shutdown" { From b55dbba91866b26b13876468084e5c566fda04f2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 071/153] refactor: make proxy lease release idempotent Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/lease.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.docker/scripts/proxy/lease.sh b/.docker/scripts/proxy/lease.sh index 72d54870..a8b590d3 100644 --- a/.docker/scripts/proxy/lease.sh +++ b/.docker/scripts/proxy/lease.sh @@ -3,24 +3,20 @@ # Globals are provided by common.sh and proxy-coordinator.sh before use. # shellcheck disable=SC2154 -proxy_lease_acquired=false - acquire_proxy_lease() { if ! container_networks "$coordinator_container" | grep -q "\"$proxy_network\""; then Docker network connect "$proxy_network" "$coordinator_container" fi - - proxy_lease_acquired=true } release_proxy_lease() { - [ "$proxy_lease_acquired" = true ] || return 0 + if ! container_networks "$coordinator_container" | grep -q "\"$proxy_network\""; then + return 0 + fi if ! Docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1; then echo 'Could not disconnect this coordinator lease from the shared proxy network; continuing with project-based lease detection.' >&2 fi - - proxy_lease_acquired=false } other_proxy_client_is_running() { From a752bcd473f2f6b333968f8de74fe277bad540ad Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 072/153] fix: support reliable proxy pre-stop cleanup Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 36 +++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index eee5ecf6..2ad62a61 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -4,6 +4,7 @@ set -eu script_dir="$(cd -- "$(dirname -- "$0")" && pwd)" proxy_lib_dir="${PROXY_LIB_DIR:-$script_dir/proxy}" +release_marker=/tmp/librecode-proxy-lease-released # The modules are loaded from a runtime path in the coordinator container. # shellcheck disable=SC1090,SC1091 @@ -46,10 +47,23 @@ success() { esac } +release() { + if [ -f "$release_marker" ]; then + return 0 + fi + + echo 'Releasing shared development proxy lease.' + if release_proxy_if_unused; then + touch "$release_marker" + return 0 + fi + + return 1 +} + shutdown() { trap - INT TERM HUP - echo 'Releasing shared development proxy lease.' - release_proxy_if_unused || true + release || true exit 0 } @@ -62,7 +76,7 @@ wait_for_shutdown() { done } -main() { +run() { validate_environment ensure_proxy_network install_proxy_assets @@ -70,6 +84,7 @@ main() { proxy_state="$(ensure_proxy_running)" acquire_proxy_lease + rm -f "$release_marker" trap shutdown INT TERM HUP connect_project_services @@ -82,6 +97,15 @@ main() { wait_for_shutdown } -if [ "${PROXY_COORDINATOR_SOURCE_ONLY:-false}" != "true" ]; then - main "$@" -fi +case "${1:-run}" in + run) + run + ;; + release) + release + ;; + *) + echo "Unknown proxy coordinator command: $1" >&2 + exit 2 + ;; +esac From d751cfe9bdbe0cdf5ecc8e1902fa3fc26cfc91dc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 073/153] fix: make local shutdown fast and deterministic Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0554df69..7f201185 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: - ~/.npm:/var/www/.npm/ - ./volumes/nextcloud:/var/www/html restart: unless-stopped + stop_signal: SIGTERM + stop_grace_period: 3s depends_on: - database environment: @@ -53,6 +55,8 @@ services: # context: .docker/ # dockerfile: Dockerfile.nginx restart: unless-stopped + stop_signal: SIGTERM + stop_grace_period: 3s depends_on: - nextcloud volumes: @@ -78,18 +82,22 @@ services: - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} + pre_stop: + - command: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh", "release"] restart: "no" - stop_grace_period: 15s + stop_grace_period: 8s depends_on: - nextcloud mailpit: image: axllent/mailpit + stop_grace_period: 3s environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost - VIRTUAL_PORT=8025 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost redis: image: redis + stop_grace_period: 3s eurooffice: image: ghcr.io/euro-office/documentserver:latest profiles: @@ -158,4 +166,4 @@ services: - WHATSAPP_WEBHOOK_EVENTS=${WHATSAPP_WEBHOOK_EVENTS:-message,message.ack,group.participants,chat_presence} - WHATSAPP_AUTO_REPLY="Conta de sistema. Para falar com a LibreCode, mande email para contato@libresign.coop" volumes: - - ./volumes/go-whatsapp-web:/usr/src/app/files + - ./volumes/go-whatsapp-web:/usr/src/app/files \ No newline at end of file From 6f4156a180509d78cb2cfb140cc2c4938510a097 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 074/153] test: exercise coordinator pre-stop cleanup Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index 0e91d81d..68a12300 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -1,6 +1,8 @@ services: nginx: image: nginx:alpine + stop_signal: SIGTERM + stop_grace_period: 3s environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost - VIRTUAL_PORT=80 @@ -23,7 +25,9 @@ services: - NEXTCLOUD_ADMIN_USER=admin - NEXTCLOUD_ADMIN_PASSWORD=admin - VERSION_NEXTCLOUD=master + pre_stop: + - command: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh", "release"] restart: "no" - stop_grace_period: 10s + stop_grace_period: 8s depends_on: - nginx From c63e3ad6bdc891dfb0f500d743b2385e0562ddbe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 075/153] test: cover idempotent proxy lease release Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/lease.bats | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/proxy/lease.bats b/tests/proxy/lease.bats index 8ebc5deb..dd56c542 100644 --- a/tests/proxy/lease.bats +++ b/tests/proxy/lease.bats @@ -65,8 +65,10 @@ setup() { } @test "last lease stops the shared proxy with bounded timeout" { - proxy_lease_acquired=true PROXY_STOP_TIMEOUT_SECONDS=7 + container_networks() { + printf '{"%s":{}}\n' "$proxy_network" + } proxy_is_used_by_another_environment() { return 1 } @@ -80,11 +82,14 @@ setup() { run release_proxy_if_unused [ "$status" -eq 0 ] + grep -q "^docker network disconnect $proxy_network $coordinator_container$" "$TEST_LOG" grep -q '^proxy-compose down --timeout 7 --remove-orphans$' "$TEST_LOG" } @test "another lease prevents proxy shutdown" { - proxy_lease_acquired=true + container_networks() { + printf '{"%s":{}}\n' "$proxy_network" + } proxy_is_used_by_another_environment() { return 0 } @@ -98,9 +103,24 @@ setup() { run release_proxy_if_unused [ "$status" -eq 0 ] + grep -q "^docker network disconnect $proxy_network $coordinator_container$" "$TEST_LOG" ! grep -q '^proxy-compose down' "$TEST_LOG" } +@test "release is safe when compose already disconnected the coordinator" { + container_networks() { + printf '{}\n' + } + Docker() { + printf 'docker %s\n' "$*" >> "$TEST_LOG" + } + + run release_proxy_lease + + [ "$status" -eq 0 ] + ! grep -q '^docker network disconnect' "$TEST_LOG" +} + @test "acquiring a lease connects the coordinator only when needed" { container_networks() { printf '{}\n' @@ -112,5 +132,4 @@ setup() { acquire_proxy_lease grep -q "^docker network connect $proxy_network $coordinator_container$" "$TEST_LOG" - [ "$proxy_lease_acquired" = true ] } From 96ffa12a148bb52926aa3cd119fa50241f76a455 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 076/153] test: bound attached compose shutdown time Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats index 07f872c9..cc29b924 100644 --- a/tests/proxy/integration.bats +++ b/tests/proxy/integration.bats @@ -91,7 +91,7 @@ wait_for_https_status() { wait_for_absent librecode-dev-proxy-ssl-companion } -@test "Ctrl+C on attached compose stops the last shared proxy" { +@test "Ctrl+C on attached compose stops the last shared proxy promptly" { log="$BATS_TEST_TMPDIR/compose-up.log" COMPOSE_PROJECT_NAME=proxytesta REPO_ROOT="$REPO_ROOT" \ @@ -105,11 +105,16 @@ wait_for_https_status() { wait_for_running librecode-dev-proxy-ssl-companion wait_for_https_status proxytesta.localhost 200 + started_at="$(date +%s)" kill -INT "$compose_pid" wait "$compose_pid" || true + finished_at="$(date +%s)" wait_for_absent librecode-dev-proxy wait_for_absent librecode-dev-proxy-ssl-companion + + elapsed=$((finished_at - started_at)) + [ "$elapsed" -lt 10 ] } @test "shared proxy stays alive until the last project stops" { From fa2012d651bf0d2f7a55e4908023ef6a0b03ef3f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 077/153] fix: keep compose lifecycle hooks backward compatible Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7f201185..ec012240 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,8 +82,6 @@ services: - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} - pre_stop: - - command: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh", "release"] restart: "no" stop_grace_period: 8s depends_on: @@ -166,4 +164,4 @@ services: - WHATSAPP_WEBHOOK_EVENTS=${WHATSAPP_WEBHOOK_EVENTS:-message,message.ack,group.participants,chat_presence} - WHATSAPP_AUTO_REPLY="Conta de sistema. Para falar com a LibreCode, mande email para contato@libresign.coop" volumes: - - ./volumes/go-whatsapp-web:/usr/src/app/files \ No newline at end of file + - ./volumes/go-whatsapp-web:/usr/src/app/files From b53ff0d0a603bc2d0c051fd1b550010c5ced7e2d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 078/153] test: avoid unsupported compose lifecycle hook Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index 68a12300..a2901a62 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -25,8 +25,6 @@ services: - NEXTCLOUD_ADMIN_USER=admin - NEXTCLOUD_ADMIN_PASSWORD=admin - VERSION_NEXTCLOUD=master - pre_stop: - - command: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh", "release"] restart: "no" stop_grace_period: 8s depends_on: From e68db4241acfffef4ddbbbb1ac7e9ab9de4bec62 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:10 -0300 Subject: [PATCH 079/153] fix: process coordinator shutdown signals promptly Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 2ad62a61..5ac45df7 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -70,8 +70,11 @@ shutdown() { wait_for_shutdown() { trap shutdown INT TERM HUP + # Keep the wait interval short. Some /bin/sh implementations defer traps + # while waiting for a child process, so a long sleep can outlive Compose's + # stop grace period and prevent the lease cleanup from running. while :; do - sleep 3600 & + sleep "${PROXY_SIGNAL_POLL_SECONDS:-1}" & wait "$!" || true done } From 18eaf67a5d8106c6512f4f4e1a788c6d15f25d83 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 080/153] fix: run proxy coordinator with init Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ec012240..4420f313 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,6 +69,7 @@ services: - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: image: docker:29-cli + init: true entrypoint: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" From ff0122c514463233f8fc2dc361a479e3e26b94f7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 081/153] test: run proxy coordinator with init Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index a2901a62..7700a0ed 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -10,6 +10,7 @@ services: proxy-coordinator: image: docker:29-cli + init: true entrypoint: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" From 1e8ab5844dfb73b6727a889cd908e32a5e1bedee Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 082/153] feat: generate proxy runtime diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/diagnostics.sh | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .docker/scripts/proxy/diagnostics.sh diff --git a/.docker/scripts/proxy/diagnostics.sh b/.docker/scripts/proxy/diagnostics.sh new file mode 100644 index 00000000..4dba0cad --- /dev/null +++ b/.docker/scripts/proxy/diagnostics.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# Globals are provided by common.sh before this module is sourced. +# shellcheck disable=SC2154 + +runtime_version() { + component="$1" + + case "$component" in + docker) + Docker version --format '{{.Server.Version}}' + ;; + compose) + Docker compose version --short + ;; + runc) + Docker version --format '{{range .Server.Components}}{{if eq .Name "runc"}}{{.Version}}{{end}}{{end}}' + ;; + esac +} + +version_at_most() { + version="${1#v}" + maximum="${2#v}" + + version="${version%%-*}" + maximum="${maximum%%-*}" + + old_ifs="$IFS" + IFS=. + set -- $version + version_major="${1:-0}" + version_minor="${2:-0}" + version_patch="${3:-0}" + set -- $maximum + maximum_major="${1:-0}" + maximum_minor="${2:-0}" + maximum_patch="${3:-0}" + IFS="$old_ifs" + + [ "$version_major" -lt "$maximum_major" ] && return 0 + [ "$version_major" -gt "$maximum_major" ] && return 1 + [ "$version_minor" -lt "$maximum_minor" ] && return 0 + [ "$version_minor" -gt "$maximum_minor" ] && return 1 + [ "$version_patch" -le "$maximum_patch" ] +} + +runtime_has_known_shutdown_risk() { + docker_version="$1" + runc_version="$2" + + version_at_most "$docker_version" "${PROXY_KNOWN_BAD_DOCKER_MAX:-25.0.2}" && + version_at_most "$runc_version" "${PROXY_KNOWN_BAD_RUNC_MAX:-1.1.12}" +} + +runtime_diagnostics_json() { + docker_version="$(runtime_version docker)" + compose_version="$(runtime_version compose)" + runc_version="$(runtime_version runc)" + + if runtime_has_known_shutdown_risk "$docker_version" "$runc_version"; then + warnings='[{"code":"outdated-docker-runtime","message":"This Docker and runc combination may fail to stop containers correctly on recent Linux/AppArmor hosts. Update Docker Engine before investigating shutdown problems."}]' + else + warnings='[]' + fi + + printf '{"docker":"%s","compose":"%s","runc":"%s","warnings":%s}\n' \ + "$docker_version" "$compose_version" "$runc_version" "$warnings" +} + +install_runtime_diagnostics() { + runtime_diagnostics_json | + Docker run --rm -i \ + -v "$proxy_assets_volume:/target" \ + docker:29-cli \ + sh -c 'cat > /target/runtime.json' +} From 00353cbba3957f97cad84d16fe86e2d7d9f230a2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 083/153] feat: publish proxy runtime diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 5ac45df7..9532cbbb 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -14,6 +14,8 @@ release_marker=/tmp/librecode-proxy-lease-released # shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/assets.sh" # shellcheck disable=SC1090,SC1091 +. "$proxy_lib_dir/diagnostics.sh" +# shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/services.sh" # shellcheck disable=SC1090,SC1091 . "$proxy_lib_dir/lease.sh" @@ -83,6 +85,7 @@ run() { validate_environment ensure_proxy_network install_proxy_assets + install_runtime_diagnostics proxy_state="$(ensure_proxy_running)" From 55bee39d7c1696db2209e66a88ef28c7c7a1248d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 084/153] feat: serve proxy runtime diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/localhost_location_override | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/nginx-proxy/localhost_location_override b/.docker/nginx-proxy/localhost_location_override index a831b0aa..b3f4e9c6 100644 --- a/.docker/nginx-proxy/localhost_location_override +++ b/.docker/nginx-proxy/localhost_location_override @@ -1,2 +1,2 @@ root /usr/share/nginx/html; -try_files /index.html =404; +try_files $uri /index.html =404; From d1bdf659269ad9764bfb0c1fb90e6e8916a1c65a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 085/153] feat: show runtime diagnostics on proxy dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 54 +++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl index b8c31fb0..799fbe25 100644 --- a/.docker/nginx-proxy/dashboard.tmpl +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -46,7 +46,11 @@ li { margin: .45rem 0; } a { color: LinkText; } code { padding: .15em .4em; border-radius: 4px; background: color-mix(in srgb, CanvasText 10%, transparent); } - .not-found { padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 24%, transparent); border-radius: 8px; } + .not-found, .runtime-warning { padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 24%, transparent); border-radius: 8px; } + .runtime-warning { margin-top: 1rem; border-color: #c47f00; } + .runtime-warning strong { display: block; margin-bottom: .4rem; } + .runtime-list { list-style: none; padding: 0; } + .runtime-list li { margin: .35rem 0; } .muted { opacity: .7; } @@ -62,6 +66,12 @@

The shared development proxy is running.

Active environments

+ + +

This page is generated from the routes currently connected to the shared development proxy.

@@ -141,6 +151,48 @@ link.textContent = closest; suggestion.hidden = false; } + } else { + const runtimeSection = document.getElementById('runtime-section'); + runtimeSection.hidden = false; + + fetch('/runtime.json', { cache: 'no-store' }) + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((runtime) => { + const checks = document.getElementById('runtime-checks'); + checks.replaceChildren(); + + const list = document.createElement('ul'); + list.className = 'runtime-list'; + for (const [label, version] of [ + ['Docker Engine', runtime.docker], + ['Docker Compose', runtime.compose], + ['runc', runtime.runc], + ]) { + const item = document.createElement('li'); + item.textContent = `βœ“ ${label} ${version || 'unknown'}`; + list.appendChild(item); + } + checks.appendChild(list); + + for (const warning of runtime.warnings || []) { + const panel = document.createElement('div'); + panel.className = 'runtime-warning'; + const title = document.createElement('strong'); + title.textContent = '⚠ Docker runtime needs attention'; + const message = document.createElement('span'); + message.textContent = warning.message; + panel.append(title, message); + checks.appendChild(panel); + } + }) + .catch(() => { + const checks = document.getElementById('runtime-checks'); + checks.textContent = 'Runtime information is not available.'; + checks.className = 'muted'; + }); } From 49f1374add9d5ffb3c72a84fb986fe1359bf6b14 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 086/153] test: cover proxy runtime diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/diagnostics.bats | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/proxy/diagnostics.bats diff --git a/tests/proxy/diagnostics.bats b/tests/proxy/diagnostics.bats new file mode 100644 index 00000000..0f25756c --- /dev/null +++ b/tests/proxy/diagnostics.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" + + # shellcheck source=.docker/scripts/proxy/common.sh + source "$REPO_ROOT/.docker/scripts/proxy/common.sh" + # shellcheck source=.docker/scripts/proxy/diagnostics.sh + source "$REPO_ROOT/.docker/scripts/proxy/diagnostics.sh" +} + +@test "known old Docker and runc combination reports shutdown risk" { + run runtime_has_known_shutdown_risk 25.0.2 1.1.12 + + [ "$status" -eq 0 ] +} + +@test "new Docker runtime does not report known shutdown risk" { + run runtime_has_known_shutdown_risk 29.8.0 1.5.1 + + [ "$status" -eq 1 ] +} + +@test "new runc avoids warning even with old Docker" { + run runtime_has_known_shutdown_risk 25.0.2 1.5.1 + + [ "$status" -eq 1 ] +} + +@test "runtime diagnostics include versions without warning for current runtime" { + runtime_version() { + case "$1" in + docker) printf '%s\n' 29.8.0 ;; + compose) printf '%s\n' 5.5.1 ;; + runc) printf '%s\n' 1.5.1 ;; + esac + } + + run runtime_diagnostics_json + + [ "$status" -eq 0 ] + [[ "$output" == *'"docker":"29.8.0"'* ]] + [[ "$output" == *'"compose":"5.5.1"'* ]] + [[ "$output" == *'"runc":"1.5.1"'* ]] + [[ "$output" == *'"warnings":[]'* ]] +} + +@test "runtime diagnostics warn for known old runtime combination" { + runtime_version() { + case "$1" in + docker) printf '%s\n' 25.0.2 ;; + compose) printf '%s\n' 2.24.5 ;; + runc) printf '%s\n' 1.1.12 ;; + esac + } + + run runtime_diagnostics_json + + [ "$status" -eq 0 ] + [[ "$output" == *'"code":"outdated-docker-runtime"'* ]] + [[ "$output" == *'Update Docker Engine'* ]] +} From 357cd023885dc514c65c0c9c3e69aa80aa73e5a4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 087/153] ci: run proxy runtime diagnostic tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/proxy-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index 411c943a..d4b515bd 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -23,7 +23,7 @@ jobs: detik-install: false file-install: false - name: Run unit tests - run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats + run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats integration: name: Proxy Docker integration tests From b6701a55ad9c477fe28afbb6990e2cd542e42c64 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 088/153] test: cover proxy runtime diagnostics endpoint Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats index cc29b924..3e84e0ee 100644 --- a/tests/proxy/integration.bats +++ b/tests/proxy/integration.bats @@ -55,22 +55,27 @@ wait_for_absent() { return 1 } -wait_for_https_status() { +wait_for_https_path_status() { host="$1" - expected="$2" + path="$2" + expected="$3" for _ in $(seq 1 60); do status="$(curl --silent --show-error --insecure \ --resolve "$host:443:127.0.0.1" \ --output "$BODY" \ --write-out '%{http_code}' \ - "https://$host/" 2>/dev/null || true)" + "https://$host$path" 2>/dev/null || true)" [ "$status" = "$expected" ] && return 0 sleep 0.5 done return 1 } +wait_for_https_status() { + wait_for_https_path_status "$1" / "$2" +} + @test "single project starts routing and releases the shared proxy" { compose_test proxytesta up --detach @@ -78,6 +83,12 @@ wait_for_https_status() { wait_for_running librecode-dev-proxy-ssl-companion wait_for_https_status localhost 200 grep -q 'LibreCode Development Proxy' "$BODY" + grep -q 'Environment checks' "$BODY" + + wait_for_https_path_status localhost /runtime.json 200 + grep -q '"docker":"' "$BODY" + grep -q '"compose":"' "$BODY" + grep -q '"runc":"' "$BODY" wait_for_https_status proxytesta.localhost 200 grep -q 'Welcome to nginx' "$BODY" From 0cfea3eb3b80f44fc4dca1f81a86e98d8587bd68 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 089/153] fix: report host runtime versions only Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/diagnostics.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.docker/scripts/proxy/diagnostics.sh b/.docker/scripts/proxy/diagnostics.sh index 4dba0cad..07c49e17 100644 --- a/.docker/scripts/proxy/diagnostics.sh +++ b/.docker/scripts/proxy/diagnostics.sh @@ -10,9 +10,6 @@ runtime_version() { docker) Docker version --format '{{.Server.Version}}' ;; - compose) - Docker compose version --short - ;; runc) Docker version --format '{{range .Server.Components}}{{if eq .Name "runc"}}{{.Version}}{{end}}{{end}}' ;; @@ -55,7 +52,6 @@ runtime_has_known_shutdown_risk() { runtime_diagnostics_json() { docker_version="$(runtime_version docker)" - compose_version="$(runtime_version compose)" runc_version="$(runtime_version runc)" if runtime_has_known_shutdown_risk "$docker_version" "$runc_version"; then @@ -64,8 +60,8 @@ runtime_diagnostics_json() { warnings='[]' fi - printf '{"docker":"%s","compose":"%s","runc":"%s","warnings":%s}\n' \ - "$docker_version" "$compose_version" "$runc_version" "$warnings" + printf '{"docker":"%s","runc":"%s","warnings":%s}\n' \ + "$docker_version" "$runc_version" "$warnings" } install_runtime_diagnostics() { From 031a6010531b851af566c177b86193c204847086 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 090/153] test: use host runtime versions in diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/diagnostics.bats | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/proxy/diagnostics.bats b/tests/proxy/diagnostics.bats index 0f25756c..634910cb 100644 --- a/tests/proxy/diagnostics.bats +++ b/tests/proxy/diagnostics.bats @@ -27,11 +27,10 @@ setup() { [ "$status" -eq 1 ] } -@test "runtime diagnostics include versions without warning for current runtime" { +@test "runtime diagnostics include host versions without warning for current runtime" { runtime_version() { case "$1" in docker) printf '%s\n' 29.8.0 ;; - compose) printf '%s\n' 5.5.1 ;; runc) printf '%s\n' 1.5.1 ;; esac } @@ -40,7 +39,6 @@ setup() { [ "$status" -eq 0 ] [[ "$output" == *'"docker":"29.8.0"'* ]] - [[ "$output" == *'"compose":"5.5.1"'* ]] [[ "$output" == *'"runc":"1.5.1"'* ]] [[ "$output" == *'"warnings":[]'* ]] } @@ -49,7 +47,6 @@ setup() { runtime_version() { case "$1" in docker) printf '%s\n' 25.0.2 ;; - compose) printf '%s\n' 2.24.5 ;; runc) printf '%s\n' 1.1.12 ;; esac } From 5c5f9a0bbefcd940ddb5af3887e12b650e7416f3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 091/153] fix: show only host runtime versions Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 1 - 1 file changed, 1 deletion(-) diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl index 799fbe25..699214e6 100644 --- a/.docker/nginx-proxy/dashboard.tmpl +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -168,7 +168,6 @@ list.className = 'runtime-list'; for (const [label, version] of [ ['Docker Engine', runtime.docker], - ['Docker Compose', runtime.compose], ['runc', runtime.runc], ]) { const item = document.createElement('li'); From d1be7b7e040c2bc9a1f584523b273547aadc342f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 092/153] test: assert host runtime diagnostics only Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats index 3e84e0ee..d9e4eb72 100644 --- a/tests/proxy/integration.bats +++ b/tests/proxy/integration.bats @@ -87,7 +87,6 @@ wait_for_https_status() { wait_for_https_path_status localhost /runtime.json 200 grep -q '"docker":"' "$BODY" - grep -q '"compose":"' "$BODY" grep -q '"runc":"' "$BODY" wait_for_https_status proxytesta.localhost 200 From 70f0747b867c724de2ca03a3b1d3a5825ade1cc4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 093/153] fix: publish runtime diagnostics from running proxy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/diagnostics.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.docker/scripts/proxy/diagnostics.sh b/.docker/scripts/proxy/diagnostics.sh index 07c49e17..306d0b57 100644 --- a/.docker/scripts/proxy/diagnostics.sh +++ b/.docker/scripts/proxy/diagnostics.sh @@ -25,10 +25,13 @@ version_at_most() { old_ifs="$IFS" IFS=. + # Intentional field splitting turns semantic versions into components. + # shellcheck disable=SC2086 set -- $version version_major="${1:-0}" version_minor="${2:-0}" version_patch="${3:-0}" + # shellcheck disable=SC2086 set -- $maximum maximum_major="${1:-0}" maximum_minor="${2:-0}" @@ -65,9 +68,10 @@ runtime_diagnostics_json() { } install_runtime_diagnostics() { + proxy_container="$(compatible_proxy_container)" + [ -n "$proxy_container" ] || return 1 + runtime_diagnostics_json | - Docker run --rm -i \ - -v "$proxy_assets_volume:/target" \ - docker:29-cli \ - sh -c 'cat > /target/runtime.json' + Docker exec -i "$proxy_container" \ + sh -c 'cat > /usr/share/nginx/html/runtime.json' } From 59c22d85e2af7a73ec2d963caef87b41879572da Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 094/153] fix: publish diagnostics after proxy startup Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 9532cbbb..6b1fddc1 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -85,9 +85,9 @@ run() { validate_environment ensure_proxy_network install_proxy_assets - install_runtime_diagnostics proxy_state="$(ensure_proxy_running)" + install_runtime_diagnostics acquire_proxy_lease rm -f "$release_marker" From 36a9bd19d3c5f0926e5fd4c75623c9389af1b664 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 095/153] refactor: remove shutdown workarounds and pin default tools Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4420f313..815a9100 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,6 @@ services: - ~/.npm:/var/www/.npm/ - ./volumes/nextcloud:/var/www/html restart: unless-stopped - stop_signal: SIGTERM - stop_grace_period: 3s depends_on: - database environment: @@ -55,8 +53,6 @@ services: # context: .docker/ # dockerfile: Dockerfile.nginx restart: unless-stopped - stop_signal: SIGTERM - stop_grace_period: 3s depends_on: - nextcloud volumes: @@ -68,8 +64,7 @@ services: - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: - image: docker:29-cli - init: true + image: docker:29.8.0-cli entrypoint: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" @@ -84,19 +79,16 @@ services: - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - VERSION_NEXTCLOUD=${VERSION_NEXTCLOUD:-master} restart: "no" - stop_grace_period: 8s depends_on: - nextcloud mailpit: - image: axllent/mailpit - stop_grace_period: 3s + image: axllent/mailpit:v1.31.1 environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost - VIRTUAL_PORT=8025 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost redis: - image: redis - stop_grace_period: 3s + image: redis:8.8.2 eurooffice: image: ghcr.io/euro-office/documentserver:latest profiles: From 604606f41cc5d528c755a513597fe275aa51d85d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 096/153] refactor: remove coordinator shutdown test workarounds Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 6b1fddc1..c843f854 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -72,11 +72,8 @@ shutdown() { wait_for_shutdown() { trap shutdown INT TERM HUP - # Keep the wait interval short. Some /bin/sh implementations defer traps - # while waiting for a child process, so a long sleep can outlive Compose's - # stop grace period and prevent the lease cleanup from running. while :; do - sleep "${PROXY_SIGNAL_POLL_SECONDS:-1}" & + sleep 3600 & wait "$!" || true done } @@ -103,15 +100,4 @@ run() { wait_for_shutdown } -case "${1:-run}" in - run) - run - ;; - release) - release - ;; - *) - echo "Unknown proxy coordinator command: $1" >&2 - exit 2 - ;; -esac +run From 56849524acdbfcc21f1dc435791691a22898b811 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 097/153] test: remove proxy fixture shutdown workarounds Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index 7700a0ed..76e65d32 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -1,16 +1,13 @@ services: nginx: - image: nginx:alpine - stop_signal: SIGTERM - stop_grace_period: 3s + image: nginx:1.31.5-alpine environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: - image: docker:29-cli - init: true + image: docker:29.8.0-cli entrypoint: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" @@ -27,6 +24,5 @@ services: - NEXTCLOUD_ADMIN_PASSWORD=admin - VERSION_NEXTCLOUD=master restart: "no" - stop_grace_period: 8s depends_on: - nginx From 8bb49c5df776374fcee4faf5c9d2206dc41d1c3d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:11 -0300 Subject: [PATCH 098/153] chore: pin proxy helper image Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index affaf05d..6442d3fc 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -4,6 +4,8 @@ # The single-quoted shell snippets are intentionally evaluated in helper containers. # shellcheck disable=SC2154,SC2016 +proxy_helper_image="${PROXY_HELPER_IMAGE:-docker:29.8.0-cli}" + copy_to_named_volume() { volume="$1" source="$2" @@ -11,7 +13,7 @@ copy_to_named_volume() { Docker run --rm -i \ -v "$volume:/target" \ - docker:29-cli \ + "$proxy_helper_image" \ sh -c 'cat > "/target/$1"' sh "$destination" \ < "$source" } @@ -22,7 +24,7 @@ install_proxy_assets() { Docker run --rm \ -v "$proxy_assets_volume:/target" \ - docker:29-cli \ + "$proxy_helper_image" \ sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/Procfile" Procfile @@ -31,7 +33,7 @@ install_proxy_assets() { Docker run --rm \ -v "$proxy_vhost_volume:/target" \ - docker:29-cli \ + "$proxy_helper_image" \ sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/localhost_location_override" localhost_location_override From f6997351a7b2e477f7d2e8913cfbab008dd6d9e9 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 099/153] chore: pin shared proxy images Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index a98bf158..bfa873ac 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -1,6 +1,6 @@ services: nginx-proxy: - image: nginxproxy/nginx-proxy:1.11-alpine + image: nginxproxy/nginx-proxy:1.11.6-alpine container_name: librecode-dev-proxy labels: coop.librecode.dev-proxy: "true" @@ -24,7 +24,7 @@ services: - proxy ssl-companion: - image: sebastienheyd/self-signed-proxy-companion:latest + image: sebastienheyd/self-signed-proxy-companion:1.1.0 container_name: librecode-dev-proxy-ssl-companion labels: coop.librecode.dev-proxy-companion: "true" From ba21959137e08da0f53ce3a77ccf2cc69f267f0b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 100/153] ci: update compose images with dependabot Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/dependabot.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index acfb8abc..a1174c85 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,19 @@ version: 2 updates: - - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" + - package-ecosystem: "docker" directory: "/.docker/" schedule: interval: "daily" + + - package-ecosystem: "docker-compose" + directories: + - "/" + - "/.docker/" + - "/tests/proxy/fixtures/" + schedule: + interval: "daily" From b051320c3ed765c1bbd02f4db28ed4c0a78b14ed Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 101/153] test: enforce proxy compose runtime policy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/compose-policy.bats | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/proxy/compose-policy.bats diff --git a/tests/proxy/compose-policy.bats b/tests/proxy/compose-policy.bats new file mode 100644 index 00000000..5859bddd --- /dev/null +++ b/tests/proxy/compose-policy.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" +} + +@test "default third-party runtime images use concrete versions" { + grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: axllent/mailpit:v[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: redis:[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: nginxproxy/nginx-proxy:[0-9]+\.[0-9]+\.[0-9]+-alpine$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" + grep -Eq '^[[:space:]]+image: sebastienheyd/self-signed-proxy-companion:[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" +} + +@test "proxy integration fixture uses concrete image versions" { + grep -Eq '^[[:space:]]+image: nginx:[0-9]+\.[0-9]+\.[0-9]+-alpine$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" + grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" +} + +@test "compose files do not keep shutdown workarounds from runtime debugging" { + for file in "$REPO_ROOT/docker-compose.yml" "$REPO_ROOT/tests/proxy/fixtures/compose.yml"; do + ! grep -Eq '^[[:space:]]+(stop_signal|stop_grace_period|init):' "$file" + done +} From 0d1b69825d24dcda65a0b61df68e40b3e44498ea Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 102/153] ci: enforce proxy compose runtime policy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/proxy-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index d4b515bd..862628ab 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -23,7 +23,7 @@ jobs: detik-install: false file-install: false - name: Run unit tests - run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats + run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats integration: name: Proxy Docker integration tests From 5629a090934d84f6145aba3396e49b3aeeb0d04d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 103/153] refactor: avoid shellcheck suppression in diagnostics Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/diagnostics.sh | 30 ++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/.docker/scripts/proxy/diagnostics.sh b/.docker/scripts/proxy/diagnostics.sh index 306d0b57..5d011051 100644 --- a/.docker/scripts/proxy/diagnostics.sh +++ b/.docker/scripts/proxy/diagnostics.sh @@ -1,8 +1,5 @@ #!/bin/sh -# Globals are provided by common.sh before this module is sourced. -# shellcheck disable=SC2154 - runtime_version() { component="$1" @@ -23,20 +20,19 @@ version_at_most() { version="${version%%-*}" maximum="${maximum%%-*}" - old_ifs="$IFS" - IFS=. - # Intentional field splitting turns semantic versions into components. - # shellcheck disable=SC2086 - set -- $version - version_major="${1:-0}" - version_minor="${2:-0}" - version_patch="${3:-0}" - # shellcheck disable=SC2086 - set -- $maximum - maximum_major="${1:-0}" - maximum_minor="${2:-0}" - maximum_patch="${3:-0}" - IFS="$old_ifs" + IFS=. read -r version_major version_minor version_patch < Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 104/153] refactor: make proxy module dependencies explicit Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index c843f854..1bc773b8 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -6,23 +6,25 @@ script_dir="$(cd -- "$(dirname -- "$0")" && pwd)" proxy_lib_dir="${PROXY_LIB_DIR:-$script_dir/proxy}" release_marker=/tmp/librecode-proxy-lease-released -# The modules are loaded from a runtime path in the coordinator container. -# shellcheck disable=SC1090,SC1091 +# shellcheck source=proxy/common.sh . "$proxy_lib_dir/common.sh" -# shellcheck disable=SC1090,SC1091 + +coordinator_container="$(hostname)" +project="$(container_project "$coordinator_container")" + +# These modules share the context initialized above. Keep the source directives +# in sync with the runtime paths so ShellCheck can analyze the complete graph. +# shellcheck source=proxy/infrastructure.sh . "$proxy_lib_dir/infrastructure.sh" -# shellcheck disable=SC1090,SC1091 +# shellcheck source=proxy/assets.sh . "$proxy_lib_dir/assets.sh" -# shellcheck disable=SC1090,SC1091 +# shellcheck source=proxy/diagnostics.sh . "$proxy_lib_dir/diagnostics.sh" -# shellcheck disable=SC1090,SC1091 +# shellcheck source=proxy/services.sh . "$proxy_lib_dir/services.sh" -# shellcheck disable=SC1090,SC1091 +# shellcheck source=proxy/lease.sh . "$proxy_lib_dir/lease.sh" -coordinator_container="$(hostname)" -project="$(container_project "$coordinator_container")" - validate_environment() { if [ -z "$project" ]; then echo 'Could not determine the Compose project from the coordinator container.' >&2 From a175def62688d97cb157071b5d5600362f3b11a6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 105/153] refactor: replace shared proxy globals with accessors Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/common.sh | 42 ++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/.docker/scripts/proxy/common.sh b/.docker/scripts/proxy/common.sh index 59415123..4870282c 100644 --- a/.docker/scripts/proxy/common.sh +++ b/.docker/scripts/proxy/common.sh @@ -1,14 +1,28 @@ #!/bin/sh -# This module defines and consumes globals shared by the sourced proxy modules. -# shellcheck disable=SC2034,SC2154 +proxy_project_name() { + printf '%s\n' "${PROXY_PROJECT:-librecode-dev-proxy}" +} + +proxy_network_name() { + printf '%s\n' "${PROXY_NETWORK:-librecode-dev-proxy}" +} + +proxy_container_label() { + printf '%s\n' "${PROXY_LABEL:-coop.librecode.dev-proxy=true}" +} -proxy_project=${PROXY_PROJECT:-librecode-dev-proxy} -proxy_network=${PROXY_NETWORK:-librecode-dev-proxy} -proxy_label=${PROXY_LABEL:-coop.librecode.dev-proxy=true} -proxy_client_label=${PROXY_CLIENT_LABEL:-coop.librecode.dev-proxy-client=true} -proxy_assets_volume=${PROXY_ASSETS_VOLUME:-librecode-dev-proxy-assets} -proxy_vhost_volume=${PROXY_VHOST_VOLUME:-librecode-dev-proxy-vhost} +proxy_client_label() { + printf '%s\n' "${PROXY_CLIENT_LABEL:-coop.librecode.dev-proxy-client=true}" +} + +proxy_assets_volume_name() { + printf '%s\n' "${PROXY_ASSETS_VOLUME:-librecode-dev-proxy-assets}" +} + +proxy_vhost_volume_name() { + printf '%s\n' "${PROXY_VHOST_VOLUME:-librecode-dev-proxy-vhost}" +} Docker() { docker "$@" @@ -36,16 +50,16 @@ container_networks() { compose() { Docker compose \ - --project-name "$project" \ - --project-directory "$PROJECT_DIR" \ - --file "${PROJECT_COMPOSE_FILE:-$PROJECT_DIR/docker-compose.yml}" \ + --project-name "${PROJECT_NAME:-}" \ + --project-directory "${PROJECT_DIR:-}" \ + --file "${PROJECT_COMPOSE_FILE:-${PROJECT_DIR:-}/docker-compose.yml}" \ "$@" } proxy_compose() { Docker compose \ - --project-name "$proxy_project" \ - --project-directory "$PROJECT_DIR" \ - --file "${PROXY_COMPOSE_FILE:-$PROJECT_DIR/.docker/docker-compose.proxy.yml}" \ + --project-name "$(proxy_project_name)" \ + --project-directory "${PROJECT_DIR:-}" \ + --file "${PROXY_COMPOSE_FILE:-${PROJECT_DIR:-}/.docker/docker-compose.proxy.yml}" \ "$@" } From 68b8416df16e2ac1890477fec1953bcaedd71b84 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 106/153] refactor: export coordinator context for proxy modules Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 1bc773b8..4d0dfd51 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -10,10 +10,12 @@ release_marker=/tmp/librecode-proxy-lease-released . "$proxy_lib_dir/common.sh" coordinator_container="$(hostname)" -project="$(container_project "$coordinator_container")" +PROJECT_NAME="$(container_project "$coordinator_container")" +export PROJECT_NAME -# These modules share the context initialized above. Keep the source directives -# in sync with the runtime paths so ShellCheck can analyze the complete graph. +# These modules share only exported environment and common.sh accessors. +# Keep the source directives in sync with the runtime paths so ShellCheck can +# analyze the complete dependency graph without file-wide suppressions. # shellcheck source=proxy/infrastructure.sh . "$proxy_lib_dir/infrastructure.sh" # shellcheck source=proxy/assets.sh @@ -26,7 +28,7 @@ project="$(container_project "$coordinator_container")" . "$proxy_lib_dir/lease.sh" validate_environment() { - if [ -z "$project" ]; then + if [ -z "$PROJECT_NAME" ]; then echo 'Could not determine the Compose project from the coordinator container.' >&2 return 1 fi @@ -36,7 +38,7 @@ validate_environment() { return 1 fi - echo "Validating Compose project ${project} at ${PROJECT_DIR}." + echo "Validating Compose project ${PROJECT_NAME} at ${PROJECT_DIR}." compose config --quiet } From 2700db8c5634d5998ffd40b13c070826ff4de510 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 107/153] refactor: remove shared globals from proxy infrastructure Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/infrastructure.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.docker/scripts/proxy/infrastructure.sh b/.docker/scripts/proxy/infrastructure.sh index 0e3fe71d..b71aa085 100644 --- a/.docker/scripts/proxy/infrastructure.sh +++ b/.docker/scripts/proxy/infrastructure.sh @@ -1,8 +1,5 @@ #!/bin/sh -# Globals are provided by common.sh before this module is sourced. -# shellcheck disable=SC2154 - container_for_published_port() { port="$1" @@ -17,7 +14,7 @@ port_is_in_use() { compatible_proxy_container() { Docker ps \ - --filter "label=$proxy_label" \ + --filter "label=$(proxy_container_label)" \ --format '{{.ID}}' | head -n 1 } @@ -77,16 +74,18 @@ ensure_ports_available() { } ensure_proxy_network() { - if Docker network inspect "$proxy_network" >/dev/null 2>&1; then + network="$(proxy_network_name)" + + if Docker network inspect "$network" >/dev/null 2>&1; then return 0 fi - if Docker network create "$proxy_network" >/dev/null 2>&1; then + if Docker network create "$network" >/dev/null 2>&1; then return 0 fi # Another checkout may have created it concurrently. - Docker network inspect "$proxy_network" >/dev/null + Docker network inspect "$network" >/dev/null } start_proxy() { From cfd3bd35050fb18cdfaeb38304791ef648b2d67b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 108/153] refactor: remove shared globals from proxy services Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/services.sh | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/.docker/scripts/proxy/services.sh b/.docker/scripts/proxy/services.sh index e219f7e2..43d76a0b 100644 --- a/.docker/scripts/proxy/services.sh +++ b/.docker/scripts/proxy/services.sh @@ -1,8 +1,5 @@ #!/bin/sh -# Globals are provided by common.sh before this module is sourced. -# shellcheck disable=SC2154 - service_is_running() { compose ps --status running --services | grep -qx "$1" @@ -15,14 +12,15 @@ container_for_service() { connect_to_proxy_network() { service="$1" container="$(container_for_service "$service")" + network="$(proxy_network_name)" [ -n "$container" ] || return 0 - if container_networks "$container" | grep -q "\"$proxy_network\""; then + if container_networks "$container" | grep -q "\"$network\""; then return 0 fi - Docker network connect "$proxy_network" "$container" + Docker network connect "$network" "$container" } connect_running_service_to_proxy_network() { @@ -40,25 +38,25 @@ connect_project_services() { report_environment_ready() { set -- \ - -e ENV_NEXTCLOUD_URL="https://${project}.localhost" \ - -e ENV_ADMIN_USER="$NEXTCLOUD_ADMIN_USER" \ - -e ENV_ADMIN_PASSWORD="$NEXTCLOUD_ADMIN_PASSWORD" \ - -e ENV_NEXTCLOUD_BRANCH="$VERSION_NEXTCLOUD" + -e ENV_NEXTCLOUD_URL="https://${PROJECT_NAME:-}.localhost" \ + -e ENV_ADMIN_USER="${NEXTCLOUD_ADMIN_USER:-admin}" \ + -e ENV_ADMIN_PASSWORD="${NEXTCLOUD_ADMIN_PASSWORD:-admin}" \ + -e ENV_NEXTCLOUD_BRANCH="${VERSION_NEXTCLOUD:-master}" if service_is_running mailpit; then - set -- "$@" -e ENV_MAILPIT_URL="https://${project}-mailpit.localhost" + set -- "$@" -e ENV_MAILPIT_URL="https://${PROJECT_NAME:-}-mailpit.localhost" fi if service_is_running eurooffice; then - set -- "$@" -e ENV_EUROOFFICE_URL="https://${project}-eurooffice.localhost" + set -- "$@" -e ENV_EUROOFFICE_URL="https://${PROJECT_NAME:-}-eurooffice.localhost" fi if service_is_running playwright; then - set -- "$@" -e ENV_PLAYWRIGHT_URL="https://${project}-playwright.localhost" + set -- "$@" -e ENV_PLAYWRIGHT_URL="https://${PROJECT_NAME:-}-playwright.localhost" fi if service_is_running signal-gateway; then - set -- "$@" -e ENV_SIGNAL_URL="https://${project}-signal.localhost" + set -- "$@" -e ENV_SIGNAL_URL="https://${PROJECT_NAME:-}-signal.localhost" fi compose exec -T \ From 6d33c6564959684f2ad74da289a798d518b58818 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 109/153] refactor: remove shared globals from proxy assets Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index 6442d3fc..7309b1ea 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -1,9 +1,5 @@ #!/bin/sh -# Globals are provided by common.sh before this module is sourced. -# The single-quoted shell snippets are intentionally evaluated in helper containers. -# shellcheck disable=SC2154,SC2016 - proxy_helper_image="${PROXY_HELPER_IMAGE:-docker:29.8.0-cli}" copy_to_named_volume() { @@ -19,24 +15,27 @@ copy_to_named_volume() { } install_proxy_assets() { - Docker volume create "$proxy_assets_volume" >/dev/null - Docker volume create "$proxy_vhost_volume" >/dev/null + assets_volume="$(proxy_assets_volume_name)" + vhost_volume="$(proxy_vhost_volume_name)" + + Docker volume create "$assets_volume" >/dev/null + Docker volume create "$vhost_volume" >/dev/null Docker run --rm \ - -v "$proxy_assets_volume:/target" \ + -v "$assets_volume:/target" \ "$proxy_helper_image" \ sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' - copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/Procfile" Procfile - copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/docker-gen.cfg" docker-gen.cfg - copy_to_named_volume "$proxy_assets_volume" "$PROJECT_DIR/.docker/nginx-proxy/dashboard.tmpl" dashboard.tmpl + copy_to_named_volume "$assets_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/Procfile" Procfile + copy_to_named_volume "$assets_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/docker-gen.cfg" docker-gen.cfg + copy_to_named_volume "$assets_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/dashboard.tmpl" dashboard.tmpl Docker run --rm \ - -v "$proxy_vhost_volume:/target" \ + -v "$vhost_volume:/target" \ "$proxy_helper_image" \ sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' - copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/localhost_location_override" localhost_location_override - copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/*.localhost" '*.localhost' - copy_to_named_volume "$proxy_vhost_volume" "$PROJECT_DIR/.docker/nginx-proxy/*.localhost_location_override" '*.localhost_location_override' + copy_to_named_volume "$vhost_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/localhost_location_override" localhost_location_override + copy_to_named_volume "$vhost_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/*.localhost" '*.localhost' + copy_to_named_volume "$vhost_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/*.localhost_location_override" '*.localhost_location_override' } From f7ca20041d79bc6418ef5367903048f19057b823 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 110/153] refactor: avoid nested shell expansion for proxy assets Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index 7309b1ea..43259cfd 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -10,8 +10,8 @@ copy_to_named_volume() { Docker run --rm -i \ -v "$volume:/target" \ "$proxy_helper_image" \ - sh -c 'cat > "/target/$1"' sh "$destination" \ - < "$source" + tee "/target/$destination" \ + < "$source" >/dev/null } install_proxy_assets() { From 53d8fff68ae9260591d904e1c916452b15ac4c2f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 111/153] refactor: remove shared globals from proxy lease Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/lease.sh | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.docker/scripts/proxy/lease.sh b/.docker/scripts/proxy/lease.sh index a8b590d3..b8358a4a 100644 --- a/.docker/scripts/proxy/lease.sh +++ b/.docker/scripts/proxy/lease.sh @@ -1,41 +1,47 @@ #!/bin/sh -# Globals are provided by common.sh and proxy-coordinator.sh before use. -# shellcheck disable=SC2154 - acquire_proxy_lease() { - if ! container_networks "$coordinator_container" | grep -q "\"$proxy_network\""; then - Docker network connect "$proxy_network" "$coordinator_container" + network="$(proxy_network_name)" + + if ! container_networks "${COORDINATOR_CONTAINER:-}" | grep -q "\"$network\""; then + Docker network connect "$network" "${COORDINATOR_CONTAINER:-}" fi } release_proxy_lease() { - if ! container_networks "$coordinator_container" | grep -q "\"$proxy_network\""; then + network="$(proxy_network_name)" + + if ! container_networks "${COORDINATOR_CONTAINER:-}" | grep -q "\"$network\""; then return 0 fi - if ! Docker network disconnect "$proxy_network" "$coordinator_container" >/dev/null 2>&1; then + if ! Docker network disconnect "$network" "${COORDINATOR_CONTAINER:-}" >/dev/null 2>&1; then echo 'Could not disconnect this coordinator lease from the shared proxy network; continuing with project-based lease detection.' >&2 fi } other_proxy_client_is_running() { + network="$(proxy_network_name)" + for container in $(Docker ps \ - --filter "label=$proxy_client_label" \ - --filter "network=$proxy_network" \ + --filter "label=$(proxy_client_label)" \ + --filter "network=$network" \ --format '{{.ID}}'); do - [ "$(container_project "$container")" = "$project" ] || return 0 + [ "$(container_project "$container")" = "${PROJECT_NAME:-}" ] || return 0 done return 1 } other_proxy_route_is_running() { - for container in $(Docker ps --filter "network=$proxy_network" --format '{{.ID}}'); do + network="$(proxy_network_name)" + proxy_project="$(proxy_project_name)" + + for container in $(Docker ps --filter "network=$network" --format '{{.ID}}'); do container_project_name="$(container_project "$container")" case "$container_project_name" in - "$project"|"$proxy_project") + "${PROJECT_NAME:-}"|"$proxy_project") continue ;; esac From f78ce8dc2d36b73606832a9bffa3d6164d9fa1f2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 112/153] refactor: export coordinator container context Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 4d0dfd51..5a5c061c 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -9,9 +9,9 @@ release_marker=/tmp/librecode-proxy-lease-released # shellcheck source=proxy/common.sh . "$proxy_lib_dir/common.sh" -coordinator_container="$(hostname)" -PROJECT_NAME="$(container_project "$coordinator_container")" -export PROJECT_NAME +COORDINATOR_CONTAINER="$(hostname)" +PROJECT_NAME="$(container_project "$COORDINATOR_CONTAINER")" +export COORDINATOR_CONTAINER PROJECT_NAME # These modules share only exported environment and common.sh accessors. # Keep the source directives in sync with the runtime paths so ShellCheck can From 3e89ec86b271ff684760205306ba2f29c2f67063 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 113/153] test: use explicit proxy lease context Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/lease.bats | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/proxy/lease.bats b/tests/proxy/lease.bats index dd56c542..8efc6f51 100644 --- a/tests/proxy/lease.bats +++ b/tests/proxy/lease.bats @@ -10,8 +10,8 @@ setup() { # shellcheck source=.docker/scripts/proxy/lease.sh source "$REPO_ROOT/.docker/scripts/proxy/lease.sh" - project=current - coordinator_container=current-coordinator + PROJECT_NAME=current + COORDINATOR_CONTAINER=current-coordinator PROXY_LEASE_GRACE_SECONDS=0 } @@ -50,7 +50,7 @@ setup() { } container_project() { case "$1" in - proxy) printf '%s\n' "$proxy_project" ;; + proxy) proxy_project_name ;; current-route) printf '%s\n' current ;; other-route) printf '%s\n' other ;; esac @@ -67,7 +67,7 @@ setup() { @test "last lease stops the shared proxy with bounded timeout" { PROXY_STOP_TIMEOUT_SECONDS=7 container_networks() { - printf '{"%s":{}}\n' "$proxy_network" + printf '{"%s":{}}\n' "$(proxy_network_name)" } proxy_is_used_by_another_environment() { return 1 @@ -82,13 +82,13 @@ setup() { run release_proxy_if_unused [ "$status" -eq 0 ] - grep -q "^docker network disconnect $proxy_network $coordinator_container$" "$TEST_LOG" + grep -q "^docker network disconnect $(proxy_network_name) $COORDINATOR_CONTAINER$" "$TEST_LOG" grep -q '^proxy-compose down --timeout 7 --remove-orphans$' "$TEST_LOG" } @test "another lease prevents proxy shutdown" { container_networks() { - printf '{"%s":{}}\n' "$proxy_network" + printf '{"%s":{}}\n' "$(proxy_network_name)" } proxy_is_used_by_another_environment() { return 0 @@ -103,7 +103,7 @@ setup() { run release_proxy_if_unused [ "$status" -eq 0 ] - grep -q "^docker network disconnect $proxy_network $coordinator_container$" "$TEST_LOG" + grep -q "^docker network disconnect $(proxy_network_name) $COORDINATOR_CONTAINER$" "$TEST_LOG" ! grep -q '^proxy-compose down' "$TEST_LOG" } @@ -131,5 +131,5 @@ setup() { acquire_proxy_lease - grep -q "^docker network connect $proxy_network $coordinator_container$" "$TEST_LOG" + grep -q "^docker network connect $(proxy_network_name) $COORDINATOR_CONTAINER$" "$TEST_LOG" } From 5cd9fa5e88d2421b1364e8b23e71f21d6d39d97b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 114/153] test: use proxy network accessor in service tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/services.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/proxy/services.bats b/tests/proxy/services.bats index 4fcf639c..74f5655f 100644 --- a/tests/proxy/services.bats +++ b/tests/proxy/services.bats @@ -10,7 +10,7 @@ setup() { # shellcheck source=.docker/scripts/proxy/services.sh source "$REPO_ROOT/.docker/scripts/proxy/services.sh" - project=current + PROJECT_NAME=current } @test "running service is connected to proxy network" { @@ -23,13 +23,13 @@ setup() { connect_running_service_to_proxy_network nginx - grep -q "^docker network connect $proxy_network service-container$" "$TEST_LOG" + grep -q "^docker network connect $(proxy_network_name) service-container$" "$TEST_LOG" } @test "service already on proxy network is not connected twice" { service_is_running() { return 0; } container_for_service() { printf '%s\n' service-container; } - container_networks() { printf '{\"%s\":{}}\n' "$proxy_network"; } + container_networks() { printf '{\"%s\":{}}\n' "$(proxy_network_name)"; } Docker() { printf 'docker %s\n' "$*" >> "$TEST_LOG" } From 12d3addcd0c6dffa89077baf59ad62f159bce95f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:12 -0300 Subject: [PATCH 115/153] test: use proxy network accessor in infrastructure tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/infrastructure.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/proxy/infrastructure.bats b/tests/proxy/infrastructure.bats index 0890e9d7..65aed021 100644 --- a/tests/proxy/infrastructure.bats +++ b/tests/proxy/infrastructure.bats @@ -56,7 +56,7 @@ setup() { Docker() { printf 'docker %s\n' "$*" >> "$TEST_LOG" case "$*" in - "network inspect $proxy_network") return 0 ;; + "network inspect $(proxy_network_name)") return 0 ;; esac return 1 } @@ -64,6 +64,6 @@ setup() { run ensure_proxy_network [ "$status" -eq 0 ] - grep -q "^docker network inspect $proxy_network$" "$TEST_LOG" + grep -q "^docker network inspect $(proxy_network_name)$" "$TEST_LOG" ! grep -q '^docker network create' "$TEST_LOG" } From 161bd7f9ef6b139ff2c8fcd788e917559d942a1c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 116/153] ci: analyze shell modules together Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/shellcheck.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 247f2184..c985f7e9 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -16,3 +16,4 @@ jobs: uses: ludeeus/action-shellcheck@master with: scandir: .docker/ + check_together: 'true' From b6025cccf8bd990b642f5da6fead4a2ca5e2a212 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 117/153] ci: make shell source paths explicit Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy-coordinator.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.docker/scripts/proxy-coordinator.sh b/.docker/scripts/proxy-coordinator.sh index 5a5c061c..dad5f7cf 100755 --- a/.docker/scripts/proxy-coordinator.sh +++ b/.docker/scripts/proxy-coordinator.sh @@ -6,7 +6,7 @@ script_dir="$(cd -- "$(dirname -- "$0")" && pwd)" proxy_lib_dir="${PROXY_LIB_DIR:-$script_dir/proxy}" release_marker=/tmp/librecode-proxy-lease-released -# shellcheck source=proxy/common.sh +# shellcheck source=.docker/scripts/proxy/common.sh . "$proxy_lib_dir/common.sh" COORDINATOR_CONTAINER="$(hostname)" @@ -16,15 +16,15 @@ export COORDINATOR_CONTAINER PROJECT_NAME # These modules share only exported environment and common.sh accessors. # Keep the source directives in sync with the runtime paths so ShellCheck can # analyze the complete dependency graph without file-wide suppressions. -# shellcheck source=proxy/infrastructure.sh +# shellcheck source=.docker/scripts/proxy/infrastructure.sh . "$proxy_lib_dir/infrastructure.sh" -# shellcheck source=proxy/assets.sh +# shellcheck source=.docker/scripts/proxy/assets.sh . "$proxy_lib_dir/assets.sh" -# shellcheck source=proxy/diagnostics.sh +# shellcheck source=.docker/scripts/proxy/diagnostics.sh . "$proxy_lib_dir/diagnostics.sh" -# shellcheck source=proxy/services.sh +# shellcheck source=.docker/scripts/proxy/services.sh . "$proxy_lib_dir/services.sh" -# shellcheck source=proxy/lease.sh +# shellcheck source=.docker/scripts/proxy/lease.sh . "$proxy_lib_dir/lease.sh" validate_environment() { From 2123988a8424550f535f9a0b0c74f68fa9e7bbd8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 118/153] ci: follow sourced shell modules Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/shellcheck.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index c985f7e9..cbf1bfb3 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -14,6 +14,8 @@ jobs: - uses: actions/checkout@v7 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: -x with: scandir: .docker/ check_together: 'true' From ce36dced32a7c72b0ae57f1ee9c2ae20c4ab8264 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 119/153] ci: inspect runtime image digests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/proxy-tests.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index 862628ab..e5cfdf34 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -25,6 +25,24 @@ jobs: - name: Run unit tests run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats + image-digests: + name: Inspect runtime image digests + runs-on: ubuntu-latest + steps: + - name: Resolve multi-platform digests + run: | + for image in \ + docker:29.8.0-cli \ + axllent/mailpit:v1.31.1 \ + redis:8.8.2 \ + nginxproxy/nginx-proxy:1.11.6-alpine \ + sebastienheyd/self-signed-proxy-companion:1.1.0 \ + nginx:1.31.5-alpine + do + echo "IMAGE $image" + docker buildx imagetools inspect "$image" | sed -n '1,5p' + done + integration: name: Proxy Docker integration tests runs-on: ubuntu-latest From 7d54755117350ac4a5091dfe5abbb0afd0cab1f7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 120/153] build: pin default runtime images by digest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 815a9100..5b2609cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,7 @@ services: - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: - image: docker:29.8.0-cli + image: docker:29.8.0-cli@sha256:eccaacfeed644c7de222ff047483568cb988dde95476fbaaf10ea2d04921bb66 entrypoint: ["/bin/sh", "/usr/local/lib/librecode/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" @@ -82,13 +82,13 @@ services: depends_on: - nextcloud mailpit: - image: axllent/mailpit:v1.31.1 + image: axllent/mailpit:v1.31.1@sha256:98b916bd3c8d61f7633a52d3ea2f58d00620cb01ca57ab59edde68c347a95365 environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost - VIRTUAL_PORT=8025 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}-mailpit.localhost redis: - image: redis:8.8.2 + image: redis:8.8.2@sha256:37227fff5638322f4ebea25d6d0dc3ee50848604e82b81426f11507b3ec7d2cc eurooffice: image: ghcr.io/euro-office/documentserver:latest profiles: From a6f6f5d4a316ae630c81cfc3cdea8707aa07e295 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 121/153] build: pin shared proxy images by digest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/docker-compose.proxy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/docker-compose.proxy.yml b/.docker/docker-compose.proxy.yml index bfa873ac..d229a9b3 100644 --- a/.docker/docker-compose.proxy.yml +++ b/.docker/docker-compose.proxy.yml @@ -1,6 +1,6 @@ services: nginx-proxy: - image: nginxproxy/nginx-proxy:1.11.6-alpine + image: nginxproxy/nginx-proxy:1.11.6-alpine@sha256:99376e95c8754547213d411222a2c4dca046d974e753ae2a1c9aef09b2c03364 container_name: librecode-dev-proxy labels: coop.librecode.dev-proxy: "true" @@ -24,7 +24,7 @@ services: - proxy ssl-companion: - image: sebastienheyd/self-signed-proxy-companion:1.1.0 + image: sebastienheyd/self-signed-proxy-companion:1.1.0@sha256:dd698341f804aaffe2974931a185aa088561d76094e040e374847f3a64004d43 container_name: librecode-dev-proxy-ssl-companion labels: coop.librecode.dev-proxy-companion: "true" From fbcc795e834b47697522e82005c24680f09c4ea2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 122/153] test: pin proxy fixture images by digest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/fixtures/compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/proxy/fixtures/compose.yml b/tests/proxy/fixtures/compose.yml index 76e65d32..d1304ea8 100644 --- a/tests/proxy/fixtures/compose.yml +++ b/tests/proxy/fixtures/compose.yml @@ -1,13 +1,13 @@ services: nginx: - image: nginx:1.31.5-alpine + image: nginx:1.31.5-alpine@sha256:72ba65eb42c10344912a84ff42408db7d34f2feb642204570ab8fc5ffd29f1d3 environment: - VIRTUAL_HOST=${COMPOSE_PROJECT_NAME}.localhost - VIRTUAL_PORT=80 - SELF_SIGNED_HOST=${COMPOSE_PROJECT_NAME}.localhost proxy-coordinator: - image: docker:29.8.0-cli + image: docker:29.8.0-cli@sha256:eccaacfeed644c7de222ff047483568cb988dde95476fbaaf10ea2d04921bb66 entrypoint: ["/bin/sh", "/project/.docker/scripts/proxy-coordinator.sh"] labels: coop.librecode.dev-proxy-client: "true" From 34524f48bbc737f44887d6761e9eba4dff68e351 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 123/153] build: pin proxy helper image by digest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index 43259cfd..b7fc04a7 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -1,6 +1,6 @@ #!/bin/sh -proxy_helper_image="${PROXY_HELPER_IMAGE:-docker:29.8.0-cli}" +proxy_helper_image="${PROXY_HELPER_IMAGE:-docker:29.8.0-cli@sha256:eccaacfeed644c7de222ff047483568cb988dde95476fbaaf10ea2d04921bb66}" copy_to_named_volume() { volume="$1" From 83f18e2b3e1b1320e7ae245090cc4a0d7f9a32ba Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 124/153] build: pin database images by digest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/database-services.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/database-services.yml b/.docker/database-services.yml index 91701323..46691b04 100644 --- a/.docker/database-services.yml +++ b/.docker/database-services.yml @@ -2,7 +2,7 @@ services: mysql: # Pin to 8.4: mysql:latest currently resolves to 9.x, and Nextcloud autoinstall # runs with the application user, which cannot read mysql.component there. - image: mysql:8.4 + image: mysql:8.4@sha256:b3b90af2a6552ae30c266fdb7d5dd55f3afb72404bb78d37fe8a23eb857fd3fb volumes: - ../volumes/mysql/init:/docker-entrypoint-initdb.d - ../volumes/mysql/data:/var/lib/mysql @@ -18,7 +18,7 @@ services: - MYSQL_USER=${MYSQL_USER:-nextcloud} - MYSQL_PASSWORD=${MYSQL_PASSWORD:-nextcloud} pgsql: - image: postgres:13-alpine + image: postgres:13-alpine@sha256:fb9065b6e3e213bdc07edd372a5b2a26245840b7fb65d1fd8b6700106d51805c volumes: - ../volumes/postgres/data:/var/lib/postgresql/data ports: From 8c5a17f61792c2dbb56eeb44a80e2fd72ed9d393 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 125/153] test: require digest-pinned runtime images Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/compose-policy.bats | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/proxy/compose-policy.bats b/tests/proxy/compose-policy.bats index 5859bddd..dbc352ff 100644 --- a/tests/proxy/compose-policy.bats +++ b/tests/proxy/compose-policy.bats @@ -4,17 +4,20 @@ setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" } -@test "default third-party runtime images use concrete versions" { - grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli$' "$REPO_ROOT/docker-compose.yml" - grep -Eq '^[[:space:]]+image: axllent/mailpit:v[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/docker-compose.yml" - grep -Eq '^[[:space:]]+image: redis:[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/docker-compose.yml" - grep -Eq '^[[:space:]]+image: nginxproxy/nginx-proxy:[0-9]+\.[0-9]+\.[0-9]+-alpine$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" - grep -Eq '^[[:space:]]+image: sebastienheyd/self-signed-proxy-companion:[0-9]+\.[0-9]+\.[0-9]+$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" +@test "default third-party runtime images are pinned by tag and digest" { + grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli@sha256:[0-9a-f]{64}$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: axllent/mailpit:v[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: redis:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$REPO_ROOT/docker-compose.yml" + grep -Eq '^[[:space:]]+image: mysql:8\.4@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/database-services.yml" + grep -Eq '^[[:space:]]+image: postgres:13-alpine@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/database-services.yml" + grep -Eq '^[[:space:]]+image: nginxproxy/nginx-proxy:[0-9]+\.[0-9]+\.[0-9]+-alpine@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" + grep -Eq '^[[:space:]]+image: sebastienheyd/self-signed-proxy-companion:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" + grep -Eq '^proxy_helper_image="\$\{PROXY_HELPER_IMAGE:-docker:[0-9]+\.[0-9]+\.[0-9]+-cli@sha256:[0-9a-f]{64}\}"$' "$REPO_ROOT/.docker/scripts/proxy/assets.sh" } -@test "proxy integration fixture uses concrete image versions" { - grep -Eq '^[[:space:]]+image: nginx:[0-9]+\.[0-9]+\.[0-9]+-alpine$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" - grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" +@test "proxy integration fixture images are pinned by tag and digest" { + grep -Eq '^[[:space:]]+image: nginx:[0-9]+\.[0-9]+\.[0-9]+-alpine@sha256:[0-9a-f]{64}$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" + grep -Eq '^[[:space:]]+image: docker:[0-9]+\.[0-9]+\.[0-9]+-cli@sha256:[0-9a-f]{64}$' "$REPO_ROOT/tests/proxy/fixtures/compose.yml" } @test "compose files do not keep shutdown workarounds from runtime debugging" { From 9d59c576e26f4fd3ec334c2be3d520acea7526e8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 126/153] ci: remove temporary image digest inspection Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/proxy-tests.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/proxy-tests.yml b/.github/workflows/proxy-tests.yml index e5cfdf34..862628ab 100644 --- a/.github/workflows/proxy-tests.yml +++ b/.github/workflows/proxy-tests.yml @@ -25,24 +25,6 @@ jobs: - name: Run unit tests run: bats tests/proxy/lease.bats tests/proxy/infrastructure.bats tests/proxy/services.bats tests/proxy/diagnostics.bats tests/proxy/compose-policy.bats - image-digests: - name: Inspect runtime image digests - runs-on: ubuntu-latest - steps: - - name: Resolve multi-platform digests - run: | - for image in \ - docker:29.8.0-cli \ - axllent/mailpit:v1.31.1 \ - redis:8.8.2 \ - nginxproxy/nginx-proxy:1.11.6-alpine \ - sebastienheyd/self-signed-proxy-companion:1.1.0 \ - nginx:1.31.5-alpine - do - echo "IMAGE $image" - docker buildx imagetools inspect "$image" | sed -n '1,5p' - done - integration: name: Proxy Docker integration tests runs-on: ubuntu-latest From a190d67d444bb0e2a895d7b3be1740a839a4f1be Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 127/153] refactor: expose container image reference Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/common.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.docker/scripts/proxy/common.sh b/.docker/scripts/proxy/common.sh index 4870282c..9ccc2667 100644 --- a/.docker/scripts/proxy/common.sh +++ b/.docker/scripts/proxy/common.sh @@ -34,6 +34,12 @@ container_project() { "$1" 2>/dev/null || true } +container_image() { + Docker inspect \ + --format '{{ .Config.Image }}' \ + "$1" 2>/dev/null || true +} + container_virtual_host() { Docker inspect \ --format '{{range .Config.Env}}{{println .}}{{end}}' \ From 1efade38d1596121661e57b70e27fe5c0d81d62b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 128/153] refactor: reuse coordinator image for proxy helpers Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/assets.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.docker/scripts/proxy/assets.sh b/.docker/scripts/proxy/assets.sh index b7fc04a7..58991c5a 100644 --- a/.docker/scripts/proxy/assets.sh +++ b/.docker/scripts/proxy/assets.sh @@ -1,15 +1,25 @@ #!/bin/sh -proxy_helper_image="${PROXY_HELPER_IMAGE:-docker:29.8.0-cli@sha256:eccaacfeed644c7de222ff047483568cb988dde95476fbaaf10ea2d04921bb66}" +proxy_helper_image() { + if [ -n "${PROXY_HELPER_IMAGE:-}" ]; then + printf '%s\n' "$PROXY_HELPER_IMAGE" + return 0 + fi + + container_image "${COORDINATOR_CONTAINER:-}" +} copy_to_named_volume() { volume="$1" source="$2" destination="$3" + helper_image="$(proxy_helper_image)" + + [ -n "$helper_image" ] || return 1 Docker run --rm -i \ -v "$volume:/target" \ - "$proxy_helper_image" \ + "$helper_image" \ tee "/target/$destination" \ < "$source" >/dev/null } @@ -17,13 +27,16 @@ copy_to_named_volume() { install_proxy_assets() { assets_volume="$(proxy_assets_volume_name)" vhost_volume="$(proxy_vhost_volume_name)" + helper_image="$(proxy_helper_image)" + + [ -n "$helper_image" ] || return 1 Docker volume create "$assets_volume" >/dev/null Docker volume create "$vhost_volume" >/dev/null Docker run --rm \ -v "$assets_volume:/target" \ - "$proxy_helper_image" \ + "$helper_image" \ sh -c 'rm -f /target/Procfile /target/docker-gen.cfg /target/dashboard.tmpl' copy_to_named_volume "$assets_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/Procfile" Procfile @@ -32,7 +45,7 @@ install_proxy_assets() { Docker run --rm \ -v "$vhost_volume:/target" \ - "$proxy_helper_image" \ + "$helper_image" \ sh -c 'rm -f /target/librecode-localhost.conf /target/localhost /target/localhost_location_override /target/\*.localhost /target/\*.localhost_location_override' copy_to_named_volume "$vhost_volume" "${PROJECT_DIR:-}/.docker/nginx-proxy/localhost_location_override" localhost_location_override From 91ff3dd275fd75398c8a78c348df0de2a5316120 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 129/153] test: keep proxy helper image single sourced Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/compose-policy.bats | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/proxy/compose-policy.bats b/tests/proxy/compose-policy.bats index dbc352ff..9f8060aa 100644 --- a/tests/proxy/compose-policy.bats +++ b/tests/proxy/compose-policy.bats @@ -12,7 +12,11 @@ setup() { grep -Eq '^[[:space:]]+image: postgres:13-alpine@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/database-services.yml" grep -Eq '^[[:space:]]+image: nginxproxy/nginx-proxy:[0-9]+\.[0-9]+\.[0-9]+-alpine@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" grep -Eq '^[[:space:]]+image: sebastienheyd/self-signed-proxy-companion:[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$REPO_ROOT/.docker/docker-compose.proxy.yml" - grep -Eq '^proxy_helper_image="\$\{PROXY_HELPER_IMAGE:-docker:[0-9]+\.[0-9]+\.[0-9]+-cli@sha256:[0-9a-f]{64}\}"$' "$REPO_ROOT/.docker/scripts/proxy/assets.sh" +} + +@test "proxy helper reuses the coordinator image instead of duplicating its version" { + grep -q 'container_image "${COORDINATOR_CONTAINER:-}"' "$REPO_ROOT/.docker/scripts/proxy/assets.sh" + ! grep -Eq 'docker:[0-9]+\.[0-9]+\.[0-9]+-cli@sha256:' "$REPO_ROOT/.docker/scripts/proxy/assets.sh" } @test "proxy integration fixture images are pinned by tag and digest" { From d541a8616a8a56d472c705d95fd90742a2e5afef Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 130/153] feat: report long Compose project names Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/scripts/proxy/diagnostics.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.docker/scripts/proxy/diagnostics.sh b/.docker/scripts/proxy/diagnostics.sh index 5d011051..aaf79ea0 100644 --- a/.docker/scripts/proxy/diagnostics.sh +++ b/.docker/scripts/proxy/diagnostics.sh @@ -49,9 +49,18 @@ runtime_has_known_shutdown_risk() { version_at_most "$runc_version" "${PROXY_KNOWN_BAD_RUNC_MAX:-1.1.12}" } +project_name_needs_shortening_hint() { + project_name="$1" + [ -n "$project_name" ] || return 1 + + [ "$project_name" = "${PROXY_REPOSITORY_PROJECT_NAME:-nextcloud-docker-development}" ] && return 0 + [ "${#project_name}" -gt "${PROXY_PROJECT_NAME_HINT_LENGTH:-20}" ] +} + runtime_diagnostics_json() { docker_version="$(runtime_version docker)" runc_version="$(runtime_version runc)" + project_name="${PROJECT_NAME:-}" if runtime_has_known_shutdown_risk "$docker_version" "$runc_version"; then warnings='[{"code":"outdated-docker-runtime","message":"This Docker and runc combination may fail to stop containers correctly on recent Linux/AppArmor hosts. Update Docker Engine before investigating shutdown problems."}]' @@ -59,8 +68,14 @@ runtime_diagnostics_json() { warnings='[]' fi - printf '{"docker":"%s","runc":"%s","warnings":%s}\n' \ - "$docker_version" "$runc_version" "$warnings" + if project_name_needs_shortening_hint "$project_name"; then + hints="[{\"code\":\"long-project-name\",\"projectName\":\"$project_name\",\"exampleHost\":\"$project_name-playwright.localhost\"}]" + else + hints='[]' + fi + + printf '{"docker":"%s","runc":"%s","project":{"name":"%s"},"warnings":%s,"hints":%s}\n' \ + "$docker_version" "$runc_version" "$project_name" "$warnings" "$hints" } install_runtime_diagnostics() { From 6405003b805626b4882d7b986a2f5d4b1db978da Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 131/153] test: cover Compose project name hints Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/diagnostics.bats | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/proxy/diagnostics.bats b/tests/proxy/diagnostics.bats index 634910cb..0caefdfb 100644 --- a/tests/proxy/diagnostics.bats +++ b/tests/proxy/diagnostics.bats @@ -27,6 +27,24 @@ setup() { [ "$status" -eq 1 ] } +@test "repository project name suggests a shorter Compose project name" { + run project_name_needs_shortening_hint nextcloud-docker-development + + [ "$status" -eq 0 ] +} + +@test "long Compose project name suggests a shorter name" { + run project_name_needs_shortening_hint this-is-a-very-long-project-name + + [ "$status" -eq 0 ] +} + +@test "short Compose project name does not suggest a shorter name" { + run project_name_needs_shortening_hint dev + + [ "$status" -eq 1 ] +} + @test "runtime diagnostics include host versions without warning for current runtime" { runtime_version() { case "$1" in @@ -34,13 +52,16 @@ setup() { runc) printf '%s\n' 1.5.1 ;; esac } + PROJECT_NAME=dev run runtime_diagnostics_json [ "$status" -eq 0 ] [[ "$output" == *'"docker":"29.8.0"'* ]] [[ "$output" == *'"runc":"1.5.1"'* ]] + [[ "$output" == *'"project":{"name":"dev"}'* ]] [[ "$output" == *'"warnings":[]'* ]] + [[ "$output" == *'"hints":[]'* ]] } @test "runtime diagnostics warn for known old runtime combination" { @@ -50,6 +71,7 @@ setup() { runc) printf '%s\n' 1.1.12 ;; esac } + PROJECT_NAME=dev run runtime_diagnostics_json @@ -57,3 +79,20 @@ setup() { [[ "$output" == *'"code":"outdated-docker-runtime"'* ]] [[ "$output" == *'Update Docker Engine'* ]] } + +@test "runtime diagnostics include project-name hint with example host" { + runtime_version() { + case "$1" in + docker) printf '%s\n' 29.8.0 ;; + runc) printf '%s\n' 1.5.1 ;; + esac + } + PROJECT_NAME=nextcloud-docker-development + + run runtime_diagnostics_json + + [ "$status" -eq 0 ] + [[ "$output" == *'"code":"long-project-name"'* ]] + [[ "$output" == *'"projectName":"nextcloud-docker-development"'* ]] + [[ "$output" == *'"exampleHost":"nextcloud-docker-development-playwright.localhost"'* ]] +} From 32dd07a2f4536623c5ddac32a36df58752d74454 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 132/153] feat: show Compose project name hints Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 42 +++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl index 699214e6..d1196a73 100644 --- a/.docker/nginx-proxy/dashboard.tmpl +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -46,9 +46,12 @@ li { margin: .45rem 0; } a { color: LinkText; } code { padding: .15em .4em; border-radius: 4px; background: color-mix(in srgb, CanvasText 10%, transparent); } - .not-found, .runtime-warning { padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 24%, transparent); border-radius: 8px; } - .runtime-warning { margin-top: 1rem; border-color: #c47f00; } - .runtime-warning strong { display: block; margin-bottom: .4rem; } + .not-found, .runtime-warning, .runtime-hint { padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 24%, transparent); border-radius: 8px; } + .runtime-warning, .runtime-hint { margin-top: 1rem; } + .runtime-warning { border-color: #c47f00; } + .runtime-hint { border-color: color-mix(in srgb, LinkText 55%, Canvas 45%); } + .runtime-warning strong, .runtime-hint strong { display: block; margin-bottom: .4rem; } + .runtime-hint p { margin: .45rem 0 0; } .runtime-list { list-style: none; padding: 0; } .runtime-list li { margin: .35rem 0; } .muted { opacity: .7; } @@ -186,6 +189,39 @@ panel.append(title, message); checks.appendChild(panel); } + + for (const hint of runtime.hints || []) { + if (hint.code !== 'long-project-name') continue; + + const panel = document.createElement('div'); + panel.className = 'runtime-hint'; + + const title = document.createElement('strong'); + title.textContent = 'β„Ή Shorter local URLs are available'; + + const explanation = document.createElement('p'); + explanation.append( + 'This environment uses the Compose project name ', + Object.assign(document.createElement('code'), { textContent: hint.projectName }), + ', which makes local URLs longer.' + ); + + const example = document.createElement('p'); + example.append( + 'For example: ', + Object.assign(document.createElement('code'), { textContent: `https://${hint.exampleHost}/` }) + ); + + const suggestion = document.createElement('p'); + suggestion.append( + 'You can keep the current directory and use a shorter project name, for example ', + Object.assign(document.createElement('code'), { textContent: 'COMPOSE_PROJECT_NAME=dev docker compose up' }), + '.' + ); + + panel.append(title, explanation, example, suggestion); + checks.appendChild(panel); + } }) .catch(() => { const checks = document.getElementById('runtime-checks'); From e21ef44dbbf3e2eeb4f6d1a16be04d5ea08abf4d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 133/153] feat: add contributor links to proxy dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl index d1196a73..a787431b 100644 --- a/.docker/nginx-proxy/dashboard.tmpl +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -54,6 +54,9 @@ .runtime-hint p { margin: .45rem 0 0; } .runtime-list { list-style: none; padding: 0; } .runtime-list li { margin: .35rem 0; } + .project-links { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid color-mix(in srgb, CanvasText 16%, transparent); font-size: .95rem; } + .project-links p { margin: .45rem 0; } + .project-links nav { display: flex; flex-wrap: wrap; gap: .4rem 1rem; } .muted { opacity: .7; } @@ -76,6 +79,16 @@

This page is generated from the routes currently connected to the shared development proxy.

+ + From 32fe6b3d70df642cbd77bae343199d8cd26facd5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:13 -0300 Subject: [PATCH 134/153] test: verify contributor links on proxy dashboard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/proxy/integration.bats | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/proxy/integration.bats b/tests/proxy/integration.bats index d9e4eb72..df15e33c 100644 --- a/tests/proxy/integration.bats +++ b/tests/proxy/integration.bats @@ -84,6 +84,10 @@ wait_for_https_status() { wait_for_https_status localhost 200 grep -q 'LibreCode Development Proxy' "$BODY" grep -q 'Environment checks' "$BODY" + grep -q 'Help improve this development environment' "$BODY" + grep -q 'Contribute on GitHub' "$BODY" + grep -q 'Report an issue' "$BODY" + grep -q 'Star on GitHub' "$BODY" wait_for_https_path_status localhost /runtime.json 200 grep -q '"docker":"' "$BODY" From cda5594cc9884181b1afaf9720fae623b08d72fb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:24:14 -0300 Subject: [PATCH 135/153] refactor: present dashboard as development environment Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/nginx-proxy/dashboard.tmpl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.docker/nginx-proxy/dashboard.tmpl b/.docker/nginx-proxy/dashboard.tmpl index a787431b..6c8837e9 100644 --- a/.docker/nginx-proxy/dashboard.tmpl +++ b/.docker/nginx-proxy/dashboard.tmpl @@ -34,7 +34,7 @@ - LibreCode Development Proxy + LibreCode Nextcloud Development Environment