diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0aafc7a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+FROM --platform=$BUILDPLATFORM node:lts-alpine3.23 AS builder
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm install
+COPY . .
+RUN npm run build
+
+FROM --platform=$TARGETPLATFORM nginx:stable-alpine AS production
+
+RUN apk add --no-cache gettext
+
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/nowchess-frontend/browser /usr/share/nginx/html
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+COPY public/env.template.js /usr/share/nginx/html/env.template.js
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+RUN chmod +x /docker-entrypoint.sh
+
+EXPOSE 80
+ENTRYPOINT ["/docker-entrypoint.sh"]
diff --git a/angular.json b/angular.json
index aafdf64..0077510 100644
--- a/angular.json
+++ b/angular.json
@@ -58,6 +58,14 @@
],
"outputHashing": "all"
},
+ "staging": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.staging.ts"
+ }
+ ]
+ },
"development": {
"fileReplacements": [
{
@@ -83,6 +91,9 @@
"production": {
"buildTarget": "nowchess-frontend:build:production"
},
+ "staging": {
+ "buildTarget": "nowchess-frontend:build:staging"
+ },
"development": {
"buildTarget": "nowchess-frontend:build:development"
}
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 0000000..dab211f
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -e
+
+# Replace placeholders in env.template.js with environment variables and write env.js
+TEMPLATE_PATH="/usr/share/nginx/html/env.template.js"
+TARGET_PATH="/usr/share/nginx/html/env.js"
+
+if [ -f "$TEMPLATE_PATH" ]; then
+ echo "Rendering runtime config from $TEMPLATE_PATH"
+ echo "Using environment variables:"
+ printenv
+ echo "----"
+ envsubst < "$TEMPLATE_PATH" > "$TARGET_PATH"
+else
+ echo "No runtime template found at $TEMPLATE_PATH, skipping"
+fi
+
+exec nginx -g 'daemon off;'
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..ff4e939
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,22 @@
+server {
+ listen 80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /env.js {
+ root /usr/share/nginx/html;
+ default_type application/javascript;
+ expires -1;
+ add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
diff --git a/package.json b/package.json
index 0882715..37a84ae 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
- "build": "ng build",
+ "build": "ng build --configuration production",
+ "build:staging": "ng build --configuration staging",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
diff --git a/public/env.template.js b/public/env.template.js
new file mode 100644
index 0000000..72477ac
--- /dev/null
+++ b/public/env.template.js
@@ -0,0 +1,4 @@
+window.__RUNTIME_CONFIG__ = {
+ API_URL: "${API_URL}",
+ WEBSOCKET_URL: "${WEBSOCKET_URL}"
+};
diff --git a/src/app/core/config.loader.ts b/src/app/core/config.loader.ts
new file mode 100644
index 0000000..2d10fd5
--- /dev/null
+++ b/src/app/core/config.loader.ts
@@ -0,0 +1,11 @@
+/**
+ * Load runtime configuration from window.__RUNTIME_CONFIG__
+ * This is injected by docker-entrypoint.sh at container startup
+ */
+export function loadRuntimeConfig() {
+ const config = (window as any).__RUNTIME_CONFIG__ || {};
+ return {
+ apiUrl: config.API_URL || '',
+ wsUrl: config.WEBSOCKET_URL || 'ws://localhost:8080'
+ };
+}
diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts
new file mode 100644
index 0000000..a2f73b6
--- /dev/null
+++ b/src/environments/environment.staging.ts
@@ -0,0 +1,11 @@
+import { loadRuntimeConfig } from '../app/core/config.loader';
+
+const runtimeConfig = loadRuntimeConfig();
+
+export const environment = {
+ production: true,
+ apiBaseUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de',
+ accountServiceUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de',
+ wsBaseUrl: runtimeConfig.wsUrl || 'wss://st.nowchess.janis-eccarius.de',
+ apiPath: '/api/board/game'
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index 8c38733..2478e38 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -1,7 +1,11 @@
+import { loadRuntimeConfig } from '../app/core/config.loader';
+
+const runtimeConfig = loadRuntimeConfig();
+
export const environment = {
- production: true,
- apiBaseUrl: '',
- accountServiceUrl: '',
- wsBaseUrl: 'ws://localhost:8080',
+ production: false,
+ apiBaseUrl: runtimeConfig.apiUrl || '',
+ accountServiceUrl: runtimeConfig.apiUrl || '',
+ wsBaseUrl: runtimeConfig.wsUrl,
apiPath: '/api/board/game'
};
diff --git a/src/index.html b/src/index.html
index 5f83c38..1d5f35e 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,6 +6,7 @@