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
|
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
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -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
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 {
|
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(
|
||||||
|
|||||||
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user