commit 26d4c7aca6ec73babf78d2c84e6a517f34448305 Author: Bernhard Blieninger Date: Mon Mar 16 00:45:42 2026 +0100 Initial commit: self-hosted Docker setup for OpenFrontIO Includes docker-compose, setup script, nginx reverse proxy docs, and GAME_ENV configuration notes for self-hosted deployments. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9efb8a7 --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# ============================================================ +# OpenFrontIO — Docker Compose Environment Configuration +# Copy this file to .env and adjust values as needed. +# ============================================================ + +# Port to expose the game on the host machine (default: 80) +HOST_PORT=8989 + +# Git commit hash embedded in the build for version tracking. +# Automatically set by setup.sh. Can also be pinned to a specific commit. +GIT_COMMIT=unknown + +# Game environment: dev | staging | prod +# Must be set, otherwise /api/env returns HTTP 500 and the frontend breaks. +# Use "dev" for self-hosted setups — prod/staging use domain-bound Cloudflare +# Turnstile keys (openfront.io / openfront.dev) that fail on other domains (error 110200). +GAME_ENV=dev + +# ============================================================ +# Cloudflare Tunnel (optional) +# Leave all CF_* variables empty to run without a tunnel. +# The server will then be accessible only via HOST_PORT. +# ============================================================ + +# Cloudflare API token with Tunnel:Edit and DNS:Edit permissions +CF_API_TOKEN= + +# Cloudflare Account ID (from the Cloudflare dashboard URL) +CF_ACCOUNT_ID= + +# Subdomain prefix for the tunnel (e.g. "myserver" → tunnel-myserver.example.com) +SUBDOMAIN= + +# Root domain managed by Cloudflare (e.g. "example.com") +DOMAIN=openfront.muc.datenh.eu diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fbcc50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Upstream-Repository – wird von setup.sh geklont +OpenFrontIO/ + +# Lokale Konfiguration mit Secrets +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b6b921 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# OpenFrontIO — Self-Hosted Docker Setup + +Portables, reproduzierbares Docker-Setup für [OpenFrontIO](https://github.com/openfrontio/OpenFrontIO), einen browserbasierten Echtzeit-Strategietitel mit Territorien, Bündnissen und bis zu 41 gleichzeitigen Worker-Prozessen. + +--- + +## Voraussetzungen + +- Docker >= 24 +- Docker Compose >= 2.20 +- Git +- 2 GB freier RAM (empfohlen: 4 GB) +- ~2 GB freier Speicher für das Image + +--- + +## Schnellstart + +```bash +# 1. Repository klonen & vorbereiten +./setup.sh + +# 2. (optional) Konfiguration anpassen +nano .env + +# 3. Image bauen +docker compose build + +# 4. Container starten +docker compose up -d +``` + +Das Spiel ist danach unter **http://localhost** erreichbar (Standard-Port 80). + +--- + +## Verzeichnisstruktur + +``` +openfront/ +├── setup.sh # Einmaliger Setup-Assistent +├── docker-compose.yml # Compose-Konfiguration +├── startup-local.sh # Container-Entrypoint (Cloudflare optional) +├── supervisord-local.conf # Supervisor-Konfiguration ohne Cloudflared +├── .env.example # Vorlage für Umgebungsvariablen +├── .env # Deine Konfiguration (wird von setup.sh erstellt) +└── OpenFrontIO/ # Geklontes Upstream-Repository (wird von setup.sh erstellt) +``` + +--- + +## Konfiguration + +`setup.sh` erstellt automatisch eine `.env`-Datei aus `.env.example`. Alle verfügbaren Optionen: + +| Variable | Standard | Beschreibung | +|---|---|---| +| `HOST_PORT` | `80` | Externer Port auf dem Host | +| `GIT_COMMIT` | automatisch | Git-Commit-Hash, der ins Image eingebettet wird | +| `GAME_ENV` | leer | Spielumgebung: `dev`, `staging` oder `prod` (siehe unten) | +| `CF_API_TOKEN` | leer | Cloudflare API-Token (optional, siehe unten) | +| `CF_ACCOUNT_ID` | leer | Cloudflare Account-ID (optional) | +| `SUBDOMAIN` | leer | Subdomain-Präfix für den Tunnel (optional) | +| `DOMAIN` | leer | Root-Domain bei Cloudflare (optional) | + +### Anderen Port verwenden + +```bash +# .env +HOST_PORT=8080 +``` + +--- + +## Nginx Reverse Proxy mit HTTPS + +Um den Server öffentlich unter einer eigenen Domain zu betreiben: + +**1. Nginx-Config erstellen** (`/etc/nginx/conf.d/openfront.example.com`): + +```nginx +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; + server_name openfront.example.com; + + location ^~ /.well-known/acme-challenge { + default_type text/plain; + root /var/www/letsencrypt; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name openfront.example.com; + + ssl_certificate /etc/letsencrypt/live/openfront.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/openfront.example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://127.0.0.1:8989; # HOST_PORT aus .env + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} +``` + +**2. TLS-Zertifikat holen** (DNS-Challenge empfohlen, da HTTP-Challenge durch vorgelagerte Auth-Proxies blockiert werden kann): + +```bash +# Mit Cloudflare DNS: +sudo certbot certonly --dns-cloudflare \ + --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ + -d openfront.example.com + +# Oder manuell: +sudo certbot certonly --manual --preferred-challenges dns -d openfront.example.com +``` + +**3. `GAME_ENV` korrekt setzen** – Pflicht, damit `/api/env` antwortet und das Frontend korrekt initialisiert wird: + +```bash +# .env +GAME_ENV=dev +``` + +> **Wichtig:** `GAME_ENV` muss gesetzt sein, sonst gibt `/api/env` HTTP 500 zurück und Features wie der „Link kopieren"-Button funktionieren nicht. +> +> `GAME_ENV=prod` und `GAME_ENV=staging` verwenden hardcodierte Cloudflare-Turnstile-Keys für `openfront.io` bzw. `openfront.dev` – diese Keys sind domain-gebunden und schlagen auf anderen Domains mit Fehler **110200** fehl. `GAME_ENV=dev` verwendet Cloudflares offizielle Test-Keys, die immer durchgehen. + +**4. Container mit neuer Konfiguration neu erstellen:** + +```bash +docker compose up -d --force-recreate +``` + +> `docker compose restart` reicht nicht – Änderungen an `.env` werden erst nach `up -d` (oder `--force-recreate`) übernommen. + +--- + +## Cloudflare Tunnel (optional) + +Ohne Cloudflare-Konfiguration läuft der Server ausschließlich lokal auf `HOST_PORT`. Für einen öffentlich erreichbaren Server über Cloudflare Tunnel: + +1. Erstelle einen API-Token im [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens) mit den Berechtigungen **Tunnel:Edit** und **DNS:Edit**. +2. Trage die Werte in `.env` ein: + +```bash +CF_API_TOKEN=dein_token +CF_ACCOUNT_ID=deine_account_id +SUBDOMAIN=myserver # → tunnel-myserver.example.com +DOMAIN=example.com +``` + +3. Beim nächsten Start erstellt der Container automatisch den Tunnel und den passenden DNS-CNAME-Eintrag. + +--- + +## Nützliche Befehle + +```bash +# Logs in Echtzeit verfolgen +docker compose logs -f + +# Nur Node-Server-Logs +docker compose logs -f openfront | grep "\[node\]" + +# Container-Status prüfen +docker compose ps + +# Container neustarten +docker compose restart + +# Stoppen +docker compose down + +# Stoppen und Image entfernen +docker compose down --rmi local + +# Neu bauen nach Upstream-Updates +git -C OpenFrontIO pull +docker compose build --no-cache +docker compose up -d +``` + +--- + +## Architektur + +Das Image enthält drei Prozesse, verwaltet von [Supervisor](http://supervisord.org/): + +``` +Container (Port 80) +└── Nginx (Reverse Proxy + Cache) + ├── / → Node.js Master (Port 3000) + ├── /api/* → Node.js Master (Port 3000) + ├── /lobbies → Node.js Master (Port 3000, WebSocket) + └── /w0 … /w40 → Node.js Worker 0–40 (Ports 3001–3041) +``` + +Nginx übernimmt Caching, WebSocket-Upgrades und das Routing zu den Worker-Prozessen. Alle internen Ports (3000–3041) sind **nicht** nach außen exponiert. + +--- + +## Troubleshooting + +### Build schlägt wegen `proprietary`-Verzeichnis fehl + +Das `proprietary`-Submodul ist ein privates Repository der Entwickler. `setup.sh` erstellt automatisch einen leeren Placeholder, falls es nicht erreichbar ist. Der Build und das Spiel funktionieren auch ohne dieses Verzeichnis. + +```bash +mkdir -p OpenFrontIO/proprietary +docker compose build +``` + +### Port 80 bereits belegt + +```bash +# .env anpassen: +HOST_PORT=8080 +docker compose up -d +``` + +### Container startet, Spiel ist nicht erreichbar + +```bash +# Healthcheck-Status prüfen +docker inspect openfront-openfront-1 | grep -A5 Health + +# Direkt im Container nachsehen +docker compose exec openfront supervisorctl status +``` + +### Upstream-Updates einspielen + +```bash +git -C OpenFrontIO pull +docker compose build +docker compose up -d +``` + +--- + +## Lizenz + +Der Quellcode von OpenFrontIO steht unter der [AGPL-3.0](https://github.com/openfrontio/OpenFrontIO/blob/main/LICENSE). Assets stehen unter [CC BY-SA 4.0](https://github.com/openfrontio/OpenFrontIO/blob/main/LICENSE-ASSETS). Copyright-Hinweise müssen in der Oberfläche sichtbar bleiben. + +Dieses Docker-Setup ist ein inoffizielles Convenience-Wrapper ohne eigene Lizenzansprüche. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..74fc675 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + openfront: + build: + context: ./OpenFrontIO + dockerfile: Dockerfile + args: + GIT_COMMIT: ${GIT_COMMIT:-unknown} + entrypoint: ["/usr/local/bin/startup-local.sh"] + ports: + - "${HOST_PORT:-80}:80" + env_file: + - .env + volumes: + # Override startup and supervisor config with our local versions + - ./startup-local.sh:/usr/local/bin/startup-local.sh:ro + - ./supervisord-local.conf:/etc/supervisor/conf.d/supervisord-local.conf:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..723fc87 --- /dev/null +++ b/setup.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e + +REPO_URL="https://github.com/openfrontio/OpenFrontIO.git" +REPO_DIR="./OpenFrontIO" + +echo "=== OpenFrontIO Docker Setup ===" + +# Clone repo if not already present +if [ ! -d "$REPO_DIR/.git" ]; then + echo "[1/4] Cloning OpenFrontIO repository..." + git clone "$REPO_URL" "$REPO_DIR" +else + echo "[1/4] Repository already cloned. Pulling latest changes..." + git -C "$REPO_DIR" pull +fi + +# Initialize git submodules (proprietary assets etc.) +echo "[2/4] Initializing submodules..." +git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true + +# The 'proprietary' directory may be a private submodule. +# If it doesn't exist after submodule init, create an empty one so Docker COPY doesn't fail. +if [ ! -d "$REPO_DIR/proprietary" ]; then + echo " Note: 'proprietary' submodule not available (private). Creating empty placeholder." + mkdir -p "$REPO_DIR/proprietary" +fi + +# Set up .env from example if it doesn't exist yet +echo "[3/4] Setting up .env..." +if [ ! -f ".env" ]; then + cp .env.example .env + echo " .env created from .env.example — edit it before starting!" +else + echo " .env already exists, skipping." +fi + +# Capture git commit hash for reproducible builds +GIT_COMMIT=$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") +# Update GIT_COMMIT in .env if placeholder +if grep -q "^GIT_COMMIT=unknown" .env 2>/dev/null; then + sed -i "s/^GIT_COMMIT=unknown/GIT_COMMIT=${GIT_COMMIT}/" .env +fi + +echo "[4/4] Setup complete!" +echo "" +echo "Next steps:" +echo " 1. Edit .env (at minimum set HOST_PORT if port 80 is taken)" +echo " 2. docker compose build" +echo " 3. docker compose up -d" +echo " 4. Open http://localhost (or your HOST_PORT)" +echo "" +echo " Optional: Set CF_API_TOKEN, CF_ACCOUNT_ID, SUBDOMAIN, DOMAIN in .env" +echo " for automatic Cloudflare Tunnel setup." diff --git a/startup-local.sh b/startup-local.sh new file mode 100755 index 0000000..7305a76 --- /dev/null +++ b/startup-local.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e + +SUPERVISORD_CMD="/usr/bin/supervisord" +SUPERVISORD_CONF_FULL="/etc/supervisor/conf.d/supervisord.conf" +SUPERVISORD_CONF_LOCAL="/etc/supervisor/conf.d/supervisord-local.conf" + +# Check if all required Cloudflare variables are set +if [ -n "$CF_API_TOKEN" ] && [ -n "$CF_ACCOUNT_ID" ] && [ -n "$SUBDOMAIN" ] && [ -n "$DOMAIN" ]; then + echo "Cloudflare variables detected — setting up tunnel..." + + TIMESTAMP=$(date +%Y%m%d%H%M%S) + TUNNEL_NAME="${SUBDOMAIN}-tunnel-${TIMESTAMP}" + echo "Tunnel name: ${TUNNEL_NAME}" + + TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"name\":\"${TUNNEL_NAME}\"}") + + TUNNEL_ID=$(echo "$TUNNEL_RESPONSE" | jq -r '.result.id') + TUNNEL_TOKEN=$(echo "$TUNNEL_RESPONSE" | jq -r '.result.token') + + if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" = "null" ]; then + echo "Error: Failed to create Cloudflare tunnel" + echo "$TUNNEL_RESPONSE" + exit 1 + fi + + echo "Tunnel ID: ${TUNNEL_ID}" + + curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"config\":{\"ingress\":[{\"hostname\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"service\":\"http://localhost:80\"},{\"service\":\"http_status:404\"}]}}" + + DNS_RECORDS=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json") + ZONE_ID=$(echo "$DNS_RECORDS" | jq -r '.result[0].id') + + if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" = "null" ]; then + echo "Error: Could not find DNS zone for ${DOMAIN}" + exit 1 + fi + + EXISTING=$(curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=tunnel-${SUBDOMAIN}.${DOMAIN}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json") + RECORD_ID=$(echo "$EXISTING" | jq -r '.result[0].id') + + if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" = "null" ]; then + curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"type\":\"CNAME\",\"name\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}" + else + curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"type\":\"CNAME\",\"name\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}" + fi + + echo "Site will be available at: https://tunnel-${SUBDOMAIN}.${DOMAIN}" + export CLOUDFLARE_TUNNEL_TOKEN="${TUNNEL_TOKEN}" + + # Use the full supervisor config (includes cloudflared) + exec "$SUPERVISORD_CMD" -c "$SUPERVISORD_CONF_FULL" +else + echo "No Cloudflare variables set — starting without tunnel (port 80 only)." + # Use local supervisor config (nginx + node only, no cloudflared) + exec "$SUPERVISORD_CMD" -c "$SUPERVISORD_CONF_LOCAL" +fi diff --git a/supervisord-local.conf b/supervisord-local.conf new file mode 100644 index 0000000..086622a --- /dev/null +++ b/supervisord-local.conf @@ -0,0 +1,25 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:node] +command=npm run start:server +directory=/usr/src/app +autostart=true +autorestart=true +user=node +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0