CS2 Mapveto als ein Container (Dockerfile, compose.yml, Anleitung)
Ein Image mit Webseite, Backend, SQLite und Docker-Client; startet Match-Container über /var/run/docker.sock auf demselben Host. Tests laufen beim Bauen mit. Auf amp-cluster gebaut (301 MB) und geprüft. Dazu: Testbefehl für Node 22 angepasst, beim Beenden werden ausstehende Live-Updates verworfen und laufende Server-Aktionen abgewartet, damit nach dem Schließen der Datenbank nichts mehr darauf zugreift.
This commit is contained in:
1 parent
e3a1ed4a63
commit
1b7c13b592
9 files changed
+236
-1
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
**/node_modules
|
||||||
|
client/dist
|
||||||
|
data
|
||||||
|
*.db
|
||||||
|
*.db-*
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.git
|
||||||
|
.claude
|
||||||
|
npm-debug.log*
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# CS2 Mapveto als ein Container: Webseite, Backend, Datenbank (SQLite) und Docker-Client,
|
||||||
|
# mit dem die App pro Match eigene CS2-Server-Container auf demselben Docker-Host startet.
|
||||||
|
|
||||||
|
# ---------- Bauen ----------
|
||||||
|
FROM node:22-bookworm-slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
# Werkzeuge, falls better-sqlite3 kein fertiges Binärpaket findet
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY client/package.json client/
|
||||||
|
COPY server/package.json server/
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build && npm test && npm prune --omit=dev
|
||||||
|
|
||||||
|
# ---------- Laufzeit ----------
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
# Docker-Client (nur das Programm, kein Daemon): spricht über /var/run/docker.sock mit dem Host
|
||||||
|
COPY --from=docker:27-cli /usr/local/bin/docker /usr/local/bin/docker
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=3000 \
|
||||||
|
DATABASE_FILE=/data/cs2-mapveto.db
|
||||||
|
COPY --from=build /app/package.json ./
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/server ./server
|
||||||
|
COPY --from=build /app/client/dist ./client/dist
|
||||||
|
VOLUME /data
|
||||||
|
EXPOSE 3000
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/config').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
CMD ["node", "server/src/index.js"]
|
||||||
@@ -14,6 +14,17 @@ Webanwendung zur Organisation von Counter-Strike-2-Matches: Match-Lobbys mit Tea
|
|||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
**Als Container (empfohlen für den Betrieb):** ein Container mit Webseite, Backend und Datenbank, der die
|
||||||
|
CS2-Server-Container auf demselben Docker-Host startet. Siehe [docs/container.md](docs/container.md):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker -H ssh://cs2docker build -t cs2-mapveto:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
…dann den Stack aus der Anleitung in Portainer anlegen.
|
||||||
|
|
||||||
|
**Zum Entwickeln:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
cp .env.example .env # optional, Standardwerte funktionieren für die Demo
|
cp .env.example .env # optional, Standardwerte funktionieren für die Demo
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# CS2 Mapveto auf einem Docker-Host (z. B. als Portainer-Stack).
|
||||||
|
# Die App startet pro Match einen CS2-Container auf demselben Host (Docker-Adapter).
|
||||||
|
#
|
||||||
|
# Vorher einmalig: Image bauen (siehe docs/container.md) und das Volume
|
||||||
|
# "cs2-mapveto-gamefiles" mit den CS2-Spieldateien befüllen (docs/portainer-cs2.md).
|
||||||
|
|
||||||
|
services:
|
||||||
|
cs2-mapveto:
|
||||||
|
image: cs2-mapveto:latest
|
||||||
|
build: .
|
||||||
|
container_name: cs2-mapveto
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
# Adresse dieses Docker-Hosts im LAN – für Einladungslinks, Spieler-Verbindungen und Live-Logs
|
||||||
|
- PUBLIC_URL=http://${HOST_IP:-192.168.178.111}:3000
|
||||||
|
- LOG_RECEIVER_URL=http://${HOST_IP:-192.168.178.111}:3000
|
||||||
|
- SERVER_ADAPTER=docker
|
||||||
|
- CS2_PUBLIC_HOST=${HOST_IP:-192.168.178.111}
|
||||||
|
# RCON zu den Match-Containern über die veröffentlichten Ports des Hosts
|
||||||
|
- CS2_RCON_HOST=${HOST_IP:-192.168.178.111}
|
||||||
|
# leer = LAN-Modus (sv_lan 1)
|
||||||
|
- CS2_GSLT=${CS2_GSLT:-}
|
||||||
|
- CS2_DOCKER_VOLUME=cs2-mapveto-gamefiles
|
||||||
|
- CS2_DOCKER_IMAGE=joedwards32/cs2:latest
|
||||||
|
- CS2_PORT_RANGE_START=27100
|
||||||
|
- CS2_PORT_RANGE_END=27139
|
||||||
|
- CREATE_MATCH_PASSWORD=${CREATE_MATCH_PASSWORD:-}
|
||||||
|
- STREAM_KEY=${STREAM_KEY:-}
|
||||||
|
- SERIES_AUTO_ADVANCE_SECONDS=30
|
||||||
|
- SERVER_IDLE_TIMEOUT_MINUTES=30
|
||||||
|
volumes:
|
||||||
|
# Datenbank (Matches, Ergebnisse) überdauert Updates des Containers
|
||||||
|
- cs2-mapveto-data:/data
|
||||||
|
# Zugriff auf Docker, um Match-Container zu starten und zu entfernen
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cs2-mapveto-data:
|
||||||
|
name: cs2-mapveto-data
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# CS2 Mapveto als Container betreiben
|
||||||
|
|
||||||
|
CS2 Mapveto läuft als **ein** Container: Webseite, Backend, Datenbank (SQLite) und ein Docker-Client.
|
||||||
|
Er steht auf demselben Docker-Host wie die CS2-Server und startet pro Match über `/var/run/docker.sock`
|
||||||
|
einen eigenen CS2-Container (Docker-Adapter). Ein PC mit Node.js oder SSH wird dafür nicht mehr gebraucht.
|
||||||
|
|
||||||
|
```
|
||||||
|
Docker-Host (z. B. amp-cluster, 192.168.178.111)
|
||||||
|
├── cs2-mapveto ← dieser Container, Port 3000
|
||||||
|
├── cs2mv-<match> ← CS2-Server pro Match, Ports ab 27100 (startet/entfernt CS2 Mapveto)
|
||||||
|
├── Volume cs2-mapveto-gamefiles CS2-Spieldateien (~73 GB, einmal befüllt)
|
||||||
|
└── Volume cs2-mapveto-data Datenbank von CS2 Mapveto
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Nur eine Instanz pro Docker-Host.** Läuft CS2 Mapveto zusätzlich auf einem PC mit `SERVER_ADAPTER=docker`
|
||||||
|
> und demselben Docker-Host, vergeben beide unabhängig voneinander Ports und Containernamen. Die PC-Instanz
|
||||||
|
> also beenden oder auf einen anderen Adapter stellen, bevor der Container läuft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Image bauen
|
||||||
|
|
||||||
|
Das Image wird auf dem Docker-Host gebaut, beim Bauen laufen auch alle Tests. Am einfachsten vom PC aus, im
|
||||||
|
Projektordner, über den SSH-Zugang zum Host:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker -H ssh://cs2docker build -t cs2-mapveto:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis: Image `cs2-mapveto:latest` (~300 MB) auf dem Host. Alternativ direkt auf dem Host nach einem
|
||||||
|
`git clone` des Repositorys: `docker build -t cs2-mapveto:latest .`
|
||||||
|
|
||||||
|
> Meldet Docker unter Windows `docker-credential-desktop: executable file not found`, kommt das von einer
|
||||||
|
> Docker-Desktop-Einstellung. Abhilfe für den einen Befehl: `$env:DOCKER_CONFIG = "$env:TEMP\dockercfg"`
|
||||||
|
> (Ordner mit einer Datei `config.json`, Inhalt `{}`).
|
||||||
|
|
||||||
|
## 2. Stack in Portainer anlegen
|
||||||
|
|
||||||
|
Portainer → **Stacks** → **+ Add stack** → Name `cs2-mapveto` → *Web editor*:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
cs2-mapveto:
|
||||||
|
image: cs2-mapveto:latest
|
||||||
|
container_name: cs2-mapveto
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- PUBLIC_URL=http://192.168.178.111:3000
|
||||||
|
- LOG_RECEIVER_URL=http://192.168.178.111:3000
|
||||||
|
- SERVER_ADAPTER=docker
|
||||||
|
- CS2_PUBLIC_HOST=192.168.178.111
|
||||||
|
- CS2_RCON_HOST=192.168.178.111
|
||||||
|
- CS2_GSLT=
|
||||||
|
- CS2_DOCKER_VOLUME=cs2-mapveto-gamefiles
|
||||||
|
- CS2_DOCKER_IMAGE=joedwards32/cs2:latest
|
||||||
|
- CS2_PORT_RANGE_START=27100
|
||||||
|
- CS2_PORT_RANGE_END=27139
|
||||||
|
- CREATE_MATCH_PASSWORD=
|
||||||
|
- STREAM_KEY=
|
||||||
|
- SERIES_AUTO_ADVANCE_SECONDS=30
|
||||||
|
- SERVER_IDLE_TIMEOUT_MINUTES=30
|
||||||
|
volumes:
|
||||||
|
- cs2-mapveto-data:/data
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cs2-mapveto-data:
|
||||||
|
name: cs2-mapveto-data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deploy the stack.** Danach ist CS2 Mapveto unter `http://192.168.178.111:3000` erreichbar, die Stream-Übersicht
|
||||||
|
unter `/stream`.
|
||||||
|
|
||||||
|
Dieselbe Datei liegt als [`compose.yml`](../compose.yml) im Repository (dort zusätzlich mit `build: .`, für
|
||||||
|
`docker compose up -d --build` direkt auf dem Host).
|
||||||
|
|
||||||
|
### Einstellungen
|
||||||
|
|
||||||
|
Alle Umgebungsvariablen aus [`.env.example`](../.env.example) funktionieren auch hier. Die wichtigsten:
|
||||||
|
|
||||||
|
| Variable | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `PUBLIC_URL` | Adresse der Webseite für Einladungslinks |
|
||||||
|
| `LOG_RECEIVER_URL` | Adresse, an die die CS2-Container ihre Logs schicken (Live-Ergebnisse) |
|
||||||
|
| `CS2_PUBLIC_HOST` / `CS2_RCON_HOST` | IP des Docker-Hosts – Spieler verbinden sich dorthin, CS2 Mapveto steuert die Server darüber |
|
||||||
|
| `CS2_GSLT` | leer = LAN-Modus; mit Token öffentlich erreichbar (ein Token pro gleichzeitigem Match) |
|
||||||
|
| `CS2_PORT_RANGE_START/END` | Ports für Match-Container, zwei pro Match, in der Firewall freigeben |
|
||||||
|
| `CREATE_MATCH_PASSWORD` | Passwort zum Anlegen von Matches |
|
||||||
|
| `STREAM_KEY` | Schlüssel für `/stream?key=…` |
|
||||||
|
|
||||||
|
**Passwörter und Schlüssel nur aus Buchstaben und Ziffern** wählen.
|
||||||
|
|
||||||
|
## 3. Updaten
|
||||||
|
|
||||||
|
1. Neues Image bauen (Schritt 1, gleicher Befehl).
|
||||||
|
2. Portainer → Stack `cs2-mapveto` → **Update the stack** (bzw. Container → *Recreate*).
|
||||||
|
|
||||||
|
Die Datenbank liegt im Volume `cs2-mapveto-data` und bleibt erhalten. Laufende Match-Container übernimmt
|
||||||
|
CS2 Mapveto nach dem Neustart wieder; ein Serverstart, der gerade lief, wird als unterbrochen markiert und
|
||||||
|
lässt sich mit „Server neu starten“ wiederholen.
|
||||||
|
|
||||||
|
## 4. Sicherheit
|
||||||
|
|
||||||
|
Der Container hat über `/var/run/docker.sock` volle Kontrolle über Docker auf dem Host – das ist nötig, um
|
||||||
|
Match-Container zu starten, entspricht aber Root-Rechten auf dem Host. Deshalb:
|
||||||
|
|
||||||
|
- Port 3000 nur im LAN anbieten, nicht ins Internet weiterleiten.
|
||||||
|
- `CREATE_MATCH_PASSWORD` setzen, wenn nicht jeder im LAN Matches anlegen soll.
|
||||||
|
- RCON-Ports (TCP 27100–27139) möglichst nur vom Host bzw. LAN erreichbar lassen.
|
||||||
|
|
||||||
|
## 5. Fehlersuche
|
||||||
|
|
||||||
|
| Symptom | Ursache / Lösung |
|
||||||
|
|---|---|
|
||||||
|
| „Server wird angefordert“ bleibt stehen | Container-Log prüfen (Portainer → `cs2-mapveto` → Logs); Docker-Socket eingebunden? |
|
||||||
|
| `permission denied … docker.sock` im Log | Socket nicht eingebunden oder Docker mit Rootless-Modus – Pfad zum Socket anpassen |
|
||||||
|
| Match-Container startet, „Server startet“ endet in Fehler | Ports 27100+ von `CS2_RCON_HOST` aus erreichbar? Firewall des Hosts |
|
||||||
|
| Keine Live-Ergebnisse | `LOG_RECEIVER_URL` muss die Host-IP mit Port 3000 sein, nicht `localhost` |
|
||||||
|
| Port bereits belegt | Eine zweite CS2-Mapveto-Instanz nutzt denselben Host (siehe Hinweis oben) |
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node --watch src/index.js",
|
"dev": "node --watch src/index.js",
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"test": "node --test test/"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export function createApp(config, { adapter: adapterOverride, logger = console }
|
|||||||
},
|
},
|
||||||
async close() {
|
async close() {
|
||||||
manager.stopLoops();
|
manager.stopLoops();
|
||||||
|
// Laufende Server-Aktionen (Start, Konfiguration) kurz abwarten, bevor die Datenbank schließt
|
||||||
|
await manager.idle(5000);
|
||||||
|
io.disposeRealtime();
|
||||||
io.close();
|
io.close();
|
||||||
await new Promise((resolve) => server.close(() => resolve()));
|
await new Promise((resolve) => server.close(() => resolve()));
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -111,5 +111,14 @@ export function attachRealtime(httpServer, { service, config }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Beim Herunterfahren: ausstehende Sammel-Updates verwerfen, damit nichts mehr auf die Datenbank zugreift
|
||||||
|
io.disposeRealtime = () => {
|
||||||
|
clearTimeout(overviewTimer);
|
||||||
|
clearTimeout(streamTimer);
|
||||||
|
for (const t of pending.values()) clearTimeout(t);
|
||||||
|
pending.clear();
|
||||||
|
service.removeAllListeners('changed');
|
||||||
|
};
|
||||||
|
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,12 @@ export class ServerManager {
|
|||||||
this.timers.push(setInterval(() => this.reap().catch((e) => this.logger.error(e)), this.config.reaperIntervalSeconds * 1000));
|
this.timers.push(setInterval(() => this.reap().catch((e) => this.logger.error(e)), this.config.reaperIntervalSeconds * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wartet, bis keine Server-Aktion mehr läuft (höchstens timeoutMs). */
|
||||||
|
async idle(timeoutMs = 5000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (this.busy.size > 0 && Date.now() < deadline) await new Promise((r) => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
|
||||||
stopLoops() {
|
stopLoops() {
|
||||||
this.simulator.stopAll();
|
this.simulator.stopAll();
|
||||||
for (const t of this.autoAdvance.values()) clearTimeout(t);
|
for (const t of this.autoAdvance.values()) clearTimeout(t);
|
||||||
|
|||||||
Reference in new issue
Block a user