Load frontend configuration at container startup
This commit is contained in:
@@ -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
|
||||
|
||||
20
README.md
20
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
37
docker/40-generate-runtime-config.sh
Normal file
37
docker/40-generate-runtime-config.sh
Normal 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"
|
||||
@@ -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>
|
||||
|
||||
@@ -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
1
public/config.js
Normal file
@@ -0,0 +1 @@
|
||||
window.__PLIMI_CONFIG__ = {};
|
||||
37
src/core/config/runtime-config.ts
Normal file
37
src/core/config/runtime-config.ts
Normal 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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user