Personal project / Solo developer
Nodewake
Factorio hosting that starts a server when a player connects, then saves, stops, and verifies a backup when the server sits empty.
Docker & AWS
I split the platform into separate containers, with a worker managing the game’s lifecycle.
Restrict Docker host access
- Why
- Creating game containers requires powerful host privileges. I kept those privileges out of the web API and public relay to limit what a compromised service could do.
- Built
- The worker calls a private host agent; only the socket proxy mounts the Docker socket. The agent constructs the permitted container configuration before calling Docker through the proxy.
- Ownership and epoch checks reject unmanaged containers and stale worker requests. The agent restricts capabilities and sets resource and log limits; the proxy separately limits available Docker API endpoints.
- Result
- The public API and relay can request lifecycle changes without access to arbitrary host workloads. Container configuration and host operations stay behind the agent’s checks and the proxy’s permitted endpoints.
Separate infrastructure from the game lifecycle
- Why
- A game session needs readiness checks, save commands, and a shutdown sequence in addition to container orchestration.
- Built
- Amazon ECS runs the cloud containers; Terraform defines their infrastructure.
- Nodewake’s Python services decide when a game is needed, check whether players can join, and coordinate saves and shutdown. Manual starts and player-triggered wakes use that same lifecycle.
- Result
- Infrastructure definitions can change separately from game-session behavior. The game’s save protocol and readiness rules stay in the application code rather than becoming part of provisioning logic.
Inspect the Docker deployment 11 containers · connections & permissions
- Blue: control / state
- Orange: player UDP
- Cream: Docker host operations
- Solid: containers · dashed: clients / host process
Core runtime only. Monitoring, build images, migration jobs, and Grafana’s launcher are omitted.
Startup & worker failover
Manual starts and player connections share one control path.
Reconcile starts through PostgreSQL
- Why
- Starting containers directly from the relay would duplicate API logic and require broader host permissions. Worker handovers also need to prevent an old leader from continuing to make changes.
- Built
- PostgreSQL stores the desired state. The API records requests; the relay uses a gate whose database role can call only wake and status functions. One worker compares that state with the running game and asks the host agent to make changes.
- A leader lock selects the worker. Increasing leadership versions (epochs) let the database and host agent reject stale work. The lock selects who should lead; the epoch checks fence off an old leader.
- Result
- Manual starts and player connections use the same startup logic. Tests cover a worker losing leadership during a pass and refusing to commit its transition; the relay’s credentials do not grant general session-management access.
Keep web controls outside the runtime
- Why
- An owner needs to sign in and start games without giving web services Docker host privileges.
- Built
- Caddy serves the static TypeScript/Next.js frontend and routes API requests to FastAPI.
- Authelia handles sign-in and a second factor. An owner pressing Start reaches the same database and worker path as a player connecting.
- Result
- Owner controls stay separate from privileged runtime operations. Authentication, web serving, and the API request changes through the control plane; the host agent performs the container operations.
Check when the game can accept players
- Why
- A container can be running while the map is still loading. Process startup alone does not mean a player can join.
- Built
- The worker verifies hosting state and RCON (remote console) responses before recording readiness.
- The relay watches for the first UDP reply and starts forwarding immediately, independently of the worker’s checks.
- Result
- Players can connect as soon as the game responds, without waiting for the control plane’s next polling interval. The worker still records readiness only after its game checks pass.
Handling cold starts
Factorio can stop sending join requests before a server finishes loading.
Put Quilkin at the public edge
- Why
- I chose a maintained, memory-safe public UDP proxy while keeping custom game logic in Python.
- Built
- Quilkin owns the public game port and forwards UDP traffic to the Python relay over a private network.
- The relay owns join validation and wake requests, along with buffering and replay. It asks the restricted gate to wake the game; Quilkin forwards packets without taking on game-management logic.
- Result
- The public proxy has no route to the management API or Docker agent. Its network exposure and packet-forwarding role remain separate from the custom relay’s game-specific behavior.
Buffer join packets through startup
- Why
- Factorio’s client sends join requests early, then waits. Those requests would otherwise be lost while a stopped server is starting.
- Built
- The asyncio relay buffers a limited number of packets per player and replays them every 250 ms until the game answers. It then becomes a normal forwarder.
- Join validation, per-source cooldowns, and an hourly wake cap limit unwanted starts from stray or repeated traffic.
- Result
- In the recorded small-save test, the client joined a stopped server in about three seconds with one Connect action. Larger saves can exceed the client’s ten-second window and need another attempt.
Factorio 2.0.77 with a small save. Larger saves can exceed the client’s 10-second window and need another connection attempt.
Backups & operations
An empty server shuts down with a verified local backup. Restores preserve the current world first.
Verify backups and protect restores
- Why
- Stopping a process does not confirm that a usable save exists. Restoring an older backup also needs to preserve the current world in case the older save is wrong.
- Built
- Verified shutdown: warn players, save the game, stop the container, create a local backup, and verify it. Integration tests cover a save, backup, restore, and boot round trip with Factorio and local storage.
- Archive before restore: Nodewake keeps the current save before replacement. If backup verification fails, the live world stays untouched rather than being partially replaced.
- Result
- Tampered backups leave the live save untouched. A completed stop produces a verified backup, and the pre-restore world remains available after a restore.
Monitor failed wakes and proxy overload
- Why
- A proxy can keep running while rejecting new sessions. A start request can also fail before the game responds.
- Built
- Prometheus scrapes private metrics from the API, worker, relay, and Quilkin. Grafana displays them, and Alertmanager routes notifications.
- Alerts cover proxy availability, rejected sessions, slow starts, and an unreachable gate. Resource limits and restart/direct-play runbooks address known failure paths.
- Result
- An operator can distinguish gate failures, proxy overload, and a game that has not answered. The runbooks cover restarting the proxy or connecting directly when the normal edge path is unavailable.
A flood can fill Quilkin’s session table and require a proxy restart; the runbook covers recovery and direct connections.
Testing the failure paths
The repository’s unit and integration tests cover startup, permissions, worker handover, and recovery.
- Relay behavior. Replay buffered packets once the game responds, reject invalid wake traffic, and avoid duplicate wakes when another player joins.
- Permissions. Reject unmanaged containers and unapproved mounts. Check the gate’s database role, container capabilities, and resource limits.
- Worker handover. Allow one leader at a time. Reject state writes from stale epochs or workers that lose leadership mid-pass.
- Recovery. Save, back up, restore, and boot a game. Reject a tampered backup without changing the live save.