From c77f5799693d5c0c9b71bb2f75d0b2c86a4c8e45 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Mar 2026 13:15:57 -0800 Subject: [PATCH 1/4] bugfix: change SingleplayerMapAchievementSchema.mapName schema to be a string A player got an achievement on the beta deployment for a map that doesn't exist in prod. Now they can't log in on prod because the map doesn't exist. --- src/core/ApiSchemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index dafce427d..0afad490b 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -43,7 +43,7 @@ export const DiscordUserSchema = z.object({ export type DiscordUser = z.infer; const SingleplayerMapAchievementSchema = z.object({ - mapName: z.enum(GameMapType), + mapName: z.string(), difficulty: z.enum(Difficulty), }); From 40a7b49b8953911c5dd158bdabe21e6f60958f9d Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Mar 2026 13:34:45 -0800 Subject: [PATCH 2/4] Bugfix: Update PlayerGameSchema.map from GameMapType enum to string Same issue as the users/@me response: a user played a map that doesn't exist on production --- src/core/ApiSchemas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 0afad490b..5d0d35fd4 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { base64urlToUuid } from "./Base64"; import { BigIntStringSchema, PlayerStatsSchema } from "./StatsSchemas"; -import { Difficulty, GameMapType, GameMode, GameType } from "./game/Game"; +import { Difficulty, GameMode, GameType } from "./game/Game"; export const RefreshResponseSchema = z.object({ token: z.string(), @@ -99,7 +99,7 @@ export const PlayerGameSchema = z.object({ start: z.iso.datetime(), mode: z.enum(GameMode), type: z.enum(GameType), - map: z.enum(GameMapType), + map: z.string(), difficulty: z.enum(Difficulty), clientId: z.string().optional(), }); From d828ecfabf8141a902b2bc68b7a576e024d6aa11 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Mar 2026 18:34:47 -0800 Subject: [PATCH 3/4] Add traefik with tls to startup script (#3343) ## Description: We are migrating to traefik and away from CF tunnels due to reliability issues. This PR sets up traefik with tls configured. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- setup.sh | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++- update.sh | 3 +- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/setup.sh b/setup.sh index 5a515dab3..ec4c21503 100644 --- a/setup.sh +++ b/setup.sh @@ -7,10 +7,32 @@ echo "=====================================================" echo "🚀 STARTING SERVER SETUP" echo "=====================================================" +# Load environment variables from .env.setup if present +ENV_FILE="$(dirname "$0")/.env.setup" +if [ -f "$ENV_FILE" ]; then + echo "📂 Loading environment from $ENV_FILE" + set -a + # shellcheck source=/dev/null + source "$ENV_FILE" + set +a +else + echo "â„šī¸ No .env.setup file found" + exit 1 +fi + # Verify required environment variables if [ -z "$OTEL_EXPORTER_OTLP_ENDPOINT" ] || [ -z "$OTEL_AUTH_HEADER" ]; then echo "❌ ERROR: Required environment variables are not set!" - echo "Please set OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_AUTH_HEADER" + echo "Please set OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_AUTH_HEADER in .env.setup" + exit 1 +fi + +# CF_ORIGIN_CERT and CF_ORIGIN_KEY: Cloudflare Origin Certificate and private key. +# Generate at: Cloudflare dashboard → SSL/TLS → Origin Server → Create Certificate +if [ -z "$CF_ORIGIN_CERT" ] || [ -z "$CF_ORIGIN_KEY" ]; then + echo "❌ ERROR: CF_ORIGIN_CERT and CF_ORIGIN_KEY are not set!" + echo "Generate an origin certificate at: Cloudflare → SSL/TLS → Origin Server → Create Certificate" + echo "Then add CF_ORIGIN_CERT and CF_ORIGIN_KEY to .env.setup" exit 1 fi @@ -82,6 +104,96 @@ fi chown -R openfront:openfront /home/openfront echo "Set proper ownership for openfront's home directory" +# Set up Traefik reverse proxy +echo "🔀 Setting up Traefik..." + +# Create the shared Docker network used by Traefik and app containers +if docker network ls --format '{{.Name}}' | grep -q '^web$'; then + echo "Docker network 'web' already exists" +else + docker network create web + echo "Created Docker network 'web'" +fi + +TRAEFIK_CONFIG_DIR="/home/openfront/traefik" +TRAEFIK_CERTS_DIR="$TRAEFIK_CONFIG_DIR/certs" +mkdir -p "$TRAEFIK_CERTS_DIR" + +# Write Cloudflare origin certificate and key (passed as env vars) +echo "$CF_ORIGIN_CERT" > "$TRAEFIK_CERTS_DIR/origin.crt" +echo "$CF_ORIGIN_KEY" > "$TRAEFIK_CERTS_DIR/origin.key" +chmod 600 "$TRAEFIK_CERTS_DIR/origin.crt" "$TRAEFIK_CERTS_DIR/origin.key" + +# No [api] block — dashboard is disabled for production. +# To access it for debugging, SSH tunnel: ssh -L 8080:localhost:8080 user@server +cat > "$TRAEFIK_CONFIG_DIR/traefik.toml" << 'EOF' +[log] + level = "INFO" + +[entryPoints] + [entryPoints.websecure] + address = ":443" + +[providers] + [providers.docker] + endpoint = "unix:///var/run/docker.sock" + exposedByDefault = false # Only route containers with traefik.enable=true + network = "web" + watch = true + [providers.file] + filename = "/etc/traefik/tls.toml" + watch = true +EOF + +# Static TLS configuration referencing the Cloudflare origin cert +cat > "$TRAEFIK_CONFIG_DIR/tls.toml" << 'EOF' +[[tls.certificates]] + certFile = "/certs/origin.crt" + keyFile = "/certs/origin.key" + +[tls.options] + [tls.options.default] + minVersion = "VersionTLS12" +EOF + +cat > "$TRAEFIK_CONFIG_DIR/compose.yaml" << 'EOF' +networks: + web: + # External so blue/green containers can join independently. + external: true + +services: + traefik: + image: traefik:v3.6 + container_name: traefik + restart: unless-stopped + ports: + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /home/openfront/traefik/traefik.toml:/etc/traefik/traefik.toml:ro + - /home/openfront/traefik/tls.toml:/etc/traefik/tls.toml:ro + - /home/openfront/traefik/certs:/certs:ro + networks: + - web +EOF + +# Give openfront ownership of config files but keep certs owned by root. +# Traefik runs as root inside its container so it can read them, but the +# openfront app user cannot access the TLS private key. +chown -R openfront:openfront "$TRAEFIK_CONFIG_DIR" +chown root:root "$TRAEFIK_CERTS_DIR" "$TRAEFIK_CERTS_DIR/origin.crt" "$TRAEFIK_CERTS_DIR/origin.key" + +docker compose -f "$TRAEFIK_CONFIG_DIR/compose.yaml" pull +docker compose -f "$TRAEFIK_CONFIG_DIR/compose.yaml" up -d + +if docker ps | grep -q traefik; then + echo "✅ Traefik started successfully!" +else + echo "❌ Failed to start Traefik. Check logs with: docker logs traefik" + exit 1 +fi + # Create directory for OpenTelemetry configuration echo "📊 Setting up Node Exporter and OpenTelemetry Collector..." OTEL_CONFIG_DIR="/home/openfront/otel" @@ -176,6 +288,7 @@ echo "🎉 SETUP COMPLETE!" echo "=====================================================" echo "The openfront user has been set up and has Docker permissions." echo "UDP buffer sizes have been configured for optimal QUIC/WebSocket performance." +echo "Traefik reverse proxy is running (HTTP :80, HTTPS :443 with Cloudflare origin cert)." echo "Node Exporter is collecting system metrics." echo "OpenTelemetry Collector is forwarding metrics to your endpoint." echo "" diff --git a/update.sh b/update.sh index 89e056f1e..6d16ed9aa 100755 --- a/update.sh +++ b/update.sh @@ -73,7 +73,8 @@ docker run -d \ --network web \ --label "traefik.enable=true" \ --label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${SUBDOMAIN}.${DOMAIN}\`)" \ - --label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=web" \ + --label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=websecure" \ + --label "traefik.http.routers.${CONTAINER_NAME}.tls=true" \ --label "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=80" \ "${GHCR_IMAGE}" From 799da9d1b70ee066f96666a66a255489b98c00d3 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Mar 2026 18:45:24 -0800 Subject: [PATCH 4/4] Migrate to a new prod machine: falk2 (#3346) ## Description: Migrate to a beefier machine ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/release.yml | 24 ++++++++++++------------ build-deploy.sh | 12 ++++++------ deploy.sh | 9 ++++++--- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 807502598..dd0ea91be 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,7 @@ on: options: - masters - staging + - falk2 - falk1 target_subdomain: description: "Deployment Subdomain" @@ -94,6 +95,7 @@ jobs: env: SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }} SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | @@ -102,6 +104,7 @@ jobs: echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa test -n "$SERVER_HOST_MASTERS" && ssh-keyscan -H "$SERVER_HOST_MASTERS" >> ~/.ssh/known_hosts test -n "$SERVER_HOST_FALK1" && ssh-keyscan -H "$SERVER_HOST_FALK1" >> ~/.ssh/known_hosts + test -n "$SERVER_HOST_FALK2" && ssh-keyscan -H "$SERVER_HOST_FALK2" >> ~/.ssh/known_hosts test -n "$SERVER_HOST_STAGING" && ssh-keyscan -H "$SERVER_HOST_STAGING" >> ~/.ssh/known_hosts chmod 600 ~/.ssh/id_rsa - name: đŸšĸ Deploy @@ -118,6 +121,7 @@ jobs: API_KEY: ${{ secrets.API_KEY }} SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }} SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }} SSH_KEY: ~/.ssh/id_rsa VERSION_TAG: latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a09e6072..be979218c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,13 +105,13 @@ jobs: - uses: actions/checkout@v4 - name: 🔑 Create SSH private key env: - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | set -euxo pipefail mkdir -p ~/.ssh echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa - test -n "$SERVER_HOST_FALK1" && ssh-keyscan -H "$SERVER_HOST_FALK1" >> ~/.ssh/known_hosts + test -n "$SERVER_HOST_FALK2" && ssh-keyscan -H "$SERVER_HOST_FALK2" >> ~/.ssh/known_hosts chmod 600 ~/.ssh/id_rsa - name: 🚀 Deploy image env: @@ -125,11 +125,11 @@ jobs: OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }} TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }} API_KEY: ${{ secrets.API_KEY }} - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_KEY: ~/.ssh/id_rsa run: | set -euxo pipefail - ./deploy.sh prod falk1 "${IMAGE_ID}" beta + ./deploy.sh prod falk2 "${IMAGE_ID}" beta - name: âŗ Wait for deployment to start env: FQDN: beta.${{ vars.DOMAIN }} @@ -156,13 +156,13 @@ jobs: - uses: actions/checkout@v4 - name: 🔑 Create SSH private key env: - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | set -euxo pipefail mkdir -p ~/.ssh echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa - test -n "$SERVER_HOST_FALK1" && ssh-keyscan -H "$SERVER_HOST_FALK1" >> ~/.ssh/known_hosts + test -n "$SERVER_HOST_FALK2" && ssh-keyscan -H "$SERVER_HOST_FALK2" >> ~/.ssh/known_hosts chmod 600 ~/.ssh/id_rsa - name: 🚀 Deploy image env: @@ -176,11 +176,11 @@ jobs: OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }} TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }} API_KEY: ${{ secrets.API_KEY }} - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_KEY: ~/.ssh/id_rsa run: | set -euxo pipefail - ./deploy.sh prod falk1 "${IMAGE_ID}" blue + ./deploy.sh prod falk2 "${IMAGE_ID}" blue - name: âŗ Wait for deployment to start env: FQDN: blue.${{ vars.DOMAIN }} @@ -207,13 +207,13 @@ jobs: - uses: actions/checkout@v4 - name: 🔑 Create SSH private key env: - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | set -euxo pipefail mkdir -p ~/.ssh echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa - test -n "$SERVER_HOST_FALK1" && ssh-keyscan -H "$SERVER_HOST_FALK1" >> ~/.ssh/known_hosts + test -n "$SERVER_HOST_FALK2" && ssh-keyscan -H "$SERVER_HOST_FALK2" >> ~/.ssh/known_hosts chmod 600 ~/.ssh/id_rsa - name: 🚀 Deploy image env: @@ -227,11 +227,11 @@ jobs: OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }} TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }} API_KEY: ${{ secrets.API_KEY }} - SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }} + SERVER_HOST_FALK2: ${{ secrets.SERVER_HOST_FALK2 }} SSH_KEY: ~/.ssh/id_rsa run: | set -euxo pipefail - ./deploy.sh prod falk1 "${IMAGE_ID}" green + ./deploy.sh prod falk2 "${IMAGE_ID}" green - name: âŗ Wait for deployment to start env: FQDN: green.${{ vars.DOMAIN }} diff --git a/build-deploy.sh b/build-deploy.sh index e4190cf8f..6471fcc51 100755 --- a/build-deploy.sh +++ b/build-deploy.sh @@ -15,34 +15,34 @@ print_header "BUILD AND DEPLOY WRAPPER" echo "This script will run build.sh and deploy.sh in sequence." echo "You can also run them separately:" echo " ./build.sh [prod|staging] [version_tag]" -echo " ./deploy.sh [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]" +echo " ./deploy.sh [prod|staging] [falk1|falk2|nbg1|staging|masters] [version_tag] [subdomain]" echo "" # Check command line arguments if [ $# -lt 3 ] || [ $# -gt 5 ]; then echo "Error: Please specify environment, host, and subdomain" - echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]" + echo "Usage: $0 [prod|staging] [falk1|falk2|nbg1|staging|masters] [subdomain]" exit 1 fi # Validate first argument (environment) if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then echo "Error: First argument must be either 'prod' or 'staging'" - echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]" + echo "Usage: $0 [prod|staging] [falk1|falk2|nbg1|staging|masters] [subdomain]" exit 1 fi # Validate second argument (host) -if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then +if [ "$2" != "falk1" ] && [ "$2" != "falk2" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'" - echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]" + echo "Usage: $0 [prod|staging] [falk1|falk2|nbg1|staging|masters] [subdomain]" exit 1 fi # Validate third argument (subdomain) if [ -z "$3" ]; then echo "Error: Subdomain is required" - echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]" + echo "Usage: $0 [prod|staging] [falk1|falk2|nbg1|staging|masters] [subdomain]" exit 1 fi diff --git a/deploy.sh b/deploy.sh index 6e0c6727a..dc2f9edac 100755 --- a/deploy.sh +++ b/deploy.sh @@ -28,9 +28,9 @@ if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then fi # Validate second argument (host) -if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then - echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'" - echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]" +if [ "$2" != "falk1" ] && [ "$2" != "falk2" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then + echo "Error: Second argument must be either 'falk1', 'falk2', 'nbg1', 'staging', or 'masters'" + echo "Usage: $0 [prod|staging] [falk1|falk2|nbg1|staging|masters] [version_tag] [subdomain]" exit 1 fi @@ -75,6 +75,9 @@ elif [ "$HOST" == "nbg1" ]; then elif [ "$HOST" == "masters" ]; then print_header "DEPLOYING TO MASTERS HOST" SERVER_HOST=$SERVER_HOST_MASTERS +elif [ "$HOST" == "falk2" ]; then + print_header "DEPLOYING TO FALK2 HOST" + SERVER_HOST=$SERVER_HOST_FALK2 else print_header "DEPLOYING TO FALK1 HOST" SERVER_HOST=$SERVER_HOST_FALK1