refacto: Changing some docker images to hardened non-root ones + README update
This commit is contained in:
350
README.md
350
README.md
@@ -4,119 +4,301 @@ Exploitable ring only (outer Ø100, inner Ø60). Planet positions are determinis
|
||||
|
||||
> **Desktop only** — the galaxy map is not playable on phones or narrow screens (< 768 px wide).
|
||||
|
||||
## Layout
|
||||
---
|
||||
|
||||
The UI is split into two panes:
|
||||
## Table of contents
|
||||
|
||||
1. [Architecture](#architecture)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Deploy on a new server](#deploy-on-a-new-server)
|
||||
4. [Environment variables](#environment-variables)
|
||||
5. [Runtime config (`game.settings.json`)](#runtime-config-gamesettingsjson)
|
||||
6. [Local development (without Docker)](#local-development-without-docker)
|
||||
7. [UI layout](#ui-layout)
|
||||
8. [Game mechanics](#game-mechanics)
|
||||
9. [API reference](#api-reference)
|
||||
10. [Docker image notes](#docker-image-notes)
|
||||
11. [Database persistence](#database-persistence)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Web server / API | Node.js 20 + Express |
|
||||
| Game database | PostgreSQL 16 (`star_wars_grid`) |
|
||||
| Users database | PostgreSQL 16 (`star_wars_users`) |
|
||||
| Auth | JWT (7-day tokens, signed with `JWT_SECRET`) |
|
||||
| Config hot-reload | File mtime polling (`game.settings.json`) |
|
||||
| Economy engine | Server-side tick loop (every 5 s) |
|
||||
| Container runtime | Docker / Docker Compose |
|
||||
|
||||
The app runs as a single Node process that serves both the static frontend (`public/`) and the REST API (`/api/*`). Two separate PostgreSQL instances hold game state and user accounts respectively.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Minimum version |
|
||||
|-------------|----------------|
|
||||
| Docker Engine | 24+ |
|
||||
| Docker Compose plugin | v2 (`docker compose`) |
|
||||
| Git | any recent |
|
||||
|
||||
No Node.js or PostgreSQL installation is needed on the host — everything runs inside Docker.
|
||||
|
||||
---
|
||||
|
||||
## Deploy on a new server
|
||||
|
||||
### 1 — Clone the repository
|
||||
|
||||
```bash
|
||||
git clone <repo-url> star-wars-wild-space
|
||||
cd star-wars-wild-space
|
||||
```
|
||||
|
||||
### 2 — Create the environment file
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Open `.env` and set every value — see [Environment variables](#environment-variables) for details. At a minimum you **must** change:
|
||||
|
||||
- `JWT_SECRET` — generate a strong random string:
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
||||
```
|
||||
- `POSTGRES_PASSWORD`
|
||||
- `POSTGRES_USERS_PASSWORD`
|
||||
- `ADMIN_PASSWORD`
|
||||
|
||||
### 3 — Review the game config
|
||||
|
||||
`config/game.settings.json` is mounted into the container and hot-reloaded at runtime. Adjust values before first boot if needed — see [Runtime config](#runtime-config-gamesettingsjson).
|
||||
|
||||
### 4 — Create the data directories
|
||||
|
||||
Docker bind-mounts the PostgreSQL data directories from the host. Create them before the first run so that Docker does not create them as root:
|
||||
|
||||
```bash
|
||||
mkdir -p data/postgres data/postgres_users
|
||||
```
|
||||
|
||||
### 5 — Build and start
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
The `app` service waits for both databases to pass their health checks before starting (pg_isready polling every 3 s, up to 15 retries). On first boot the server automatically creates all required tables.
|
||||
|
||||
### 6 — Verify the deployment
|
||||
|
||||
```bash
|
||||
# Check all three containers are running
|
||||
docker compose ps
|
||||
|
||||
# Tail logs
|
||||
docker compose logs -f app
|
||||
|
||||
# Health check (should return JSON)
|
||||
curl http://localhost:8080/api/config
|
||||
```
|
||||
|
||||
Open `http://<server-ip>:8080` in a browser.
|
||||
|
||||
### 7 — (Optional) Reverse proxy
|
||||
|
||||
To expose the app on port 80/443, put Nginx or Caddy in front and proxy to `http://localhost:8080`. Set `CORS_ORIGIN` in `.env` to your public domain.
|
||||
|
||||
### Stopping / restarting
|
||||
|
||||
```bash
|
||||
docker compose down # stop (data preserved)
|
||||
docker compose down -v # stop and remove named volumes (data lost)
|
||||
docker compose restart app # restart only the app container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment variables
|
||||
|
||||
**Never commit secrets.** All credentials live in **`.env`** (git-ignored). Copy from `.env.example` before the first run.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `8080` | HTTP port exposed by the app |
|
||||
| `JWT_SECRET` | *(required)* | Secret used to sign JWT tokens — use a long random string in production |
|
||||
| `POSTGRES_USER` | `game` | Game-DB username |
|
||||
| `POSTGRES_PASSWORD` | *(required)* | Game-DB password |
|
||||
| `POSTGRES_DB` | `star_wars_grid` | Game-DB name |
|
||||
| `POSTGRES_USERS_USER` | `users` | Users-DB username |
|
||||
| `POSTGRES_USERS_PASSWORD` | *(required)* | Users-DB password |
|
||||
| `POSTGRES_USERS_DB` | `star_wars_users` | Users-DB name |
|
||||
| `CORS_ORIGIN` | `*` | Value of the `Access-Control-Allow-Origin` header — set to your public domain in production |
|
||||
|
||||
---
|
||||
|
||||
## Runtime config (`game.settings.json`)
|
||||
|
||||
Edit **`config/game.settings.json`** on the host (bind-mounted into the container at `/app/config/game.settings.json`). The server checks the file's **mtime** on a schedule; when it changes the new values are applied without a restart, within at most `configReloadIntervalSeconds` seconds (minimum 5 s).
|
||||
|
||||
### Core fields
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|-------|---------|---------|
|
||||
| `clickCooldownSeconds` | `10` | Per-user cooldown in seconds between new tile reveals. `0` disables it. |
|
||||
| `databaseWipeoutIntervalSeconds` | `21600` | World period length in seconds (6 h). The world seed is `swg-<slot>` where `slot = floor(UTC unix seconds / this value)`. When the slot advances, `grid_cells` is truncated and a new era begins. Minimum `60`. |
|
||||
| `configReloadIntervalSeconds` | `30` | How often the server polls the config file; also used by the client to poll `/api/config`. Minimum `5`. |
|
||||
|
||||
### Economy fields
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `elementWorth` | Per-element multipliers (`common`, `food`, `petrol`, `goods`, `industry`, `medic`, `money`, `science`). |
|
||||
| `resourceWorth.common` / `resourceWorth.rare` | Base income per resource type for common and rare planet tiers. Drives the server-side economy tick (every 5 s). |
|
||||
|
||||
`GET /api/config` returns the core fields plus **`worldSeed`**, **`seedPeriodStartsAtUtc`**, **`seedPeriodEndsAtUtc`**, and economy values.
|
||||
|
||||
---
|
||||
|
||||
## Local development (without Docker)
|
||||
|
||||
Requires two running PostgreSQL 16 instances, then:
|
||||
|
||||
```bash
|
||||
cp .env.example .env # fill in all values
|
||||
|
||||
npm install
|
||||
|
||||
# Set connection strings (PowerShell example)
|
||||
$env:DATABASE_URL = "postgres://game:game@localhost:5432/star_wars_grid"
|
||||
$env:USERS_DATABASE_URL = "postgres://users:users@localhost:5433/star_wars_users"
|
||||
$env:JWT_SECRET = "dev_secret_change_me"
|
||||
|
||||
npm start
|
||||
```
|
||||
|
||||
The server creates tables automatically on first run. Ensure `config/game.settings.json` exists (the file is included in the repository).
|
||||
|
||||
### Ports (local)
|
||||
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| App | `8080` (or `PORT` env var) |
|
||||
| Game Postgres | `5432` |
|
||||
| Users Postgres | `5433` |
|
||||
|
||||
---
|
||||
|
||||
## UI layout
|
||||
|
||||
| Pane | Content |
|
||||
|------|---------|
|
||||
| **Left column** | Title, team scores, player info, cooldown timer, world seed & period countdown, Refresh button, selection details |
|
||||
| **Right side** | Galaxy map — a perfect square anchored to the right edge of the viewport |
|
||||
|
||||
## Secrets & environment variables
|
||||
---
|
||||
|
||||
**Never commit secrets.** All credentials live in **`.env`** (git-ignored).
|
||||
Copy the template and fill in your values before the first run:
|
||||
## Game mechanics
|
||||
|
||||
```powershell
|
||||
cp .env.example .env
|
||||
# then edit .env
|
||||
### Teams (Résistance / Premier Ordre)
|
||||
|
||||
- Users register with a team (`Résistance` or `Premier Ordre`) that is locked to their account and embedded in their JWT.
|
||||
- **Your** discovered tiles use your team colour. Unclaimed ring tiles use the neutral idle tint.
|
||||
- Tiles discovered by the **other** team appear grey, show no planet, and are not clickable. The first reveal owns the tile (`discovered_by`).
|
||||
|
||||
### Cooldown
|
||||
|
||||
After revealing a **new** tile, a per-user cooldown starts. During cooldown you cannot reveal additional unseen tiles, but you can still click tiles your team has already discovered to view their stats. The effective cooldown is reduced by the team's element bonus:
|
||||
|
||||
```
|
||||
effectiveCooldown = clickCooldownSeconds / (1 + elementBonus / 100)
|
||||
```
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `JWT_SECRET` | Secret used to sign JWT tokens — use a long random string in production |
|
||||
| `POSTGRES_USER` | Game-DB username (default `game`) |
|
||||
| `POSTGRES_PASSWORD` | Game-DB password — **change in production** |
|
||||
| `POSTGRES_DB` | Game-DB name (default `star_wars_grid`) |
|
||||
| `POSTGRES_USERS_USER` | Users-DB username (default `users`) |
|
||||
| `POSTGRES_USERS_PASSWORD` | Users-DB password — **change in production** |
|
||||
| `POSTGRES_USERS_DB` | Users-DB name (default `star_wars_users`) |
|
||||
| `PORT` | HTTP port exposed by the app (default `8080`) |
|
||||
### World seed & wipe
|
||||
|
||||
## Runtime config (file + volume)
|
||||
The world seed changes automatically when the UTC period slot advances. On a seed change the server truncates `grid_cells` and starts a fresh era. Data in `users`, cooldown tables, econ scores, and victory points is kept across wipes.
|
||||
|
||||
Edit **`config/game.settings.json`** on the host (mounted into the container at `/app/config/game.settings.json`). The server reloads it when the file's **mtime** changes, on a schedule controlled by **`configReloadIntervalSeconds`** (minimum 5 s), so frequent polling is avoided when nothing changed.
|
||||
### Economy
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `clickCooldownSeconds` | Cooldown between **new** reveals (same as before). |
|
||||
| `databaseWipeoutIntervalSeconds` | World period length in **seconds** (default `21600` = 6 h). The **world seed** is `swg-<slot>` with `slot = floor(UTC unix seconds / this value)`. When the slot changes, **`grid_cells` is truncated** (full wipe). |
|
||||
| `debugModeForTeams` | If `true`, the **Blue / Red** segmented control is shown; if `false`, it is hidden. |
|
||||
| `configReloadIntervalSeconds` | How often the server **checks** the config file (mtime); also used by the client to poll `/api/config`. |
|
||||
A server-side tick runs every 5 seconds. It reads the current grid from the database, computes income and element bonus for each team from their discovered planets, and persists the deltas. No browser needs to be connected for the economy to progress.
|
||||
|
||||
`GET /api/config` returns these values plus **`worldSeed`**, **`seedPeriodEndsAtUtc`**, **`seedPeriodStartsAtUtc`**.
|
||||
---
|
||||
|
||||
## Teams (blue / red)
|
||||
## API reference
|
||||
|
||||
- With **`debugModeForTeams`**: use the **Team** control (top-left) to switch perspective.
|
||||
- **Your** discovered tiles use your team colour. Unclaimed ring tiles use the classic idle tint for both teams.
|
||||
- Tiles discovered by the **other** team appear **grey**, show **no planet**, and are **not clickable**. First reveal **owns** the tile (`discovered_by`).
|
||||
All endpoints are relative to the app root.
|
||||
|
||||
## Cooldown
|
||||
### Public
|
||||
|
||||
After revealing a **new** tile, a **cooldown** runs (left column). During cooldown you **cannot reveal additional unseen tiles**, but you can still **click tiles your team already discovered** to view their stats.
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/config` | Game config plus current world seed and period timestamps. Accepts optional `?team=blue|red` and Bearer token to compute per-user cooldown remaining. |
|
||||
| `GET` | `/api/grid/:seed` | All revealed cells for `seed`. Returns `410` if `seed` is not the current world seed. |
|
||||
| `GET` | `/api/scores` | `{ blue: N, red: N }` — tile counts for the current period. |
|
||||
| `GET` | `/api/econ-scores` | Economic score totals for the current period. |
|
||||
| `GET` | `/api/element-bonus` | Current element bonus percentages per team. |
|
||||
| `GET` | `/api/victory-points` | All awarded victory points across eras. |
|
||||
| `GET` | `/api/db-info` | Database creation timestamp. |
|
||||
|
||||
## Run with Docker Compose (Node + PostgreSQL)
|
||||
### Auth (no token required)
|
||||
|
||||
### Prerequisites
|
||||
| Method | Path | Body | Response |
|
||||
|--------|------|------|----------|
|
||||
| `POST` | `/api/auth/register` | `{ username, email, password, team }` | `{ token, user }` |
|
||||
| `POST` | `/api/auth/login` | `{ username, password }` | `{ token, user }` |
|
||||
|
||||
1. Copy the environment template and set your secrets:
|
||||
```powershell
|
||||
cp star_wars_grid_game/.env.example star_wars_grid_game/.env
|
||||
# Edit .env — at minimum change JWT_SECRET, POSTGRES_PASSWORD, POSTGRES_USERS_PASSWORD
|
||||
```
|
||||
### Auth (Bearer token required)
|
||||
|
||||
2. From `star_wars_grid_game/`:
|
||||
```powershell
|
||||
docker compose up --build
|
||||
```
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/auth/me` | Returns refreshed `{ token, user }`. |
|
||||
| `POST` | `/api/cell/reveal` | Body `{ seed, x, y }`. Team is taken from the JWT. First reveal wins; **`409`** if the other team already owns the tile; **`410`** if seed is stale; **`429`** if on cooldown. |
|
||||
|
||||
3. Open `http://localhost:8080`.
|
||||
### Auth field rules
|
||||
|
||||
### Ports
|
||||
| Field | Rules |
|
||||
|-------|-------|
|
||||
| `username` | 2–32 characters |
|
||||
| `password` | Minimum 6 characters |
|
||||
| `team` | `"Résistance"` or `"Premier Ordre"` |
|
||||
| `email` | Must be unique |
|
||||
|
||||
| Service | Port | Notes |
|
||||
|---------|------|-------|
|
||||
| App | `8080` | configurable via `PORT` in `.env` |
|
||||
| Game Postgres | `5432` | user/pass from `.env` |
|
||||
| Users Postgres | `5433` | user/pass from `.env` |
|
||||
Tokens are valid for **7 days**. Call `GET /api/auth/me` to silently renew.
|
||||
|
||||
### Database persistence
|
||||
|
||||
PostgreSQL data is under **`./data/postgres`** and **`./data/postgres_users`** (bind mounts). The **world wipe** only clears `grid_cells` when the UTC period slot changes; it does not delete the Postgres data directory.
|
||||
|
||||
## Local dev (without Docker)
|
||||
|
||||
Requires PostgreSQL, then:
|
||||
|
||||
```powershell
|
||||
cd star_wars_grid_game
|
||||
cp .env.example .env # fill in DATABASE_URL etc.
|
||||
npm install
|
||||
# export vars or use a tool like dotenv-cli
|
||||
$env:DATABASE_URL="postgres://game:game@localhost:5432/star_wars_grid"
|
||||
$env:USERS_DATABASE_URL="postgres://users:users@localhost:5433/star_wars_users"
|
||||
$env:JWT_SECRET="dev_secret"
|
||||
npm start
|
||||
```
|
||||
|
||||
Ensure `config/game.settings.json` exists (or copy from the repo).
|
||||
---
|
||||
|
||||
## Docker image notes
|
||||
|
||||
- Base image: `node:20-alpine`
|
||||
- Runs as a **non-root user** (`appuser`) for security
|
||||
- `NODE_ENV=production` is set inside the image
|
||||
- Built-in **health check**: polls `GET /api/config` every 15 s (`wget`)
|
||||
- Secrets (`JWT_SECRET`, DB passwords) are **never baked into the image** — pass them via `.env` / Compose environment
|
||||
- Build uses a **two-stage** Dockerfile:
|
||||
- Stage 1 (`node:20-alpine`): installs production dependencies via `npm ci`.
|
||||
- Stage 2 (`gcr.io/distroless/nodejs20-debian12:nonroot`): minimal runtime with no shell or package manager; runs as UID 65532.
|
||||
- `NODE_ENV=production` is set inside the image.
|
||||
- Built-in health check: calls `server/healthcheck.js` via the Node binary every 15 s (no `wget`/`curl` — distroless has none).
|
||||
- Secrets are **never baked into the image** — always passed via `.env` / Compose environment.
|
||||
|
||||
## API
|
||||
---
|
||||
|
||||
- `GET /api/config` — cooldown, wipe interval, debug flag, poll interval, `worldSeed`, period timestamps.
|
||||
- `GET /api/grid/:seed` — cells for that seed; **`410`** if `seed` is not the current world seed.
|
||||
- `POST /api/cell/reveal` — body `{ seed, x, y, team: "blue" | "red" }` — first reveal wins; **`409`** if the other team owns the tile; **`410`** if seed is stale.
|
||||
- `POST /api/auth/register` — `{ username, email, password, team }` → `{ token, user }`.
|
||||
- `POST /api/auth/login` — `{ username, password }` → `{ token, user }`.
|
||||
- `GET /api/auth/me` — Bearer token required → refreshed `{ token, user }`.
|
||||
- `GET /api/scores` — `{ blue: N, red: N }` tile counts for the current period.
|
||||
## Database persistence
|
||||
|
||||
PostgreSQL data lives in bind-mount directories on the host:
|
||||
|
||||
| Directory | Database |
|
||||
|-----------|----------|
|
||||
| `./data/postgres` | Game DB (`star_wars_grid`) |
|
||||
| `./data/postgres_users` | Users DB (`star_wars_users`) |
|
||||
|
||||
A **world wipe** only truncates the `grid_cells` table when the UTC period slot changes; it does not touch the host data directories. Users, cooldowns, econ scores, and victory points survive wipes.
|
||||
|
||||
To fully reset the game state (including all user accounts), stop the stack and delete both data directories, then start again:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
rm -rf data/postgres data/postgres_users
|
||||
mkdir -p data/postgres data/postgres_users
|
||||
docker compose up -d
|
||||
```
|
||||
Reference in New Issue
Block a user