fix(deploy): add optional Turnstile and customer mTLS proxy

This commit is contained in:
root committed 2026-09-04 14:17:18 +00:00
1 parent e06a43c76f
commit 35068319e5
7 files changed
+186 -28

No files matched your search

+10 -1
View File
@@ -13,7 +13,16 @@ Generated keys are written only to `/opt/overcooked/production.env` with mode
600. The installer does not log generated values, activation codes, or Auth 600. The installer does not log generated values, activation codes, or Auth
configuration. Failure logs are redacted before display. configuration. Failure logs are redacted before display.
Turnstile is optional. Empty site and secret keys disable only that abuse
prevention challenge; Auth, license validation, and login policy remain on.
E production control-plane traffic uses mTLS. The installer creates a local
CA and service/client certificates, then generates a minimal HTTPS proxy in the
installation directory and runs it from the approved S WebUI image. The proxy
has no public port and only forwards authenticated `/internal/v1/` calls to
S. The approved Media Node image is exported to the node-image archive used by
S node deployment.
The installer must stop on missing prerequisites, invalid public address, The installer must stop on missing prerequisites, invalid public address,
occupied ports, failed image verification, failed schema migration, or failed occupied ports, failed image verification, failed schema migration, or failed
health checks. It must never continue with a partially initialized database. health checks. It must never continue with a partially initialized database.
+19 -5
View File
@@ -12,6 +12,13 @@ only these public images:
It does not download or start an Auth image, and it does not use Docker build It does not download or start an Auth image, and it does not use Docker build
contexts. contexts.
Because E production mode requires an encrypted control-plane connection, the
installer generates a minimal allowlisted HTTPS reverse proxy at install time
and runs it from the already-listed S WebUI image. The proxy only accepts
mTLS-authenticated `/internal/v1/` requests and `/health/live`, and forwards
only to the S control service; it is not a public endpoint and does not expose
Shell, SQL, Docker, or arbitrary file access.
## Install ## Install
```sh ```sh
@@ -23,8 +30,6 @@ sudo env \\
OVERCOOKED_SERVER_IP=203.0.113.10 \\ OVERCOOKED_SERVER_IP=203.0.113.10 \\
OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \\ OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \\
OVERCOOKED_ACTIVATION_CODE='ACTIVATION_CODE' \\ OVERCOOKED_ACTIVATION_CODE='ACTIVATION_CODE' \\
OVERCOOKED_TURNSTILE_SITE_KEY='TURNSTILE_SITE_KEY' \\
OVERCOOKED_TURNSTILE_SECRET_KEY='TURNSTILE_SECRET_KEY' \\
./install.sh ./install.sh
``` ```
@@ -33,9 +38,10 @@ with `OVERCOOKED_AUTH_URL`. Auth private keys, administrator credentials,
license databases, provider credentials, and production secrets are never license databases, provider credentials, and production secrets are never
included in this repository. included in this repository.
The current S production image requires the Turnstile key pair shown above. Turnstile is optional. Set `OVERCOOKED_TURNSTILE_SITE_KEY` and
The installer fails early if the pair is missing or incomplete; it never uses `OVERCOOKED_TURNSTILE_SECRET_KEY` together to enable it; leaving both empty
placeholder credentials. disables only the Turnstile challenge and does not disable Auth, license, or
login policy checks. A partial pair is rejected by S configuration validation.
## Endpoints ## Endpoints
@@ -48,6 +54,10 @@ The installer checks these host ports before changing service state and does
not bind ports 80 or 443. Configure the firewall to allow the chosen public not bind ports 80 or 443. Configure the firewall to allow the chosen public
ports and restrict database/Valkey traffic to the Docker networks. ports and restrict database/Valkey traffic to the Docker networks.
S's E client uses the private backend Docker network (`http://e-server:8096`)
with its explicit private-network opt-in. E's control-plane client is a
separate production path and always uses the generated HTTPS/mTLS proxy.
The server address is selected from `OVERCOOKED_SERVER_IP`, or from the The server address is selected from `OVERCOOKED_SERVER_IP`, or from the
interface selected by `ip -4 route get 1.1.1.1`. A detected RFC1918, interface selected by `ip -4 route get 1.1.1.1`. A detected RFC1918,
loopback, link-local, or carrier-grade NAT address is rejected unless explicitly loopback, link-local, or carrier-grade NAT address is rejected unless explicitly
@@ -73,6 +83,10 @@ image. S uses only the schema migration assets published in this repository.
If either migration fails, the installer exits non-zero and does not start the If either migration fails, the installer exits non-zero and does not start the
corresponding application service. corresponding application service.
The installer also exports the approved Media Node image into
`/opt/overcooked/node-images/media-node.tar.gz`; S uses that archive when a
customer adds a node without a source checkout.
## Common 404/401 download failures ## Common 404/401 download failures
The customer command must fetch the raw script from the public Installer The customer command must fetch the raw script from the public Installer
+3 -3
View File
@@ -1,6 +1,6 @@
3395c27ad4158e70d47e7d002c141b1261f502875f2e7ac5b26ade51b7689138 install.sh 80d42d9797b20d0b8fd53ac9529cd579fae9fe294267ac8e8ab990ed961e88d1 install.sh
028d415f0c0aef0ed9a807573239079f9e7e8cb3ed796d9dec90f4be8cafdd05 compose/s.yml e8cabc5a989d171206361e6ae8b4e0baf9bb9a611ac41106bbe42de0782e8975 compose/s.yml
fab213abb69e0141139c0d4234a7b1636588206f6588942a55c3380114069d92 compose/e.yml 203b6da918dee21dbfabbcf5466111bd92c7a8c9e754dad61e7e75e20513b880 compose/e.yml
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh 3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz 07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz
+4 -1
View File
@@ -62,6 +62,7 @@ services:
- e-data:/data - e-data:/data
- e-plugin-data:/data/plugins - e-plugin-data:/data/plugins
- ${OVERCOOKED_ROOT:-/opt/overcooked}/config/e-server.production.yaml:/etc/overcooked-e/config.yaml:ro - ${OVERCOOKED_ROOT:-/opt/overcooked}/config/e-server.production.yaml:/etc/overcooked-e/config.yaml:ro
- ${OVERCOOKED_ROOT:-/opt/overcooked}/control-plane:/run/overcooked-control-plane:ro
environment: environment:
PRODUCT_E_CONFIG: /etc/overcooked-e/config.yaml PRODUCT_E_CONFIG: /etc/overcooked-e/config.yaml
PRODUCT_E_ENVIRONMENT: production PRODUCT_E_ENVIRONMENT: production
@@ -72,6 +73,9 @@ services:
PRODUCT_E_CONTROL_PLANE_URL: ${PRODUCT_E_CONTROL_PLANE_URL:?required} PRODUCT_E_CONTROL_PLANE_URL: ${PRODUCT_E_CONTROL_PLANE_URL:?required}
PRODUCT_E_CONTROL_PLANE_KEY_ID: ${PRODUCT_E_CONTROL_PLANE_KEY_ID:?required} PRODUCT_E_CONTROL_PLANE_KEY_ID: ${PRODUCT_E_CONTROL_PLANE_KEY_ID:?required}
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET: ${PRODUCT_E_CONTROL_PLANE_SHARED_SECRET:?required} PRODUCT_E_CONTROL_PLANE_SHARED_SECRET: ${PRODUCT_E_CONTROL_PLANE_SHARED_SECRET:?required}
PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE:?required}
PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE:?required}
PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE: ${PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE:?required}
PRODUCT_E_LICENSE_ENABLED: ${PRODUCT_E_LICENSE_ENABLED:-true} PRODUCT_E_LICENSE_ENABLED: ${PRODUCT_E_LICENSE_ENABLED:-true}
PRODUCT_E_AUTH_URL: ${PRODUCT_E_AUTH_URL:?required} PRODUCT_E_AUTH_URL: ${PRODUCT_E_AUTH_URL:?required}
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP: ${PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP:-false} PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP: ${PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP:-false}
@@ -123,4 +127,3 @@ volumes:
e-data: e-data:
e-plugin-data: e-plugin-data:
e-backup-staging: e-backup-staging:
+33 -3
View File
@@ -102,8 +102,8 @@ services:
S_VERSION: ${S_VERSION:-customer} S_VERSION: ${S_VERSION:-customer}
S_BACKUP_DIRECTORY: /var/lib/overcooked-s/backups S_BACKUP_DIRECTORY: /var/lib/overcooked-s/backups
S_UPDATE_DIRECTORY: /var/lib/overcooked-s/updates S_UPDATE_DIRECTORY: /var/lib/overcooked-s/updates
S_TURNSTILE_SITE_KEY: ${S_TURNSTILE_SITE_KEY:?required} S_TURNSTILE_SITE_KEY: ${S_TURNSTILE_SITE_KEY:-}
S_TURNSTILE_SECRET_KEY: ${S_TURNSTILE_SECRET_KEY:?required} S_TURNSTILE_SECRET_KEY: ${S_TURNSTILE_SECRET_KEY:-}
S_TURNSTILE_HOSTNAME: ${S_TURNSTILE_HOSTNAME:-} S_TURNSTILE_HOSTNAME: ${S_TURNSTILE_HOSTNAME:-}
S_TELEGRAM_MTPROTO_URL: ${S_TELEGRAM_MTPROTO_URL:-http://telegram-disabled.invalid:8098} S_TELEGRAM_MTPROTO_URL: ${S_TELEGRAM_MTPROTO_URL:-http://telegram-disabled.invalid:8098}
S_PORTAL_LOCATION: ${S_PORTAL_LOCATION:-Private Node} S_PORTAL_LOCATION: ${S_PORTAL_LOCATION:-Private Node}
@@ -112,7 +112,7 @@ services:
S_PORTAL_ANNOUNCEMENT: ${S_PORTAL_ANNOUNCEMENT:-} S_PORTAL_ANNOUNCEMENT: ${S_PORTAL_ANNOUNCEMENT:-}
S_STORAGE_WEBHOOK_SECRET: ${S_STORAGE_WEBHOOK_SECRET:-} S_STORAGE_WEBHOOK_SECRET: ${S_STORAGE_WEBHOOK_SECRET:-}
S_NODE_IMAGE_ARCHIVE: /var/lib/overcooked-s/node-images/media-node.tar.gz S_NODE_IMAGE_ARCHIVE: /var/lib/overcooked-s/node-images/media-node.tar.gz
S_NODE_IMAGE_NAME: ${S_NODE_IMAGE_NAME:-overcooked-media-node:latest} S_NODE_IMAGE_NAME: ${S_NODE_IMAGE_NAME:-git.leee.xin/overcooked/overcooked-media-node:latest}
S_NODE_CONTROLLER_HOST: ${S_NODE_CONTROLLER_HOST:-} S_NODE_CONTROLLER_HOST: ${S_NODE_CONTROLLER_HOST:-}
volumes: volumes:
- s-backups:/var/lib/overcooked-s/backups - s-backups:/var/lib/overcooked-s/backups
@@ -133,6 +133,36 @@ services:
logging: *logging logging: *logging
mem_limit: 384m mem_limit: 384m
control-proxy:
image: ${S_WEBUI_IMAGE:?required}
restart: unless-stopped
init: true
depends_on:
s-control: {condition: service_healthy}
command: ["node", "/run/overcooked-control-plane/control-proxy.mjs"]
volumes:
- ${OVERCOOKED_ROOT:-/opt/overcooked}/control-plane:/run/overcooked-control-plane:ro
healthcheck:
test:
- CMD
- node
- -e
- |
const fs = require("node:fs");
const https = require("node:https");
const request = https.get("https://control-proxy:8443/health/live", {ca: fs.readFileSync("/run/overcooked-control-plane/ca.crt"), cert: fs.readFileSync("/run/overcooked-control-plane/client.crt"), key: fs.readFileSync("/run/overcooked-control-plane/client.key"), servername: "control-proxy", rejectUnauthorized: true}, (response) => { response.resume(); process.exit(response.statusCode === 200 ? 0 : 1); });
request.setTimeout(2000, () => { request.destroy(); process.exit(1); });
request.on("error", () => process.exit(1));
interval: 10s
timeout: 3s
start_period: 10s
retries: 6
networks: [control-plane]
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
logging: *logging
mem_limit: 128m
webui: webui:
image: ${S_WEBUI_IMAGE:?required} image: ${S_WEBUI_IMAGE:?required}
restart: unless-stopped restart: unless-stopped
+113 -14
View File
@@ -12,8 +12,6 @@ ACTIVATION_CODE=${OVERCOOKED_ACTIVATION_CODE:-}
AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091} AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}
AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-} AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-}
INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main} INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main}
TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
S_COMPOSE=$ROOT/compose/s.yml S_COMPOSE=$ROOT/compose/s.yml
E_COMPOSE=$ROOT/compose/e.yml E_COMPOSE=$ROOT/compose/e.yml
ENV_FILE=$ROOT/production.env ENV_FILE=$ROOT/production.env
@@ -51,7 +49,8 @@ is_private_ipv4() {
} }
check_port() { check_port() {
port=$1 port=$1
if command -v ss >/dev/null 2>&1 && ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then command -v ss >/dev/null 2>&1 || die "未找到 ss,无法安全检测端口 $port;请安装 iproute2 后重试"
if ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then
die "端口 $port 已被占用,请停止占用进程或调整部署端口" die "端口 $port 已被占用,请停止占用进程或调整部署端口"
fi fi
} }
@@ -116,12 +115,9 @@ if [ -z "$ACTIVATION_CODE" ]; then
printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE
fi fi
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空" [ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
if [ -z "$TURNSTILE_SITE_KEY" ] || [ -z "$TURNSTILE_SECRET_KEY" ]; then
die "当前 S 生产镜像需要 OVERCOOKED_TURNSTILE_SITE_KEY 和 OVERCOOKED_TURNSTILE_SECRET_KEY;请提供成对配置"
fi
check_port 18080; check_port 18083; check_port 8096 check_port 18080; check_port 18083; check_port 8096
install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images" install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images" "$ROOT/control-plane"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
fetch_asset() { fetch_asset() {
relative=$1; destination=$2; source="$SCRIPT_DIR/$relative" relative=$1; destination=$2; source="$SCRIPT_DIR/$relative"
@@ -134,11 +130,100 @@ fetch_asset() {
fetch_asset compose/s.yml "$S_COMPOSE" fetch_asset compose/s.yml "$S_COMPOSE"
fetch_asset compose/e.yml "$E_COMPOSE" fetch_asset compose/e.yml "$E_COMPOSE"
fetch_asset s-migrate.sh "$ROOT/s-migrate.sh" fetch_asset s-migrate.sh "$ROOT/s-migrate.sh"
chmod 644 "$ROOT/s-migrate.sh"
fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml" fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml"
chmod 644 "$ROOT/config/e-server.production.yaml"
fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz" fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz"
tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz" tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz"
find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete
find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 600 {} \; find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 644 {} \;
chmod 755 "$ROOT/migrations" "$ROOT/node-images"
# E requires an HTTPS control-plane origin in production. Generate a local
# CA and service/client certificates for the restricted proxy sidecar. The
# proxy is generated locally and runs from the already-approved S WebUI image;
# no additional image or source checkout is needed.
CONTROL_PLANE_DIR="$ROOT/control-plane"
cat > "$CONTROL_PLANE_DIR/control-proxy.mjs" <<'NODE_PROXY'
import fs from "node:fs";
import http from "node:http";
import https from "node:https";
const listenPort = 8443;
const options = {
key: fs.readFileSync("/run/overcooked-control-plane/server.key"),
cert: fs.readFileSync("/run/overcooked-control-plane/server.crt"),
ca: fs.readFileSync("/run/overcooked-control-plane/ca.crt"),
requestCert: true,
rejectUnauthorized: true,
minVersion: "TLSv1.2",
};
function reject(response, status, message) {
response.writeHead(status, {"content-type": "text/plain; charset=utf-8"});
response.end(message);
}
const server = https.createServer(options, (request, response) => {
let target;
try {
target = new URL(request.url ?? "/", "https://control-proxy");
} catch {
reject(response, 400, "invalid request target");
return;
}
if (target.origin !== "https://control-proxy" || target.username || target.password ||
target.pathname.includes("..") || target.pathname !== "/health/live" && !target.pathname.startsWith("/internal/v1/")) {
reject(response, 404, "not found");
return;
}
if (target.pathname === "/health/live") {
response.writeHead(200, {"content-type": "application/json"});
response.end('{"status":"live"}');
return;
}
const headers = {...request.headers};
delete headers.host;
delete headers.connection;
delete headers["x-forwarded-for"];
delete headers["x-forwarded-host"];
delete headers["x-forwarded-proto"];
headers.host = "s-control:8080";
const upstream = http.request({hostname: "s-control", port: 8080, method: request.method, path: `${target.pathname}${target.search}`, headers, timeout: 10000}, (upstreamResponse) => {
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
upstreamResponse.pipe(response);
});
upstream.on("timeout", () => upstream.destroy(new Error("upstream timeout")));
upstream.on("error", () => {
if (!response.headersSent) reject(response, 502, "control plane unavailable");
else response.destroy();
});
request.pipe(upstream);
});
server.listen(listenPort, "0.0.0.0");
NODE_PROXY
chmod 644 "$CONTROL_PLANE_DIR/control-proxy.mjs"
chown 1000:1000 "$CONTROL_PLANE_DIR/control-proxy.mjs"
openssl genrsa -out "$CONTROL_PLANE_DIR/ca.key" 3072
openssl req -x509 -new -nodes -key "$CONTROL_PLANE_DIR/ca.key" -sha256 -days 3650 -subj "/CN=OverCooked customer control plane CA" -out "$CONTROL_PLANE_DIR/ca.crt"
openssl genrsa -out "$CONTROL_PLANE_DIR/server.key" 2048
openssl req -new -key "$CONTROL_PLANE_DIR/server.key" -subj "/CN=control-proxy" -out "$CONTROL_PLANE_DIR/server.csr"
printf '%s\n' 'subjectAltName=DNS:control-proxy,DNS:product-e' 'extendedKeyUsage=serverAuth' > "$CONTROL_PLANE_DIR/server.ext"
openssl x509 -req -in "$CONTROL_PLANE_DIR/server.csr" -CA "$CONTROL_PLANE_DIR/ca.crt" -CAkey "$CONTROL_PLANE_DIR/ca.key" -CAcreateserial -out "$CONTROL_PLANE_DIR/server.crt" -days 825 -sha256 -extfile "$CONTROL_PLANE_DIR/server.ext"
openssl genrsa -out "$CONTROL_PLANE_DIR/client.key" 2048
openssl req -new -key "$CONTROL_PLANE_DIR/client.key" -subj "/CN=overcooked-e" -out "$CONTROL_PLANE_DIR/client.csr"
printf '%s\n' 'extendedKeyUsage=clientAuth' > "$CONTROL_PLANE_DIR/client.ext"
openssl x509 -req -in "$CONTROL_PLANE_DIR/client.csr" -CA "$CONTROL_PLANE_DIR/ca.crt" -CAkey "$CONTROL_PLANE_DIR/ca.key" -CAcreateserial -out "$CONTROL_PLANE_DIR/client.crt" -days 825 -sha256 -extfile "$CONTROL_PLANE_DIR/client.ext"
rm -f "$CONTROL_PLANE_DIR/ca.key" "$CONTROL_PLANE_DIR/server.csr" "$CONTROL_PLANE_DIR/server.ext" "$CONTROL_PLANE_DIR/client.csr" "$CONTROL_PLANE_DIR/client.ext" "$CONTROL_PLANE_DIR/ca.srl"
chmod 644 "$CONTROL_PLANE_DIR/ca.crt" "$CONTROL_PLANE_DIR/server.crt" "$CONTROL_PLANE_DIR/client.crt"
chmod 640 "$CONTROL_PLANE_DIR/server.key" "$CONTROL_PLANE_DIR/client.key"
chown 1000:1000 "$CONTROL_PLANE_DIR/server.key"
# The proxy healthcheck (uid 1000) and E (uid 10001) both need the client key;
# a shared group keeps it unreadable to other container users.
chown 1000:10001 "$CONTROL_PLANE_DIR/client.key"
chmod 755 "$CONTROL_PLANE_DIR"
secret() { openssl rand -hex 32; } secret() { openssl rand -hex 32; }
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret) S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
@@ -161,6 +246,8 @@ S_E_BASE_URL=http://e-server:8096
S_E_PUBLIC_URL=http://$SERVER_IP:8096 S_E_PUBLIC_URL=http://$SERVER_IP:8096
S_E_KEY_ID=customer-e S_E_KEY_ID=customer-e
S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET
# S's private backend client talks to E on the internal Docker network. This
# does not weaken E's production mTLS control-plane connection below.
S_E_ALLOW_INSECURE_HTTP=true S_E_ALLOW_INSECURE_HTTP=true
S_AUTH_BASE_URL=$AUTH_URL S_AUTH_BASE_URL=$AUTH_URL
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac) S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
@@ -173,16 +260,20 @@ S_PORTAL_SERVER_URL=http://$SERVER_IP:18080
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
S_API_PORT_MAPPING=0.0.0.0:18080:8080 S_API_PORT_MAPPING=0.0.0.0:18080:8080
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000 S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
S_TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY S_TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
S_TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY S_TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
S_NODE_IMAGE_NAME=$REGISTRY/overcooked-media-node:$VERSION
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX
PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable
PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD
PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
PRODUCT_E_CONTROL_PLANE_URL=http://s-control:8080 PRODUCT_E_CONTROL_PLANE_URL=https://control-proxy:8443
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
PRODUCT_E_CONTROL_PLANE_TLS_CERT_FILE=/run/overcooked-control-plane/client.crt
PRODUCT_E_CONTROL_PLANE_TLS_KEY_FILE=/run/overcooked-control-plane/client.key
PRODUCT_E_CONTROL_PLANE_TLS_CA_FILE=/run/overcooked-control-plane/ca.crt
PRODUCT_E_LICENSE_ENABLED=true PRODUCT_E_LICENSE_ENABLED=true
PRODUCT_E_AUTH_URL=$AUTH_URL PRODUCT_E_AUTH_URL=$AUTH_URL
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac) PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
@@ -236,23 +327,31 @@ plugins:
network_allowed_domains: [] network_allowed_domains: []
network_allowed_cidrs: [] network_allowed_cidrs: []
EOF EOF
chmod 600 "$ROOT/config/e-server.production.yaml" chmod 644 "$ROOT/config/e-server.production.yaml"
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$REGISTRY/overcooked-media-node:$VERSION"; do MEDIA_NODE_IMAGE="$REGISTRY/overcooked-media-node:$VERSION"
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$MEDIA_NODE_IMAGE"; do
docker pull "$image" docker pull "$image"
docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image" docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image"
done done
image_tar=$(mktemp "$ROOT/node-images/media-node.tar.XXXXXX")
image_archive=$(mktemp "$ROOT/node-images/media-node.tar.gz.XXXXXX")
docker save "$MEDIA_NODE_IMAGE" -o "$image_tar" || die "无法导出 Media Node 镜像归档"
gzip -c "$image_tar" > "$image_archive" || die "无法压缩 Media Node 镜像归档"
rm -f "$image_tar"
mv "$image_archive" "$ROOT/node-images/media-node.tar.gz"
chmod 644 "$ROOT/node-images/media-node.tar.gz"
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control control-proxy webui
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control webui
wait_for() { wait_for() {
label=$1; url=$2; attempt=1 label=$1; url=$2; attempt=1
+4 -1
View File
@@ -17,7 +17,10 @@ test -f compose/e.yml
test -f config/e-server.production.yaml test -f config/e-server.production.yaml
test -f s-migrations.tar.gz test -f s-migrations.tar.gz
test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0 test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0
sha256sum -c SHA256SUMS >/dev/null
! rg -n '^([[:space:]]*)build:' compose ! rg -n '^([[:space:]]*)build:' compose
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh ! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
! rg -n 'S_TURNSTILE_(SITE|SECRET)_KEY: .*\\?required' compose
! rg -n 'S_INTERNAL_TLS_(ADDR|CERT_FILE|KEY_FILE|CLIENT_CA_FILE)' compose install.sh
rg -n 'control-proxy|PRODUCT_E_CONTROL_PLANE_TLS_' compose install.sh >/dev/null
echo 'static customer installer checks passed' echo 'static customer installer checks passed'