Spin up a cheap EC2 Spot instance to host a Minecraft server managed by Discord chat.
Made for a personal and small server with few players.
- Strategy
- Workflow
- Diagram
- Cost breakdown
- Prerequisites
- Setup
- Testing and troubleshooting
- To-do
- Notes and acknowledgements
The idea is reduce costs by basically:
- Start the server only when players want to play
- Automatically stop the server when there are no players
- Avoid paying for a domain/etc by using a DDNS service
- Using spot instances
This is achieved by:
- Starting the server via Discord slash commands interactions
- Slash commands work via webhook which don't require a Discord bot running 24/7, so we can use AWS Lambda + Lambda Function URL (GCP Cloud Run could work too)
- Using the Auto-stop feature from itzg/docker-minecraft-server (a plugin like vincss/mcEmptyServerStopper could work too) alongside a systemd timer
- Setting up Duck DNS inside the instance (No-IP could work too)
The process works as follow:
- The player types
/start
in a Discord server text channel - Discord calls our Lambda function via its Function URL
- The Lambda function sends the interaction token alongside the
start
command to another Lambda via SNS and then ACKs the interaction to avoid Discord's 3s interaction response time limit. - The other Lambda which can take its time, and starts the EC2 instance. Other commands such as
stop
,restart
andstatus
can stop, reboot and describe the instance. - The instance starts
- The Duck DNS systemd service updates the domain with the new IP (this can take a while to use due to DNS caching)
- The Minecraft systemd service runs the Docker Compose file to start the server
- The Minecraft shutdown systemd timer starts checking if the container is running
- After a minute or so, the server is ready to connect and play
- After 10 minutes without a connection or after the last player disconnects, the server is shutdown automatically via the Auto-stop feature.
- After a minute or less, the Minecraft shutdown timer/service sees that the server is shutdown and shuts down the instance itself.
TL;DR: Between 0.4 to 1.1 USD for 30h of gameplay for 2 vCPU with 2.5GHz and 8GB RAM
- AWS Princing Calculator estimate
- Does not include Public IP cost, see table below
- Last updated: June 2024
- Region assumed is
us-east-2
(Ohio) - Prices are in USD
- Assumes usage of Always Free monthly offers (different from 12 month Free Tier)
- This is important mostly due to the monthly free 100GB outbound data transfer from EC2 to the internet. (See also blog post) Otherwise due to the current price rates and regular gameplay network usage, it would cost more than the instance itself
- For the EC2 prices (in the table below), keep in mind about:
- Surplus vCPU usage credits charges when using burstable instances in unlimited mode (default). See Earn CPU credits and When to use unlimited mode versus fixed CPU.
- Basically, don't play at heavy CPU usage continuously for TOO long when using those instances types.
- Spot prices change:
- With time
- Depending on the selected availability zone. See Spot Instance pricing history in "AWS Console > EC2 > Instances > Spot Requests > Pricing History" to see if this variation is significant and to choose the current best availability zone for you.
- You can always change the instance type, don't forget to change the other related Terraform variables!
- Surplus vCPU usage credits charges when using burstable instances in unlimited mode (default). See Earn CPU credits and When to use unlimited mode versus fixed CPU.
Service | Sub-service / description | Price/hour | Price 30h/month |
---|---|---|---|
EC2 | t4g.large spot instance |
$0.02379 | $0.531 |
EBS | 10GB volume for Minecraft data | $0.00109 | $0.032 |
EBS | Daily volume snapshots, for backup | - | ~$0.03 |
VPC | Public IPv4 address | $0.005 | $0.015 |
Total | $0.029 | $0.578 |
If you have access to the 12-month Free tier, you should automatically benefit from the following offers:
Some of the services used are more than covered by the "always free" monthly offers, namely:
Lambda; SNS; KMS; CloudWatch / X-Ray; Network Data Transfer from EC2 to internet.
- An AWS account and associated credentials for Terraform to create resources
- An Discord app on the Developer portal
- A Duck DNS account and domain
- A SSH keypair for SSH-ing into your instance (e.g
ssh-keygen -t ed25519 -C "minecraft-spot-discord"
)
Requirements: Terraform, Python 3.6+ (due to terraform-aws-lambda), Node 18+ (to compile Lambda functions)
- Clone and navigate to the project:
git clone https://github.com/g-otn/minecraft-spot-discord.git && cd minecraft-spot-discord
- Initialize Terraform:
terraform init
- Manually build the lambda functions (
terraform-aws-lambda
was breaking a lot during plan/apply)
npm i
npm run build --workspaces
- Create a file named
terraform.tfvars
and fill the required variables.- Check
variables.tf
to see which variables are required and their descriptions - Check
example.tfvars
for a full example
- Check
Any extra port you want to open needs to be set both in extra-ingress-rules
and minecraft_compose_ports
variables in a way in which they match.
Note that by default the Minecraft container exposes port 25565, so if you want to run the server in another port you should either change only the host port (like 12345:25565
where 12345 is the custom port) or change the SERVER_PORT
variable.
I'd recommend nothing less than 1 vCPU and 4GB RAM.
This project was mainly tested and monitored with a t4g.large
instance.
Please check out:
- The Vantage website
- Tip: Hide Name,
Windows
-related, Network Performance, On-demand and Reserved columns; Show allLinux Spot
-related andClock Speed
columns; Sort byLinux Spot Average cost
- Tip: Hide Name,
- Spot Instance advisor
In the variables you can set the JVM Heap size (Xms
and Xmx
options) via the minecraft_compose_environment
variable - INIT/MAX_MEMORY
option
and the Docker deploy resource memory limit before the OS kills your container via minecraft_compose_limits
- memory
. See example.tfvars
Firstly, around 200MB is not really available in the instance for usage.
Then I recommended reserving at least 300MB for idle OS, Docker, etc to try prevent the instance from freezing. The remaining will be your Docker memory limit for the container. You could also not set a Docker limit at all.
Finally, save around 600MiB-1.5GiB for the JVM / Off-heap memory.
Instance memory | Available memory | Docker limit | Heap size | Recommended players (Vanilla) |
---|---|---|---|---|
2GiB | 1.8GiB | 1.6GB | 1GB | 1-2 |
4GiB | 3.8GiB | 3.6GB | 2.8GB | 1-4 |
8GiB | 7.8GiB | 7.6GB | 6.1GB | 2-8 |
- DiscordSRV - We're already using Discord, so why not? However it seems this plugin overrides the interactions, so you'll have to create another Discord app on the developer portal just for this. See Installation
- AFK-Kicker - Or any other plugin which can kick afk players, so the server doesn't stays on if nobody is playing
- TabTPS - Or any other plugin for easy in-game information display of server load, etc
-
Run
terraform plan
and revise the resources to be created -
Run
terraform apply
after a while the instance and the game server should be running and accessible
-
Go to the Lambda console on the region you chose, find the
interaction-handler
Lambda and copy it's Function URL. -
Go to your Application on the Discord Developer portal > General Information and paste the URL into
Interactions Endpoint URL
and click save. -
Invite your app to a specific Discord server (guild) using the OAuth2 link found at Installation. Make sure
applications.commands
is set in the Default Install Settings. -
In the project, navigate to
scripts
and create a.env
file
cd scripts
touch .env
- Fill the environment variables required by
add-slash-commands.js
:
DISCORD_APP_ID=123456789
DISCORD_APP_GUILD_ID=3123456789
DISCORD_APP_BOT_TOKEN=MTABCDE
Guild ID is the Discord server ID, app ID and bot token can be found in the Discord Developer Portal.
- Run the script while loading the
.env
file. The script should call the Discord API and register the slash command interactions which the Lambda is ready to handle to that specific Discord server:
node --env-file=.env add-slash-commands.js
- You should now be able to use the
/start
,/stop
,/restart
,/ip
or/status
commands into one of the text channels to manage the instance.- You may need do additional permission/role setup depending on your Discord server configuration (i.e if the app can't use the text channel)
Daily snapshots of the data volume are taken via Data Lifecycle Manager. However depending on your region, you must enable regional STS endpoint. us-east-2
(Ohio) for example, requires it. Otherwise the DLM policy will error when it tries to create the snapshot.
- If applicable, enable the STS regional endpoint for your region on the IAM Console. See Activating and deactivating AWS STS in an AWS Region
CloudWatch log groups are created for the Lambda and VPC flow logs.
X-Ray tracing is also enabled, however you need to manually set up SNS permissions so the traces show up correctly in the Trace Map / etc.
Game data EBS volume is mounted at /srv/minecraft
;
Compose container name is minecraft-mc-1
.
Inside the instance:
htop
docker stats
: Visualize current RAM usage vs the limitdocker attach minecraft-mc-1
: attach terminal to Minecraft server consoledocker logs minecraft-mc-1 -f
: Latest logs from the containersudo systemctl stop minecraft_shutdown.timer
: Stops the systemd timer which prevents the instance from being shut down automatically until next reboot. Don't forget to shutdown manually or start the timer again!
I may or may not do these in the future:
- Isolate the reusable resources into a Terraform module so it's easy to spin more than one instance - similar to mamoit/minecraft-ondemand-terraform
- Make it generic so other games are supported - similar to Lemmons/minecraft-spot
- Create some generic solution for auto-stop, watching active connections etc.
- Create CloudWatch dashboard via Terraform
This project was made for studying purposes mainly. The following repos and articles were very helpful in the learning and development process:
- doctorray117/minecraft-ondemand - The main motivation for this project, I wanted to do something similar but less complex and even cheaper (without Route 53, EFS, DataSync, Twilio and Minecraft watchdog)
- JKolios/minecraft-ondemand-terraform - Gave me an general idea of what I had to do
- mamoit/minecraft-ondemand-terraform - I almost went with this solution instead of creating my own, but I wanted to use EC2 directly instead of ECS + Fargate for slightly cheaper costs
- Lemmons/minecraft-spot - Some Cloud-init and Terraform reference
- vincss/mcEmptyServerStopper - I was using this before I migrated to Docker and itzg/docker-minecraft-server
- Giving kids control of an EC2 instance via discord - Gave me the push to use Discord to reduce costs and simplify some of the workflow, and almost made me use GCP instead of AWS.