From 796fa7bb4b68ee74a74907d75a88a760f54dcb89 Mon Sep 17 00:00:00 2001 From: achraf Date: Sun, 7 Jun 2026 16:32:23 +0200 Subject: [PATCH] Load frontend configuration at container startup --- Dockerfile | 4 ++- README.md | 20 +++++++------ docker-compose.yml | 8 ++--- docker/40-generate-runtime-config.sh | 37 +++++++++++++++++++++++ index.html | 1 + nginx.conf | 6 ++++ public/config.js | 1 + src/core/config/runtime-config.ts | 37 +++++++++++++++++++++++ src/core/privacy/privacy-config.ts | 45 ++++++++++++++++++---------- src/pages/ContributePage.tsx | 7 +++-- 10 files changed, 133 insertions(+), 33 deletions(-) create mode 100644 docker/40-generate-runtime-config.sh create mode 100644 public/config.js create mode 100644 src/core/config/runtime-config.ts diff --git a/Dockerfile b/Dockerfile index 67ba335..a3e7c6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,13 @@ WORKDIR /app RUN corepack enable && corepack prepare pnpm@10.33.2 --activate COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN --mount=type=secret,id=vite_env,target=/app/.env,required=true pnpm build +RUN pnpm build FROM nginx:1.27-alpine AS runtime COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY docker/40-generate-runtime-config.sh /docker-entrypoint.d/40-generate-runtime-config.sh COPY --from=build /app/dist /usr/share/nginx/html +RUN chmod +x /docker-entrypoint.d/40-generate-runtime-config.sh EXPOSE 80 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 diff --git a/README.md b/README.md index 938102d..771165a 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ npm run build Build and run the static production app with Nginx: ```bash -cp .env.example .env -# Edit .env with the real deployment and legal values. -docker build --secret id=vite_env,src=.env -t plimi:local . +docker build -t plimi:local . -docker run --rm -p 8080:80 --name plimi plimi:local +docker run --rm -p 8080:80 --name plimi \ + --env-file .env \ + plimi:local ``` Or use Docker Compose: @@ -67,11 +67,13 @@ configured to exclude URL search parameters and hashes and honor Do Not Track. The privacy page is available at `/privacy`; deployers must replace all example controller and hosting values before publishing. -Docker mounts `.env` as a BuildKit secret only while Vite builds the static -bundle. The file is ignored by Git and excluded from the regular Docker context. -Remember that `VITE_*` values are public build-time configuration and can be -read from the resulting browser JavaScript; do not put passwords or API secrets -in them. +The Docker image is environment-independent. At container startup, its entrypoint +generates `/config.js` from `.env`, so the same image can be promoted between +environments without rebuilding. The file is ignored by Git and excluded from +the Docker build context. + +These values are public browser configuration even though they are supplied at +deploy time. Do not put passwords, private keys, or API secrets in them. --- diff --git a/docker-compose.yml b/docker-compose.yml index 85b47e2..45656b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,14 +2,10 @@ services: plimi: build: context: . - secrets: - - vite_env image: plimi:local container_name: plimi + env_file: + - .env ports: - "${PLIMI_PORT:-8080}:80" restart: unless-stopped - -secrets: - vite_env: - file: ./.env diff --git a/docker/40-generate-runtime-config.sh b/docker/40-generate-runtime-config.sh new file mode 100644 index 0000000..a5fa8a7 --- /dev/null +++ b/docker/40-generate-runtime-config.sh @@ -0,0 +1,37 @@ +#!/bin/sh +set -eu + +config_path="${PLIMI_CONFIG_PATH:-/usr/share/nginx/html/config.js}" + +json_string() { + value="${1:-}" + escaped="$(printf '%s' "$value" | sed \ + -e 's/\\/\\\\/g' \ + -e 's/"/\\"/g' \ + -e 's/\r/\\r/g')" + printf '"%s"' "$escaped" +} + +write_entry() { + key="$1" + default_value="$2" + eval "value=\${$key:-\$default_value}" + printf ' %s: ' "$key" >> "$config_path" + json_string "$value" >> "$config_path" + printf ',\n' >> "$config_path" +} + +printf 'window.__PLIMI_CONFIG__ = {\n' > "$config_path" +write_entry VITE_PLIMI_REPO_URL "https://github.com/your-org/plimi" +write_entry VITE_SITE_URL "" +write_entry VITE_LEGAL_NAME "Achraf ACHKARI-BEGDOURI" +write_entry VITE_LEGAL_COUNTRY "France" +write_entry VITE_PRIVACY_EMAIL "privacy@example.com" +write_entry VITE_PRIVACY_EFFECTIVE_DATE "2026-06-07" +write_entry VITE_ANALYTICS_PROVIDER_NAME "Umami" +write_entry VITE_ANALYTICS_SCRIPT_URL "https://u.achraf.app/script.js" +write_entry VITE_ANALYTICS_WEBSITE_ID "89d0f8d2-5b59-438c-b1f6-ab2655e97f6e" +write_entry VITE_ANALYTICS_HOST "u.achraf.app" +write_entry VITE_ANALYTICS_HOST_COUNTRY "France" +write_entry VITE_ANALYTICS_RETENTION_MONTHS "13" +printf '};\n' >> "$config_path" diff --git a/index.html b/index.html index 711b815..0094499 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@
+ diff --git a/nginx.conf b/nginx.conf index 046c0ae..c666eda 100644 --- a/nginx.conf +++ b/nginx.conf @@ -33,6 +33,12 @@ server { add_header Cache-Control "public"; } + location = /config.js { + try_files $uri =404; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + expires -1; + } + location / { try_files $uri $uri/ /index.html; } diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..262e83d --- /dev/null +++ b/public/config.js @@ -0,0 +1 @@ +window.__PLIMI_CONFIG__ = {}; diff --git a/src/core/config/runtime-config.ts b/src/core/config/runtime-config.ts new file mode 100644 index 0000000..01a7b4b --- /dev/null +++ b/src/core/config/runtime-config.ts @@ -0,0 +1,37 @@ +export interface PlimiRuntimeConfig { + VITE_PLIMI_REPO_URL?: string; + VITE_SITE_URL?: string; + VITE_LEGAL_NAME?: string; + VITE_LEGAL_COUNTRY?: string; + VITE_PRIVACY_EMAIL?: string; + VITE_PRIVACY_EFFECTIVE_DATE?: string; + VITE_ANALYTICS_PROVIDER_NAME?: string; + VITE_ANALYTICS_SCRIPT_URL?: string; + VITE_ANALYTICS_WEBSITE_ID?: string; + VITE_ANALYTICS_HOST?: string; + VITE_ANALYTICS_HOST_COUNTRY?: string; + VITE_ANALYTICS_RETENTION_MONTHS?: string; +} + +declare global { + interface Window { + __PLIMI_CONFIG__?: PlimiRuntimeConfig; + } +} + +export function runtimeConfigValue( + name: keyof PlimiRuntimeConfig, + fallback: string +): string { + const runtimeValue = window.__PLIMI_CONFIG__?.[name]; + if (typeof runtimeValue === "string" && runtimeValue.trim()) { + return runtimeValue.trim(); + } + + const buildValue = import.meta.env[name]; + if (typeof buildValue === "string" && buildValue.trim()) { + return buildValue.trim(); + } + + return fallback; +} diff --git a/src/core/privacy/privacy-config.ts b/src/core/privacy/privacy-config.ts index eee5c11..264aa21 100644 --- a/src/core/privacy/privacy-config.ts +++ b/src/core/privacy/privacy-config.ts @@ -1,30 +1,45 @@ -function env(name: keyof ImportMetaEnv, fallback: string): string { - const value = import.meta.env[name]; - return typeof value === "string" && value.trim() ? value.trim() : fallback; -} +import { runtimeConfigValue } from "../config/runtime-config"; export const privacyConfig = { siteName: "Plimi", - siteUrl: env("VITE_SITE_URL", window.location.origin), - controllerName: env("VITE_LEGAL_NAME", "Plimi website operator"), - privacyEmail: env("VITE_PRIVACY_EMAIL", "privacy@example.com"), - controllerCountry: env("VITE_LEGAL_COUNTRY", "Not configured"), - policyEffectiveDate: env("VITE_PRIVACY_EFFECTIVE_DATE", "2026-06-07"), - analyticsProvider: env("VITE_ANALYTICS_PROVIDER_NAME", "Umami"), - analyticsScriptUrl: env( + siteUrl: runtimeConfigValue("VITE_SITE_URL", window.location.origin), + controllerName: runtimeConfigValue( + "VITE_LEGAL_NAME", + "Plimi website operator" + ), + privacyEmail: runtimeConfigValue( + "VITE_PRIVACY_EMAIL", + "privacy@example.com" + ), + controllerCountry: runtimeConfigValue( + "VITE_LEGAL_COUNTRY", + "Not configured" + ), + policyEffectiveDate: runtimeConfigValue( + "VITE_PRIVACY_EFFECTIVE_DATE", + "2026-06-07" + ), + analyticsProvider: runtimeConfigValue( + "VITE_ANALYTICS_PROVIDER_NAME", + "Umami" + ), + analyticsScriptUrl: runtimeConfigValue( "VITE_ANALYTICS_SCRIPT_URL", "https://u.achraf.app/script.js" ), - analyticsWebsiteId: env( + analyticsWebsiteId: runtimeConfigValue( "VITE_ANALYTICS_WEBSITE_ID", "89d0f8d2-5b59-438c-b1f6-ab2655e97f6e" ), - analyticsHost: env("VITE_ANALYTICS_HOST", "u.achraf.app"), - analyticsHostCountry: env( + analyticsHost: runtimeConfigValue("VITE_ANALYTICS_HOST", "u.achraf.app"), + analyticsHostCountry: runtimeConfigValue( "VITE_ANALYTICS_HOST_COUNTRY", "Not configured" ), - analyticsRetentionMonths: env("VITE_ANALYTICS_RETENTION_MONTHS", "13"), + analyticsRetentionMonths: runtimeConfigValue( + "VITE_ANALYTICS_RETENTION_MONTHS", + "13" + ), }; export const hasAnalyticsConfiguration = Boolean( diff --git a/src/pages/ContributePage.tsx b/src/pages/ContributePage.tsx index 4c209f3..0b539e1 100644 --- a/src/pages/ContributePage.tsx +++ b/src/pages/ContributePage.tsx @@ -1,8 +1,11 @@ import { Link } from "react-router-dom"; import type { ReactNode } from "react"; +import { runtimeConfigValue } from "../core/config/runtime-config"; -const repoUrl = - import.meta.env.VITE_PLIMI_REPO_URL || "https://github.com/your-org/plimi"; +const repoUrl = runtimeConfigValue( + "VITE_PLIMI_REPO_URL", + "https://github.com/your-org/plimi" +); const steps = [ {