Load frontend configuration at container startup

This commit is contained in:
achraf
2026-06-07 16:32:23 +02:00
parent 0b2d38ee5e
commit 796fa7bb4b
10 changed files with 133 additions and 33 deletions

View File

@@ -11,11 +11,13 @@ WORKDIR /app
RUN corepack enable && corepack prepare pnpm@10.33.2 --activate RUN corepack enable && corepack prepare pnpm@10.33.2 --activate
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . 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 FROM nginx:1.27-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf 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 COPY --from=build /app/dist /usr/share/nginx/html
RUN chmod +x /docker-entrypoint.d/40-generate-runtime-config.sh
EXPOSE 80 EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1

View File

@@ -36,11 +36,11 @@ npm run build
Build and run the static production app with Nginx: Build and run the static production app with Nginx:
```bash ```bash
cp .env.example .env docker build -t plimi:local .
# Edit .env with the real deployment and legal values.
docker build --secret id=vite_env,src=.env -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: 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 The privacy page is available at `/privacy`; deployers must replace all example
controller and hosting values before publishing. controller and hosting values before publishing.
Docker mounts `.env` as a BuildKit secret only while Vite builds the static The Docker image is environment-independent. At container startup, its entrypoint
bundle. The file is ignored by Git and excluded from the regular Docker context. generates `/config.js` from `.env`, so the same image can be promoted between
Remember that `VITE_*` values are public build-time configuration and can be environments without rebuilding. The file is ignored by Git and excluded from
read from the resulting browser JavaScript; do not put passwords or API secrets the Docker build context.
in them.
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.
--- ---

View File

@@ -2,14 +2,10 @@ services:
plimi: plimi:
build: build:
context: . context: .
secrets:
- vite_env
image: plimi:local image: plimi:local
container_name: plimi container_name: plimi
env_file:
- .env
ports: ports:
- "${PLIMI_PORT:-8080}:80" - "${PLIMI_PORT:-8080}:80"
restart: unless-stopped restart: unless-stopped
secrets:
vite_env:
file: ./.env

View File

@@ -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"

View File

@@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -33,6 +33,12 @@ server {
add_header Cache-Control "public"; 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 / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }

1
public/config.js Normal file
View File

@@ -0,0 +1 @@
window.__PLIMI_CONFIG__ = {};

View File

@@ -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;
}

View File

@@ -1,30 +1,45 @@
function env(name: keyof ImportMetaEnv, fallback: string): string { import { runtimeConfigValue } from "../config/runtime-config";
const value = import.meta.env[name];
return typeof value === "string" && value.trim() ? value.trim() : fallback;
}
export const privacyConfig = { export const privacyConfig = {
siteName: "Plimi", siteName: "Plimi",
siteUrl: env("VITE_SITE_URL", window.location.origin), siteUrl: runtimeConfigValue("VITE_SITE_URL", window.location.origin),
controllerName: env("VITE_LEGAL_NAME", "Plimi website operator"), controllerName: runtimeConfigValue(
privacyEmail: env("VITE_PRIVACY_EMAIL", "privacy@example.com"), "VITE_LEGAL_NAME",
controllerCountry: env("VITE_LEGAL_COUNTRY", "Not configured"), "Plimi website operator"
policyEffectiveDate: env("VITE_PRIVACY_EFFECTIVE_DATE", "2026-06-07"), ),
analyticsProvider: env("VITE_ANALYTICS_PROVIDER_NAME", "Umami"), privacyEmail: runtimeConfigValue(
analyticsScriptUrl: env( "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", "VITE_ANALYTICS_SCRIPT_URL",
"https://u.achraf.app/script.js" "https://u.achraf.app/script.js"
), ),
analyticsWebsiteId: env( analyticsWebsiteId: runtimeConfigValue(
"VITE_ANALYTICS_WEBSITE_ID", "VITE_ANALYTICS_WEBSITE_ID",
"89d0f8d2-5b59-438c-b1f6-ab2655e97f6e" "89d0f8d2-5b59-438c-b1f6-ab2655e97f6e"
), ),
analyticsHost: env("VITE_ANALYTICS_HOST", "u.achraf.app"), analyticsHost: runtimeConfigValue("VITE_ANALYTICS_HOST", "u.achraf.app"),
analyticsHostCountry: env( analyticsHostCountry: runtimeConfigValue(
"VITE_ANALYTICS_HOST_COUNTRY", "VITE_ANALYTICS_HOST_COUNTRY",
"Not configured" "Not configured"
), ),
analyticsRetentionMonths: env("VITE_ANALYTICS_RETENTION_MONTHS", "13"), analyticsRetentionMonths: runtimeConfigValue(
"VITE_ANALYTICS_RETENTION_MONTHS",
"13"
),
}; };
export const hasAnalyticsConfiguration = Boolean( export const hasAnalyticsConfiguration = Boolean(

View File

@@ -1,8 +1,11 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { runtimeConfigValue } from "../core/config/runtime-config";
const repoUrl = const repoUrl = runtimeConfigValue(
import.meta.env.VITE_PLIMI_REPO_URL || "https://github.com/your-org/plimi"; "VITE_PLIMI_REPO_URL",
"https://github.com/your-org/plimi"
);
const steps = [ const steps = [
{ {