From e64babb94c7a46041f67a97164e74d51e5e52e07 Mon Sep 17 00:00:00 2001 From: "Marcelo M. Maciel" <4993482+marcelo-maciel@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:41:39 -0300 Subject: [PATCH 1/3] fix(frontend): keep the config template outside the web root so a restart can't crash-loop The entrypoints deleted config.json.template after rendering it, but the container's writable layer survives docker restart, daemon restarts and reboots, so the next start found no template and set -e exited 1 forever. Keeping the template in /etc/fsh means it is never served and every start re-renders config.json idempotently. --- clients/admin/Dockerfile | 5 +++-- clients/admin/docker/docker-entrypoint.sh | 8 +++----- clients/dashboard/Dockerfile | 2 +- clients/dashboard/docker/docker-entrypoint.sh | 3 +-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/clients/admin/Dockerfile b/clients/admin/Dockerfile index ff97828574..4f7bc42ca9 100644 --- a/clients/admin/Dockerfile +++ b/clients/admin/Dockerfile @@ -23,9 +23,10 @@ RUN apk add --no-cache gettext RUN rm -rf ./* /etc/nginx/conf.d/default.conf COPY docker/nginx.conf /etc/nginx/conf.d/default.conf -# Copy the built bundle, the runtime config template, and the entrypoint +# Copy the built bundle, the runtime config template (outside the web root so +# it is never served), and the entrypoint COPY --from=build /app/dist/ ./ -COPY docker/config.json.template ./config.json.template +COPY docker/config.json.template /etc/fsh/config.json.template COPY docker/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh diff --git a/clients/admin/docker/docker-entrypoint.sh b/clients/admin/docker/docker-entrypoint.sh index 3908a3444a..8e6bce4f3a 100644 --- a/clients/admin/docker/docker-entrypoint.sh +++ b/clients/admin/docker/docker-entrypoint.sh @@ -10,10 +10,8 @@ set -e export FSH_API_URL FSH_DASHBOARD_URL FSH_DEFAULT_TENANT -# Render the runtime config from the template, writing into nginx's web root. -envsubst < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json - -# Drop the template so it isn't served accidentally. -rm /usr/share/nginx/html/config.json.template +# Render the runtime config into nginx's web root on every start. The template +# lives outside the web root, so it is never served and survives a restart. +envsubst < /etc/fsh/config.json.template > /usr/share/nginx/html/config.json exec nginx -g 'daemon off;' diff --git a/clients/dashboard/Dockerfile b/clients/dashboard/Dockerfile index 97eb73c4bd..bf452b8b0d 100644 --- a/clients/dashboard/Dockerfile +++ b/clients/dashboard/Dockerfile @@ -13,7 +13,7 @@ RUN apk add --no-cache gettext RUN rm -rf ./* /etc/nginx/conf.d/default.conf COPY docker/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist/ ./ -COPY docker/config.json.template ./config.json.template +COPY docker/config.json.template /etc/fsh/config.json.template COPY docker/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh EXPOSE 80 diff --git a/clients/dashboard/docker/docker-entrypoint.sh b/clients/dashboard/docker/docker-entrypoint.sh index 0c19ef4e24..372542e99f 100644 --- a/clients/dashboard/docker/docker-entrypoint.sh +++ b/clients/dashboard/docker/docker-entrypoint.sh @@ -6,8 +6,7 @@ set -e export FSH_API_URL FSH_DEFAULT_TENANT -envsubst < /usr/share/nginx/html/config.json.template \ +envsubst < /etc/fsh/config.json.template \ > /usr/share/nginx/html/config.json -rm /usr/share/nginx/html/config.json.template exec nginx -g 'daemon off;' From fcf18d30acadf8971deb6fd5248ad1dcab596ed2 Mon Sep 17 00:00:00 2001 From: "Marcelo M. Maciel" <4993482+marcelo-maciel@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:13:17 -0300 Subject: [PATCH 2/3] fix(frontend): force LF for the files copied into the front-end images config.json.template and nginx.conf fell under text=auto, so an image built from a Windows checkout served config.json with CRLF line endings. --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 2b15509289..b80eff143c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,6 @@ # Dockerfiles Dockerfile text eol=lf *.dockerfile text eol=lf + +# Front-end image files are copied verbatim into a Linux image and served from it +clients/*/docker/** text eol=lf From 804ab79e5fb408fb64ccba5762497538bcd14343 Mon Sep 17 00:00:00 2001 From: "Marcelo M. Maciel" <4993482+marcelo-maciel@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:13:18 -0300 Subject: [PATCH 3/3] ci(frontend): build both images and smoke-test a container restart Nothing built or ran the nginx images, which is how an entrypoint that deleted its own template shipped: the first start worked and every restart crash-looped. The new job starts each image, checks the rendered config.json, confirms the template is not served, restarts, and checks again. --- .github/workflows/frontend.yml | 50 +++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 13440884e6..09ef7c7668 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -111,10 +111,58 @@ jobs: path: clients/${{ matrix.app }}/playwright-report retention-days: 7 + # The nginx image renders /config.json at every start. Restarting the same container + # has to bring it back: the entrypoint once deleted its own template after the first + # render, so any restart crash-looped, and nothing else here builds or runs the image. + container: + name: Container (${{ matrix.app }}) + needs: changes + if: needs.changes.outputs.frontend == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + app: [admin, dashboard] + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Build image + run: docker build -t fsh-${{ matrix.app }}:ci clients/${{ matrix.app }} + + - name: Smoke test start and restart + env: + IMAGE: fsh-${{ matrix.app }}:ci + run: | + set -euo pipefail + docker run -d --name smoke -p 8080:80 \ + -e FSH_API_URL=http://api.example.test -e FSH_DEFAULT_TENANT=acme \ + -e FSH_DASHBOARD_URL=http://app.example.test "$IMAGE" + check() { + rm -f config.json + for _ in $(seq 1 20); do + curl -fsS http://localhost:8080/config.json -o config.json && break + sleep 1 + done + [ "$(docker inspect -f '{{.State.Running}} {{.RestartCount}}' smoke)" = "true 0" ] + jq -e '.apiBase == "http://api.example.test" and .defaultTenant == "acme"' config.json + } + check + if curl -fsS http://localhost:8080/config.json.template | grep -qF '${FSH_API_URL}'; then + echo "::error::config.json.template is served from the web root" + exit 1 + fi + docker restart smoke + check + + - name: Container logs + if: failure() + run: docker logs smoke + # Single required status check — see backend.yml for the rationale. frontend-ci: name: Frontend CI - needs: [changes, lint-build, e2e] + needs: [changes, lint-build, e2e, container] if: always() runs-on: ubuntu-latest steps: