A cost-efficient multi-game dedicated server platform on AWS Fargate with a local web UI and a fully serverless Discord bot. Servers only run — and only cost money — while someone is playing.
📚 Full documentation lives at codercoco.github.io/game-server-deploy (built from
docs/by GitHub Pages). The rest of this README is a quick tour; deep-dives, setup steps, and architecture diagrams are on the site.
- AWS Fargate — runs game server containers on-demand via
RunTask(no persistent ECS Service, no idle costs). - EFS — persists world saves across server restarts, one access point per game.
- Route 53 — a Lambda auto-UPSERTs
{game}.yourdomain.comon task start and DELETEs it on stop. - Optional ALB + ACM — for any game marked
https = true, traffic goes through a load balancer with TLS termination. - Watchdog Lambda — automatically shuts down idle servers based on
NetworkPacketsIn. - Terraform — provisions every AWS resource.
- Nest.js + React management app — local dashboard to start/stop servers, edit config, monitor costs, stream logs, and manage Discord credentials.
- Serverless Discord bot — two Node.js Lambdas + DynamoDB + Secrets
Manager serve Discord HTTP interactions. Permitted Discord users/roles
can
/server-start,/server-stop,/server-status, and/server-listfrom chat without any 24/7 process running.
The docs site is organised around three roles. Pick the one that matches what you need to do.
| Guide | You are… |
|---|---|
| Setup guide | Going from a blank AWS account to a running Fargate task. |
| User guide | Driving an already-provisioned deployment — the dashboard, Discord commands, day-to-day ops. |
| Maintainer guide | Working on this codebase. |
| Private parent + submodule guide | Wrapping this repo in a private repo that holds terraform.tfvars and tfstate. Includes an interactive scaffolder (scripts/init-parent.ts) that generates the wrapper Makefile, tfvars, and .env. |
Component deep-dives:
- Architecture — full diagram +
/server-startsequence. - Terraform — every
.tffile, variables, outputs, gotchas. - Management app — Nest.js API, React dashboard,
@gsd/shared. - Lambdas — interactions, followup, update-dns, watchdog.
# 1. First-time bootstrap (installs node/terraform/aws CLI on Debian/Ubuntu,
# npm-installs all workspaces, builds Lambda bundles, runs terraform init)
chmod +x setup.sh && ./setup.sh
# 2. Configure
$EDITOR terraform/terraform.tfvars # game_servers, hosted_zone_name, ...
# 3. Deploy infra
cd terraform && terraform apply
# 4a. Run the management app in dev mode
cd .. && npm run app:dev
# http://localhost:5173 (Nest on :3001, Vite proxy)
# 4b. …or in Docker (production mode — requires a bearer token)
cd ..
touch app/server_config.json
export API_TOKEN="$(openssl rand -hex 32)"
docker compose up --build
# http://localhost:5000 (dashboard prompts for $API_TOKEN)See the setup guide for the full walkthrough, including the IAM policy, Discord bot setup, and troubleshooting.
Edit terraform/terraform.tfvars. The game_servers map is the single
source of truth — task definitions, EFS access points, DNS, watchdog config,
and Discord command autocomplete all derive from it.
aws_region = "us-east-1"
project_name = "game-servers"
hosted_zone_name = "yourdomain.com" # must exist in Route 53
game_servers = {
palworld = {
image = "thijsvanloef/palworld-server-docker:latest"
cpu = 2048
memory = 8192
ports = [
{ container = 8211, protocol = "udp" },
{ container = 27015, protocol = "udp" },
]
environment = [
{ name = "PLAYERS", value = "8" },
{ name = "SERVER_NAME", value = "My Palworld Server" },
]
efs_path = "/palworld"
https = false
}
}Cost ballpark: Fargate 2 vCPU / 8 GB ≈ $0.12/hr while running; EFS is pennies/month. Playing 4 hours/day, 5 days/week ≈ $10–12/month, vs. ~$60/month for a 24/7 t3.large.
game-server-deploy/
├── app/ # Nest.js + React monorepo (npm workspaces)
│ └── packages/
│ ├── shared/ # @gsd/shared
│ ├── server/ # @gsd/server (Nest.js API)
│ ├── web/ # @gsd/web (React + Vite)
│ └── lambda/
│ ├── interactions/ # Discord Function URL entry point
│ ├── followup/ # Async ECS work + Discord PATCH
│ ├── update-dns/ # Route 53 + ALB on task state change
│ └── watchdog/ # Idle detection + auto-stop
├── terraform/ # All AWS infra (VPC, ECS, EFS, 4 Lambdas, DDB…)
├── docs/ # Documentation site (published via GH Pages)
├── Dockerfile
├── docker-compose.yml
├── setup.sh # First-time bootstrap (node/terraform/aws)
├── scripts/ # Helper scripts (init-parent.ts scaffolder)
├── CLAUDE.md # Project instructions + invariants
├── CONTRIBUTING.md # PR conventions, local checks
└── README.md # this file
Stop every server from the dashboard first (so the update-dns Lambda cleans its records), then:
cd terraform && terraform destroyThe two Discord Secrets Manager secrets use recovery_window_in_days = 0,
so they are deleted immediately and a later apply is clean.
See LICENSE.