WinQuake modernized for the cloud — the classic Quake engine running as a headless container on Azure Container Apps, with browser-based WebRTC streaming so anyone can play from a URL.
This project was built using the spec2cloud AI-powered workflow, taking WinQuake from a 1996 Win32 desktop binary to a containerized, cloud-native game streaming platform on Azure.
Browser (WebRTC)
│
▼
┌──────────────────┐ ┌──────────────────┐
│ Streaming Gateway│────▶│ Game Worker │
│ (Go, :8090) │ │ (Headless Quake) │
│ WebSocket signal │ │ RGBA framebuffer │
│ + HTML client │ │ :8080 /healthz │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Session Manager │ │ Telemetry API │
│ (Go, :8080) │ │ (Go, :8060) │
│ POST/GET/DELETE │ │ → App Insights │
│ /api/sessions │ │ /api/events │
└──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ Assets API │
│ (Go, :8070) │
│ PAK file server │
│ /api/assets/ │
└──────────────────┘
Azure Resources provisioned via Bicep:
| Resource | Purpose |
|---|---|
| Azure Container Registry | Stores container images |
| Azure Container Apps Environment | Hosts all containers |
| Game Worker Container App | Headless Quake engine (0→N replicas) |
| Azure Files | Game data (id1/ directory) share |
| Key Vault | Secrets management (RBAC auth) |
| Application Insights + Log Analytics | Monitoring and telemetry |
- Azure subscription — Create a free account
- Azure CLI (v2.60+) — Install
- Docker — Install
- Quake game data — You need a licensed
id1/directory containingpak0.pak(shareware or full)
git clone https://github.com/kensondesu/quake-spec2cloud.git
cd quake-spec2cloud
az login
az account set --subscription "<YOUR_SUBSCRIPTION_ID>"az group create --name rg-quake-dev --location eastusThis provisions all resources (ACR, Container Apps Environment, Blob Storage, Key Vault, App Insights, a user-assigned managed identity with RBAC roles, and the game-worker Container App). Game data is stored in a blob container and downloaded into the container at startup by an init container—no storage account keys are used anywhere.
Note: The Bicep template creates a user-assigned managed identity with Storage Blob Data Reader (on the storage account) and AcrPull (on ACR). These role assignments are completed before the container app is created, eliminating manual RBAC steps.
az deployment group create \
--resource-group rg-quake-dev \
--name main \
--template-file infra/main.bicep \
--parameters infra/parameters/dev.bicepparamCapture the outputs — you'll need them in the next steps:
# Get deployment outputs
ACR_LOGIN_SERVER=$(az deployment group show \
--resource-group rg-quake-dev \
--name main \
--query properties.outputs.acrLoginServer.value -o tsv)
STORAGE_ACCOUNT=$(az deployment group show \
--resource-group rg-quake-dev \
--name main \
--query properties.outputs.storageAccountName.value -o tsv)
GAME_WORKER_FQDN=$(az deployment group show \
--resource-group rg-quake-dev \
--name main \
--query properties.outputs.gameWorkerFqdn.value -o tsv)
echo "ACR: $ACR_LOGIN_SERVER"
echo "Storage: $STORAGE_ACCOUNT"
echo "Game Worker: $GAME_WORKER_FQDN"Upload your Quake id1/ directory (containing pak0.pak) to the gamedata blob container:
# Assign yourself Storage Blob Data Contributor on the storage account
CURRENT_USER=$(az ad signed-in-user show --query id -o tsv)
STORAGE_ID=$(az storage account show \
--name "$STORAGE_ACCOUNT" \
--resource-group rg-quake-dev \
--query id -o tsv)
az role assignment create \
--assignee "$CURRENT_USER" \
--role "Storage Blob Data Contributor" \
--scope "$STORAGE_ID"
# Wait a moment for the role assignment to propagate, then upload
az storage blob upload-batch \
--account-name "$STORAGE_ACCOUNT" \
--auth-mode login \
--destination gamedata \
--source /path/to/your/id1# Log in to ACR
az acr login --name "${ACR_LOGIN_SERVER%%.*}"
# Build and push the headless Quake worker
docker build -t "$ACR_LOGIN_SERVER/quake-worker:latest" .
docker push "$ACR_LOGIN_SERVER/quake-worker:latest"Tip: You can also use ACR Tasks to build in the cloud without a local Docker install:
az acr build \ --registry "${ACR_LOGIN_SERVER%%.*}" \ --image quake-worker:latest .
The Bicep deploy created the container app with minReplicas: 0 (no pods running). Scale it up to start the game:
az containerapp update \
--name ca-game-worker-dev \
--resource-group rg-quake-dev \
--min-replicas 1The init container will download game data from blob storage using the managed identity, then the game worker starts.
The four Go microservices each have their own Dockerfile under services/:
# Build and push all services
for svc in streaming-gateway session-manager assets-api telemetry-api; do
docker build -t "$ACR_LOGIN_SERVER/$svc:latest" "services/$svc"
docker push "$ACR_LOGIN_SERVER/$svc:latest"
doneTo deploy them as additional Container Apps, create matching Bicep modules or deploy via the CLI:
az containerapp create \
--name ca-streaming-gateway-dev \
--resource-group rg-quake-dev \
--environment cae-quake-dev \
--image "$ACR_LOGIN_SERVER/streaming-gateway:latest" \
--target-port 8090 \
--ingress external \
--min-replicas 1 \
--max-replicas 3 \
--registry-server "$ACR_LOGIN_SERVER" \
--registry-identity system# Verify the game-worker is healthy
az containerapp show \
--name ca-game-worker-dev \
--resource-group rg-quake-dev \
--query "{Status:properties.runningStatus, FQDN:properties.configuration.ingress.fqdn}" \
-o tableaz containerapp logs show \
--name ca-game-worker-dev \
--resource-group rg-quake-dev \
--followYou should see log output like:
quake-worker: JSON log: engine initialized
quake-worker: JSON log: health server listening on :8080
quake-worker: JSON log: map=e1m1 skill=1
If you deployed the streaming gateway with --ingress external:
# Get the gateway URL
GATEWAY_FQDN=$(az containerapp show \
--name ca-streaming-gateway-dev \
--resource-group rg-quake-dev \
--query properties.configuration.ingress.fqdn -o tsv)
echo "Open in your browser: https://$GATEWAY_FQDN"Navigate to that URL — the embedded HTML client loads a <canvas> and establishes a WebRTC connection to the game worker. You'll see the Quake game rendered in your browser with keyboard and mouse input forwarded to the engine.
# Open Application Insights in the Azure portal
az monitor app-insights component show \
--app appi-quake-dev \
--resource-group rg-quake-dev \
--query "{Name:name, InstrumentationKey:instrumentationKey}" \
-o tableOr open the Azure Portal → Application Insights → appi-quake-dev to see live metrics, traces, and telemetry events.
For local development and testing without Azure:
# Build the headless game worker
docker build -t quake-worker:local .
# Run with your local game data
docker run -it --rm \
-p 8080:8080 \
-v /path/to/your/id1:/game/id1:ro \
-e QUAKE_MAP=e1m1 \
-e QUAKE_SKILL=1 \
quake-worker:localCheck health: curl http://localhost:8080/healthz
The game worker is configured via environment variables:
| Variable | Default | Description |
|---|---|---|
QUAKE_BASEDIR |
/game |
Base directory for game data |
QUAKE_MAP |
e1m1 |
Starting map |
QUAKE_SKILL |
1 |
Difficulty (0=Easy, 1=Normal, 2=Hard, 3=Nightmare) |
QUAKE_MEM_MB |
32 |
Engine memory allocation |
QUAKE_WIDTH |
640 |
Render width in pixels |
QUAKE_HEIGHT |
480 |
Render height in pixels |
├── WinQuake/ # Modernized Quake engine source (C)
│ ├── CMakeLists.txt # CMake build (-DHEADLESS=ON -DNOASM=ON)
│ ├── vid_headless.c # Headless video driver (framebuffer capture)
│ ├── snd_capture.c # Audio capture driver
│ ├── in_inject.c # Input injection (keyboard/mouse from network)
│ └── sys_container.c # Container system layer (JSON logging, /healthz)
├── Dockerfile # Multi-stage build for quake-worker container
├── infra/ # Azure Bicep infrastructure-as-code
│ ├── main.bicep # Root template
│ ├── modules/ # ACR, Container Apps, Storage, Key Vault, etc.
│ └── parameters/dev.bicepparam
├── services/ # Go microservices
│ ├── streaming-gateway/ # WebSocket signaling + WebRTC + browser client
│ ├── session-manager/ # Game session lifecycle REST API
│ ├── assets-api/ # PAK file content server
│ └── telemetry-api/ # Event ingestion → Application Insights
└── .github/workflows/
└── build.yml # CI: build headless worker + static analysis
The GitHub Actions workflow in .github/workflows/build.yml runs on every push to main:
- Builds the headless Quake worker binary
- Runs
cppcheckstatic analysis - Uploads the binary as a build artifact
Remove all Azure resources when you're done:
az group delete --name rg-quake-dev --yes --no-waitSee LICENSE.md.
Contributions welcome! See docs/contributing.md.
Built with spec2cloud — from legacy code to cloud in minutes.
