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

View File

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

View File

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

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>
<body>
<div id="root"></div>
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

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

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 {
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(

View File

@@ -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 = [
{