Steam Authentication Architecture
The steam-auth service runs in a separate container to isolate Steam credentials from the game server. It handles:
- Steam authentication and session management
- Game file downloads from Steam depots
- Encrypted app ticket generation for GOG Galaxy cross-platform multiplayer
Architecture Diagram
┌─────────────────────────────────────┐
│ Game Server Container │
│ │
│ ┌──────────────────────────────┐ │
│ │ AuthService.cs │ │
│ │ (SteamAppTicketFetcherHttp) │ │
│ └──────────┬───────────────────┘ │
│ │ HTTP GET │
│ │ /steam/app-ticket │
└─────────────┼──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Steam Auth Container │
│ │
│ ┌──────────────────────────────┐ │
│ │ HTTP Server (port 3001) │ │
│ │ /health, /steam/app-ticket │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ SteamKit2 │ │
│ │ (login, 2FA, downloads) │ │
│ └──────────────────────────────┘ │
│ │
│ Volumes: │
│ - steam-session (refresh tokens) │
│ - game-data (shared with server) │
└─────────────────────────────────────┘Security Benefits
- Credential Isolation: Steam username and password never leave the steam-auth container
- Token Security: Refresh tokens remain private in a dedicated volume
- Minimal Exposure: Only ephemeral encrypted app tickets are exposed to the game server
- Shared Game Data: Game files are downloaded once and shared via volume
API Endpoints
The steam-auth HTTP API exposes:
GET /health
Health check endpoint.
{
"status": "ok",
"logged_in": true,
"timestamp": "2026-01-16T12:00:00.000Z"
}GET /steam/app-ticket
Get an encrypted app ticket for GOG Galaxy authentication.
{
"app_ticket": "base64-encoded-ticket",
"steam_id": "76561198012345678"
}Available Commands
The steam-auth service supports several commands:
| Command | Description |
|---|---|
setup | Interactive login + download game (first-time setup) |
login | Interactive login only, saves session |
download | Download/update game files (uses saved session) |
ticket | Output encrypted app ticket to stdout |
export-token | Export saved refresh token for CI use |
serve | Run HTTP API for runtime ticket requests (default) |
Token Persistence
Refresh tokens are saved to /data/steam-session/session-{username}.json and reused on container restart. Steam tokens typically last 200 days.
How Invite Codes Work
- Game server starts hosting via GOG Galaxy SDK
- Galaxy creates a lobby and requests an encrypted app ticket
- Game server's
AuthServicefetches ticket from steam-auth via HTTP - Steam-auth uses SteamKit2 to get an encrypted app ticket from Steam
- Ticket is returned and used to generate an invite code with "G" prefix
File Filtering
The download process skips unnecessary files to reduce download size:
- Large audio files (Wave Bank.xwb ~370MB) — always stripped (the server runs silent)
- Non-English language files (~50MB) — stripped by default; opt back in per language with
STEAM_KEEP_LANGUAGES(see below) - Other assets not needed for dedicated server operation
This reduces the download from ~1.5GB to ~600MB.
After filtering, Content/ContentHashes.json is pruned to list only the files that were actually downloaded. The game checks this manifest (not the filesystem) to decide whether an asset exists, so leaving a stripped file listed would make the game attempt to load a missing .xnb and throw ContentLoadException. Pruning keeps the manifest honest so the game's built-in localized→English font fallback works.
Keeping a language's fonts
By default every localized font/content file is stripped and the server falls back to the English font, so non-Latin chat (e.g. Cyrillic, CJK) renders as empty boxes on clients. To keep a language's fonts in the download — so that language renders correctly — set STEAM_KEEP_LANGUAGES to a comma-separated list of language codes:
STEAM_KEEP_LANGUAGES=pt-BR,ru-RUValid codes: de-DE, es-ES, fr-FR, hu-HU, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, tr-TR, zh-CN, th-TH. CJK codes (zh-CN, ja-JP, ko-KR) also keep their larger font families, so those add more to the image size than the others. An unrecognized code is logged as a warning and ignored.
CI/CD Usage
For automated builds, use a refresh token instead of interactive login:
# Export token after local setup
docker compose run steam-auth export-token > token.json
# Use in CI (set as secret)
STEAM_REFRESH_TOKEN=xxx STEAM_USERNAME=user docker compose run steam-auth downloadEnvironment Variables
Steam Auth Container
| Variable | Description | Default |
|---|---|---|
STEAM_USERNAME | Steam account username | (prompted) |
STEAM_PASSWORD | Steam account password | (prompted) |
STEAM_REFRESH_TOKEN | Pre-existing refresh token (for CI) | - |
PORT | HTTP server port | 3001 |
SESSION_DIR | Token storage directory | /data/steam-session |
GAME_DIR | Game files directory | /data/game |
FORCE_REDOWNLOAD | Set to "1" to re-download all files | - |
STEAM_KEEP_LANGUAGES | Comma-separated language codes whose fonts to keep in the download (e.g. pt-BR,ru-RU). Default strips all localized content (English-only). | - |
Game Server Container
| Variable | Description | Default |
|---|---|---|
STEAM_AUTH_URL | URL of steam-auth service | http://steam-auth:3001 |