Most Minecraft server hosting solutions cost ~$10 a month. If you are only using the server occasionally, that's a lot of wasted money. Self-hosting is free, but if you want any of your friends to be able to join your server at any time, then you, the host, must either:
- A. keep the server online 24/7, or
- B. manually spin it up/down whenever somebody wants to hop on
This project offers a more flexible alternative: an EC2 setup that costs $0.00/month when you aren't using it, and only pennies per hour when you are.
It achieves this by hibernating (downloading your server data to your local machine/Google Drive) and deleting the cloud infrastructure when you're done for the season. When you want to play again, a single command spins the infra back up. Then, any of your friends can email the startup email to trigger server startup. The server will automatically close following inactivity.
You can interact with the system via Web UI, CLI commands, or REST API—all powered by the same backend.
Key features:
- API-First Architecture: Web UI, CLI, and REST API for maximum flexibility
- Zero Idle Cost: Hibernate your server when not in use.
- On-Demand: Spin up via email, web UI, CLI, or API.
- Auto-Shutdown: Server turns itself off when nobody is playing.
NOTE: This setup requires some initial configuration (AWS account, Cloudflare), but once set up, it requires very little maintenance.
- Usage
- Local Development with Mock Mode
- Background
- How It Works
- Repo Structure
- Setup Guide
- Connecting to the Server
- Google Drive Backups
- Weekly EBS Snapshots
- How to Manage It
- Hibernation
- Authentication
- Production Deployment
You can interact with your Minecraft server through three interfaces:
The web interface provides a dashboard for server status, cost tracking, and management operations.
pnpm dev
# Open http://localhost:3000Run these commands from the project root:
| Command | Description |
|---|---|
pnpm server:status |
Check server state |
pnpm server:start |
Start the server |
pnpm server:stop |
Stop the server |
pnpm server:hibernate |
Backup + stop + delete volume (zero cost) |
pnpm server:resume |
Create volume + start |
pnpm server:backup |
Manual backup to Google Drive |
pnpm server:restore |
Restore from backup |
pnpm server:backups |
List available backups |
All API endpoints are prefixed with /api/. Base URL is your deployed frontend URL.
| Endpoint | Method | Description |
|---|---|---|
/api/status |
GET | Server state and info |
/api/start |
POST | Start server |
/api/stop |
POST | Stop server |
/api/hibernate |
POST | Hibernate (backup + stop + delete volume) |
/api/resume |
POST | Resume from hibernation |
/api/backup |
POST | Trigger backup |
/api/restore |
POST | Restore from backup |
/api/backups |
GET | List available backups |
/api/players |
GET | Player count |
/api/costs |
GET | Cost tracking |
Mock mode allows you to develop and test the application without requiring AWS resources, credentials, or infrastructure. It's perfect for:
- Offline development: No network or AWS dependencies
- Fast iteration: Instant responses, no API latency
- Deterministic testing: Predefined scenarios for consistent test runs
- Error handling: Test edge cases and failure scenarios
# 1. Copy the minimal mock mode configuration
cp .env.example .env.local
# 2. Start dev server in mock mode with dev login enabled
pnpm dev:mock
# 3. Open browser and authenticate
open http://localhost:3001/api/auth/dev-login
# 4. Start developing!
open http://localhost:3001For detailed instructions: See docs/QUICK_START_MOCK_MODE.md
# Start dev server in mock mode
pnpm dev:mock
# Run E2E tests in mock mode
pnpm test:e2e:mock
# Run unit tests in mock mode
pnpm test:mock
# Validate dev login is working
pnpm validate:dev-login
# Reset mock state
pnpm mock:reset
# List available scenarios
pnpm mock:scenario
# Apply a specific scenario
pnpm mock:scenario running| Command | Description |
|---|---|
pnpm dev:mock |
Start dev server in mock mode |
pnpm test:e2e:mock |
Run E2E tests in mock mode |
pnpm test:mock |
Run unit tests in mock mode |
pnpm mock:reset |
Reset mock state to defaults |
pnpm mock:scenario |
List available scenarios |
pnpm mock:scenario <name> |
Apply a specific scenario |
Mock mode is controlled by the following environment variables:
| Variable | Description | Default |
|---|---|---|
MC_BACKEND_MODE |
Backend mode: aws or mock |
aws |
ENABLE_DEV_LOGIN |
Enable dev login route for local auth testing | false |
MOCK_STATE_PATH |
Optional path for mock state persistence file | (none) |
MOCK_SCENARIO |
Optional default scenario to apply on startup | (none) |
Mock mode includes 10 built-in scenarios for testing different states:
| Scenario | Description |
|---|---|
default |
Normal operation, instance stopped with defaults |
running |
Instance is running with public IP and players |
starting |
Instance is in pending state (mid-start) |
stopping |
Instance is in stopping state (mid-stop) |
hibernated |
Instance stopped without volumes (zero cost) |
high-cost |
Instance with high monthly costs for testing alerts |
no-backups |
No backups available for testing error handling |
many-players |
Instance running with high player count |
stack-creating |
CloudFormation stack in CREATE_IN_PROGRESS state |
errors |
All operations fail with errors |
Example usage:
# Apply the "running" scenario
pnpm mock:scenario running
# Apply the "errors" scenario to test error handling
pnpm mock:scenario errors
# Reset to default state
pnpm mock:resetWhen running in mock mode, you can control the mock state via HTTP endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/mock/state |
GET | Get current mock state |
/api/mock/scenario |
GET | List available scenarios |
/api/mock/scenario |
POST | Apply a scenario (body: {scenario}) |
/api/mock/reset |
POST | Reset mock state to defaults |
/api/mock/fault |
POST | Inject faults for testing |
Example: Apply scenario via API
curl -X POST http://localhost:3001/api/mock/scenario \
-H "Content-Type: application/json" \
-d '{"scenario": "running"}'Testing start/stop flows:
# Apply "stopped" scenario
pnpm mock:scenario default
# Start dev server
pnpm dev:mock
# Use the web UI or API to start the server
# The mock will transition from stopped → pending → running
# Check the state
curl http://localhost:3001/api/mock/stateTesting error scenarios:
# Apply the "errors" scenario
pnpm mock:scenario errors
# All operations will now fail with errors
# Test your error handling UI and logic
# Reset when done
pnpm mock:resetTesting UI states:
# Apply "high-cost" scenario
pnpm mock:scenario high-cost
# Start dev server
pnpm dev:mock
# Verify cost alerts and warnings display correctlyMock state can optionally be persisted to a JSON file for debugging:
# Enable persistence in .env.local
MOCK_STATE_PATH=./mock-state.json
# State will be saved to this file on every change
# Useful for debugging and reproducing issuesSee docs/MOCK_MODE_DEVELOPER_GUIDE.md for comprehensive documentation including:
- Detailed scenario descriptions
- Fault injection techniques
- Troubleshooting common issues
- Advanced usage patterns
Shell scripts in legacy/bin/ are deprecated in favor of the web UI, CLI, and REST API. The following utilities remain available for specific use cases:
legacy/bin/connect.sh— Interactive SSH access to the EC2 instance via AWS Systems Managerlegacy/bin/console.sh— Direct access to the Minecraft console screen session
All other shell scripts for backup, restore, hibernate, and resume are superseded by the CLI commands and API endpoints.
Traditional hosting providers charge a flat monthly fee. By moving to AWS and using this project's scripts, you only pay for what you use.
| Cost Component | Hibernated (Deep Storage) | Standby (Quick Start) | Active (Playing) |
|---|---|---|---|
| Compute (RAM/CPU) | $0.00 / month | $0.00 / month | ~$0.03 / hour |
| Storage (World Data) | $0.00 / month * | ~$0.75 / month | (Included) |
| Total Cost | $0.00 / month | ~$0.75 / month | ~$0.03-0.04 / hour |
* Assuming you hibernate to local disk or free cloud storage (e.g. Google Drive).
If you play for 8 hours in a specific month and hibernate the server for the rest of the time:
- Compute: 8 hours * ~$0.03/hr = $0.24
- Storage: $0.00 (Hibernated)
- Total: $0.24 for the entire month
Unless you are playing for many hours, this setup is significantly cheaper than using a traditional, dedicated provider.
There do exist on-demand hosting providers such as Exarotron* and ServerWave**. These options are definitely cheaper for many use cases. Not to mention, they'll give you a bunch of extra features and are infinitely easier to set up and use. However, if you want complete flexibility, this setup is the best because:
- If you want to extend this project, you can. You have complete control of the server and its lifecycle, configuration, etc. You could set up a Discord connection, or a simple webapp that triggers the server startup.
- Anyone who knows your start keyword and email address can easily spin the server up by sending an email. This means that anybody can play, whether you're available or not. With self-hosting, you would have to be there to turn on the server. With a traditional on-demand provider, you would have to log in to a control panel to spin the server up.
- You only pay for what you use, at per-second increments (no pre-paying for usage or hourly rounding up).
*Exarotron specifically does offer a Discord bot that you could grant access to in order to start the server, but setting up Discord is another step for your non-technical/non-gamer friends to handle. Conversely, everybody has email. Exarotron also requires that you buy credits in ~$3.00 increments, which limits spending flexibility. If you want to play for a couple months and then stop, anything left over from your last ~$3.00 increment will be wasted. Also, technically, using a t3.medium EC2 instance with 4GB of RAM costs $0.04 per hour, which is ever-so-slightly cheaper than the €0.04 per hour for a similar 4G server on Exarotron.
**ServerWave requires that you pay per-hour of usage, not per-second.
At this point, we're talking about pennies. In some cases, you'll save a few pennies with this setup, and in other cases, you'll lose a few. However, this setup exists not just to save money, but to enable independence from any third-party services (besides our almighty cloud providers, of course, upon which the rest of the observable software universe is but a wrapper). And because it's fun to build/scaffold/tinker/control.
- Startup: Send an email with the secret keyword (default "start") in the subject to your trigger address (e.g.,
start@mydomain.com). - Trigger: AWS SES catches the email and executes a Lambda function.
- Authorization: The Lambda checks if the sender is authorized (admin email or in the allowlist, if configured).
- Launch: The Lambda starts the EC2 instance and updates your Cloudflare DNS record to point to the new IP.
- Config Sync: On boot, the server automatically pulls the latest
server.propertiesandwhitelist.jsonfrom your GitHub repo. - Auto-Shutdown: A script runs every minute checking for players. After 15 minutes idle, it stops the Minecraft service and shuts down the EC2 instance.
mc-aws/
├── app/ # Next.js App Router (pages & API routes)
├── components/ # React components
├── lib/ # Shared utilities, AWS clients, types
├── hooks/ # React hooks
├── scripts/ # CLI scripts (server-cli.ts)
├── tests/ # Unit and E2E tests
├── infra/ # AWS CDK infrastructure
│ ├── bin/ # CDK entry point
│ ├── lib/ # CDK stack definitions
│ └── src/ # EC2 and Lambda code
├── config/ # Minecraft server config
├── legacy/ # Deprecated shell scripts
│ └── bin/ # Old CLI scripts
└── docs/ # Documentation and PRDs
This project uses AWS CDK to automate the entire setup process. Follow these steps to deploy your on-demand Minecraft server.
Want to get up and running as fast as possible? The automated setup.sh script handles everything for you:
- ✅ Installs mise (the modern tool manager)
- ✅ Installs Node.js and pnpm via mise
- ✅ Collects all required credentials (AWS, Cloudflare, GitHub, etc.)
- ✅ Configures environment variables
- ✅ Deploys your Minecraft server infrastructure
Prerequisites:
- A GitHub account (for the fork)
- An AWS account (free tier works)
- A Cloudflare account with a domain
Run the setup:
# 1. Clone the repository
git clone https://github.com/you/mc-aws.git
cd mc-aws
# 2. Run the automated setup
./setup.shThe script will guide you through each step, prompting for any required information. When complete, your server will be ready to use!
Note: If you prefer manual control over each step, or if the automated script encounters issues, see the detailed Prerequisites and Deployment Steps below.
Before you begin, ensure you have the following:
- Node.js installed (v18+)
- AWS CLI installed and configured:
Enter your AWS Access Key ID, Secret Access Key, default region (e.g.,
aws configure
us-west-1), and default output format (json). - Session Manager Plugin (for connecting to the server):
brew install --cask session-manager-plugin
You need a domain managed by Cloudflare for dynamic DNS updates. If you don't have one, register a domain (typically <$1/month) and point it to Cloudflare's nameservers.
You'll need three pieces of information from Cloudflare:
A. Zone ID:
- Log in to Cloudflare and select your domain
- Open the "Overview" section
- Scroll down to the "API" section on the right sidebar
- Copy your Zone ID
B. API Token:
- Go to Manage account > Account API tokens
- Click Create Token
- Choose the Edit Zone DNS template (or create custom with Zone > DNS > Edit permissions)
- Zone Resources: Include > Specific zone > Your Domain
- Click Continue to Summary → Create Token
- Copy the token immediately (you won't see it again)
C. DNS Record ID:
- Go to DNS > Records on the left sidebar
- Create an A record for your Minecraft subdomain (e.g.,
mc). Point it to1.1.1.1(placeholder; it will be updated automatically) - To get the Record ID, use the Cloudflare API:
curl -X GET "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/dns_records" \ -H "Authorization: Bearer <YOUR_API_TOKEN>" \ -H "Content-Type: application/json"
- Find your DNS record in the JSON output and copy its
idfield. You'll need to use this for theCLOUDFLARE_RECORD_IDfield in.env
AWS SES requires email verification before you can send/receive emails.
- Go to AWS Console → Amazon SES → Identities
- Click Create identity
- Select Domain and enter your domain (e.g.,
yourdomain.com) - Follow the verification steps (add DNS records to Cloudflare)
- You'll need to copy the 3 DKIM CNAME records to the Cloudflare DNS for your domain
- Then, you'll need to add an MX record (with the root domain as the name, and
inbound-smtp.us-west-1.amazonaws.comfor the mail server, if in the West region). You can set priority to10.
- Important: If you're in SES Sandbox mode, you must also verify:
- The sender email (the email you'll send the "start" command from, e.g.,
start@yourdomain.com) - The notification email (where you want to receive server alerts, if different from sender)
- The sender email (the email you'll send the "start" command from, e.g.,
If you want to use SSH for file uploads (the restore-to-ec2.sh script), create an EC2 key pair:
- Go to AWS Console → EC2 → Key Pairs
- Click Create key pair
- Name:
mc-aws - Format:
.pem
- Name:
- Download the file and move it to your local SSH directory:
mv ~/Downloads/mc-aws.pem ~/.ssh/mc-aws.pem chmod 400 ~/.ssh/mc-aws.pem
- You'll add this key pair name to your
.envfile in the next step
-
Fork and Clone:
- Fork shanebishop1/mc-aws.
- Clone your fork and open the repo
-
Install Dependencies:
npm install
-
Bootstrap CDK:
npx cdk bootstrap
-
Configure Environment:
Copy the environment template:
cp .env.example .env.local
The
.env.localfile contains your credentials. Environment-specific settings (mock mode, dev login, localhost URL) are automatically set by thepnpm devorpnpm dev:mockcommands.Then edit `.env` with your specific values: ```bash # Cloudflare (from Prerequisites section above) CLOUDFLARE_ZONE_ID="your-zone-id" CLOUDFLARE_DNS_API_TOKEN="your-api-token" CLOUDFLARE_RECORD_ID="your-record-id" CLOUDFLARE_MC_DOMAIN="mc.yourdomain.com" # AWS SES (verified emails from Prerequisites section above) VERIFIED_SENDER="start@yourdomain.com" # SES address: receives start emails + sends notifications (From) NOTIFICATION_EMAIL="you@yourdomain.com" # (Optional) Where to receive alerts START_KEYWORD="start" # (Optional) Word that triggers server start # GitHub (for server to pull config on boot) GITHUB_USER="your-github-username" GITHUB_REPO="mc-aws" # or your fork name GITHUB_TOKEN="ghp_..." # See step 5 below # AWS AWS_ACCOUNT_ID="123456789012" AWS_REGION="us-west-1" # EC2 Key Pair (Optional, for SSH file uploads) KEY_PAIR_NAME="mc-aws" # Leave blank if not using SSH # Google Drive (Optional, for cloud backups/transfers) # Note: Token is auto-setup on first use, stored in SSM Parameter Store (FREE) # GDRIVE_REMOTE="gdrive" # GDRIVE_ROOT="mc-backups" -
Create a GitHub Personal Access Token (PAT):
The EC2 instance needs to pull config files from your GitHub repo on each boot. To do this securely, you need a GitHub PAT:
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Give it a descriptive name (e.g.,
mc-aws-server) - Set an expiration (or "No expiration" if you prefer)
- Select the
reposcope (required for private repos; for public repos, no scopes are needed) - Click Generate token
- Copy the token immediately (you won't see it again)
- Add it to your
.envfile:GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
How it works: When you run
npm run deploy, the deploy script readsGITHUB_TOKENfrom your.envand securely stores it in AWS SSM Parameter Store as an encrypted SecureString. The EC2 instance fetches this token on boot to clone/pull your repo. -
Deploy:
npm run deploy
The deploy script will:
- Create the EC2 instance, Lambda function, IAM roles, and SES rules
- Automatically activate the SES Rule Set
- Ask if you want to enable weekly EBS snapshots via DLM
Note on Google Drive (optional): Google Drive backups are optional and auto-configured on first use. Just run
./bin/backup-from-ec2.sh --mode driveor./bin/restore-to-ec2.sh --mode drivewhen you want to use it—the OAuth flow will run automatically. -
Wait for Setup:
The deployment typically takes 3-5 minutes. Once complete, your server is ready to use!
Email commands are default-deny: only the admin email(s) (NOTIFICATION_EMAIL / ADMIN_EMAIL) and any emails in ALLOWED_EMAILS can trigger start.
How it works:
- Baseline allowed senders:
NOTIFICATION_EMAIL(fallback:ADMIN_EMAIL) +ALLOWED_EMAILS - Additional allowed senders are stored in AWS SSM at
/minecraft/email-allowlist - Admin can update the stored allowlist by listing emails in the email body (baseline senders always remain allowed)
- No redeployment needed - updates happen via email
To add additional emails:
Send an email to your trigger address (e.g., start@yourdomain.com) from your admin email with the authorized email addresses in the body (one per line):
Subject: (anything, can be empty)
Body:
friend1@example.com
friend2@gmail.com
teammate@company.com
You'll receive a confirmation email showing the updated allowlist. After this, those emails (plus the baseline allowlist) can start the server by putting the keyword in the subject.
To update the allowlist:
Send another email from your admin address with the new list in the body. It replaces the stored allowlist (baseline emails from env always remain allowed).
The admin email(s) (set in NOTIFICATION_EMAIL / ADMIN_EMAIL) can manage the server by sending emails with specific commands in the subject line. Only the admin email(s) can use backup, restore, hibernate, and resume commands.
Available Commands:
-
start- Start the server- Subject:
start - Anyone on the allowlist can use this
- Subject:
-
backup- Backup server to Google Drive with auto-generated name- Subject:
backup - Admin only
- Subject:
-
backup <name>- Backup with custom name- Subject:
backup my-world-jan-2026 - Admin only
- Subject:
-
restore <name>- Restore from Google Drive backup- Subject:
restore my-world-jan-2026 - Admin only
- Restores the server to a previous backup
- Subject:
-
hibernate- Backup to Drive, stop EC2, delete EBS to save costs- Subject:
hibernate - Admin only
- Deletes the EBS volume for zero storage cost (~$0.75/month saved)
- Requires a backup to be created first
- Subject:
-
resume- Start EC2, restore latest backup- Subject:
resume - Admin only
- Creates a new EBS volume and restores the most recent backup
- Subject:
-
resume <name>- Start EC2, restore specific backup- Subject:
resume my-world-jan-2026 - Admin only
- Creates a new EBS volume and restores a specific backup
- Subject:
How It Works:
- Commands go in the email subject line (body is ignored)
- Confirmation emails are sent for all operations
- All backups are stored in Google Drive (requires Google Drive setup from the deployment section)
- Hibernate deletes the EBS volume, reducing idle costs to $0.00/month
- Resume creates a new EBS volume and restores from your backup
- Only the admin email (
NOTIFICATION_EMAIL) can use backup, restore, hibernate, and resume commands - The
startcommand can be used by anyone on the allowlist
To start your server for the first time:
- Send an email with your start keyword (default:
start) in the subject to your verified sender email (e.g.,start@yourdomain.com) - Wait ~60 seconds for the server to boot
- Connect to your Minecraft server using the domain you configured (e.g.,
mc.yourdomain.com)
You have two ways to connect to your EC2.
This uses AWS Systems Manager (SSM). No SSH keys or open ports required.
-
Connect to Shell:
./bin/connect.sh
This drops you into a root shell on the server.
-
Connect to Minecraft Console:
./bin/console.sh
This connects you directly to the running Minecraft screen session.
SSH access is needed for the restore-to-ec2.sh script and traditional SFTP/rsync.
One-Time Setup:
-
Create a Key Pair: Go to EC2 Console → Key Pairs → Create Key Pair.
- Name it
mc-aws - Format:
.pem - Click Create — the file downloads automatically
- Important: You can only download this file once! If you lose it, delete the key pair and create a new one.
- Name it
-
Move the file:
mv ~/Downloads/mc-aws.pem ~/.ssh/mc-aws.pem chmod 400 ~/.ssh/mc-aws.pem
-
Add to your
.env:KEY_PAIR_NAME="mc-aws" -
Redeploy:
npm run deploy
Usage:
# SSH manually
ssh -i ~/.ssh/mc-aws.pem ec2-user@<SERVER_IP>
# Upload local server folder to replace EC2 server folder
./bin/upload-server.sh ./server/You can backup/transfer server data via Google Drive using rclone. The Google Drive token is auto-configured on first use and stored securely in AWS SSM Parameter Store. We use Google Drive to transfer data between your your machine and EC2 because it's more reliable than trying to upload/download directly.
Just use Google Drive mode in one of the scripts and authenticate when prompted:
# First time: Opens browser for Google OAuth, stores token in SSM, then backs up
./bin/backup-from-ec2.sh --mode drive
# Or for restoring via Drive:
./bin/restore-to-ec2.sh --mode driveThe OAuth flow runs automatically on first use. Subsequent runs use the stored token.
Backup (EC2 → Google Drive):
./bin/backup-from-ec2.sh --mode driveTars server data on EC2 and uploads to Google Drive (no local download).
Restore (local/Drive → EC2):
./bin/restore-to-ec2.sh --mode driveRestores server data from Google Drive to EC2.
Local mode (default):
./bin/backup-from-ec2.sh # EC2 → local (rsync)
./bin/restore-to-ec2.sh ./server/ # local → EC2 (rsync)- Google Drive token is stored in AWS SSM Parameter Store (
/minecraft/gdrive-token) with SecureString encryption—no cost - Idle-check is disabled during backup/restore operations and re-enabled afterward
- Optional environment variables (defaults shown):
GDRIVE_REMOTE="gdrive"— rclone remote nameGDRIVE_ROOT="mc-backups"— folder name on Google Drive
During deployment, you were asked if you want to enable weekly EBS snapshots via AWS Data Lifecycle Manager (DLM). This is optional but good practice for additional data protection.
Cost: ~$0.05 per snapshot (typically 1-2 snapshots retained at a time = ~$0.10/month extra)
If you enabled snapshots during deploy:
- Your EBS volume is automatically tagged with
Backup: weekly - DLM creates snapshots every Sunday at 2 AM UTC
- Snapshots are retained for 4 weeks, then automatically deleted
- You can restore from a snapshot if needed (via AWS Console → EC2 → Snapshots)
If you want to enable/disable snapshots later:
- Go to EC2 > Volumes and select your server's volume
- Add or remove the tag: Key=
Backup, Value=weekly - The DLM policy will automatically pick up the change
Playing: Send an email with your start keyword (default "start") in the subject to your trigger address. Wait ~60 seconds, then connect your server from Minecraft.
Managing Players:
- Find UUIDs: Use a tool like mcuuid.net to find the UUID for each player you want to allow.
- Edit Config: Update
config/whitelist.jsonin this repo. It should look like this:[ { "uuid": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "name": "PlayerOne" }, { "uuid": "f1e2d3c4-b5a6-9780-4321-098765fedcba", "name": "PlayerTwo" } ] - Push: Commit and push your changes to GitHub.
- Sync: The next time the server starts, it will automatically pull the latest changes.
Updating Properties:
- Edit
config/server.propertiesin your local repo. - Commit and push to GitHub.
- Restart the server (or wait for next boot) to apply changes.
If you're not going to play for an extended period (weeks/months), you can completely eliminate the ~$0.75/month storage cost by deleting the EBS volume. When you want to play again, you can restore it from your local backup.
Prerequisites: Make sure you have a local backup of your world first!
-
Backup Your World:
./bin/backup-from-ec2.sh
This saves your world to a local directory with a timestamp.
-
Hibernate (Delete EBS):
./bin/hibernate.sh
This will:
- Stop your EC2 instance
- Optionally create an AWS snapshot backup (for extra safety)
- Detach and delete the EBS volume
- Result: $0.00/month idle cost (no storage charges)
When you want to play again:
-
Resume (Create Fresh EBS):
./bin/resume.sh
This will:
- Create a new 8GB GP3 volume from the latest Amazon Linux 2023 AMI
- Attach it to your EC2 instance
- Start the instance
- Auto-configure the server (via user_data script)
-
Wait for Setup: Wait ~2 minutes for the user_data script to install Java, Paper, and configure everything.
-
Restore Your World:
./bin/restore-to-ec2.sh /path/to/your/downloaded/server
This uploads your world data back to the server.
-
Play Connect to your Minecraft server as usual, using your domain and port.
| Scenario | Monthly Cost |
|---|---|
| Normal (Server stopped, EBS attached) | ~$0.75/month |
| Hibernated (Server stopped, EBS deleted) | $0.00/month |
| Playing (Server running) | ~$0.03/hour + storage |
When to Hibernate:
- You won't play for 2+ weeks
- You want absolute minimum cost
- You have reliable local backups
When NOT to Hibernate:
- You play regularly (weekly)
- You want instant startup via email trigger
- You don't want to manage local backups
The web UI uses Google OAuth to control who can perform actions. Authentication works the same way in all environments—no code bypasses.
| Role | Can View Status | Can Start | Can Backup/Restore/Hibernate |
|---|---|---|---|
| Public (not logged in) | ✅ | ❌ | ❌ |
| Allowed (on allow list) | ✅ | ✅ | ❌ |
| Admin | ✅ | ✅ | ✅ |
This project uses separate environment files for development and production:
| File | Purpose | Loaded When |
|---|---|---|
.env.local |
Local development | pnpm dev |
.env.production |
Production deployment | pnpm build / Cloudflare |
Setup:
# For local development
cp .env.example .env.local
# For production
cp .env.example .env.productionInstead of bypassing auth in development, use the dev-login route to get a real session cookie:
- Add
ENABLE_DEV_LOGIN=trueto.env.local - Run
pnpm dev - Visit
http://localhost:3000/api/auth/dev-login - You're logged in with a real cookie for 30 days
To test different roles, edit role in app/api/auth/dev-login/route.ts:
"admin"- Full access"allowed"- Can start server"public"- View only
Why this approach?
- Your local environment behaves exactly like production
- Auth bugs are caught during development, not in production
- Easy to test different permission levels
For mock mode authentication:
When using MC_BACKEND_MODE=mock, dev login is the recommended authentication method. See Authentication in Mock Mode for detailed documentation including:
- How dev login works in mock mode
- Security features and safeguards
- Testing different user roles
- Troubleshooting common auth issues
- E2E testing with dev login
Quick start for mock mode:
# Use the convenience script (sets both MC_BACKEND_MODE and ENABLE_DEV_LOGIN)
pnpm dev:mock
# Visit dev login endpoint
open http://localhost:3000/api/auth/dev-loginSee .env.example for the minimal mock mode configuration (no AWS credentials needed).
For local development and testing without AWS resources, use mock mode:
# Enable mock mode in .env.local
MC_BACKEND_MODE=mock
ENABLE_DEV_LOGIN=trueMock mode provides:
- Offline testing: No AWS credentials or infrastructure required
- Deterministic scenarios: Predefined states (running, stopped, starting, etc.)
- Fault injection: Test error handling by injecting failures
- Fast feedback: No network latency or AWS API calls
Run E2E tests in mock mode:
pnpm test:e2e tests/mock-mode-e2e.spec.tsSee tests/MOCK_MODE_E2E.md for detailed documentation on mock mode testing.
- User clicks "Sign in with Google" in the header
- Redirected to Google OAuth consent screen
- After approval, redirected back with a session cookie
- Session lasts 7 days
- API routes check the session and enforce authorization based on user's role
This is a separate allow list from the Email Allowlist (used for SES email triggers), but they share the same baseline emails.
Email Allowlist behavior (SES triggers):
- Baseline allowed senders come from env:
NOTIFICATION_EMAIL(fallback:ADMIN_EMAIL) +ALLOWED_EMAILS - Additional allowed senders are stored in AWS Systems Manager (SSM) Parameter Store as a comma-separated string at
/minecraft/email-allowlist - CDK deploy seeds
/minecraft/email-allowlistif it doesn't exist, and the control panel keeps the baseline emails present - If both the baseline list and the SSM list are empty, only the admin email(s) can trigger commands via email (default deny)
Set the Email Allowlist (before or after deploy):
- AWS Console: Systems Manager -> Parameter Store -> create/update
/minecraft/email-allowlistin the same region as your stack - AWS CLI:
aws ssm put-parameter \
--name "/minecraft/email-allowlist" \
--type "String" \
--value "friend1@gmail.com,friend2@gmail.com" \
--overwrite \
--region "${AWS_REGION}"- Control panel (admin-only): use the Email Management panel to add/remove emails (writes the same SSM parameter)
The control panel allow list is controlled by environment variables and is enforced server-side.
ADMIN_EMAIL: the one email that always getsadminroleALLOWED_EMAILS(optional): comma-separated list of emails that getallowedrole- Any authenticated email not in either list gets
publicrole
Example:
ADMIN_EMAIL=you@gmail.com
ALLOWED_EMAILS=friend1@gmail.com,friend2@gmail.comHow it works:
- User signs in with Google
- Server maps their email to a role in
lib/auth.ts(admin/allowed/public) - API routes enforce permissions with guards in
lib/api-auth.ts(requireAdmin,requireAllowed,requireAuth)
Updating the control panel allow list:
- Local dev: edit
.env.localthen restartpnpm dev - Cloudflare Workers: update
wrangler.jsoncvars.ALLOWED_EMAILSthen redeploy
This application deploys to Cloudflare Workers using the OpenNext adapter. The deployment process is fully automated—just configure your .env.production file and run one command.
# Copy the same template (just fill in production URL)
cp .env.example .env.production
# Edit only NEXT_PUBLIC_APP_URL - everything else is identical to .env.local
vim .env.productionThe deploy script automatically sets MC_BACKEND_MODE=aws and ENABLE_DEV_LOGIN=false for production.
The deployment script will:
- ✅ Auto-generate
AUTH_SECRETif missing - ✅ Validate all required secrets are set
- ✅ Upload all secrets to Cloudflare Workers
- ✅ Extract route from your
NEXT_PUBLIC_APP_URL - ✅ Build and deploy to Cloudflare
- Cloudflare account
- Wrangler CLI authenticated:
wrangler login - Google OAuth credentials → Setup Guide
- AWS credentials → Setup Guide
- Domain configured in Cloudflare DNS
The control panel runs on the same domain as your Minecraft server:
- HTTP/HTTPS (ports 80/443): Cloudflare Workers serves the web panel
- TCP (port 25565): Proxied to your EC2 instance for Minecraft
Example: mc.yourdomain.com
https://mc.yourdomain.com→ Web control panelmc.yourdomain.com:25565→ Minecraft server
Setup:
- Go to Cloudflare Dashboard → DNS
- Add an A record:
- Name:
mc - Content: Your EC2 public IP (placeholder, auto-updated)
- Proxy status: Proxied (☁️ orange cloud)
- Name:
- The deploy script automatically configures the Workers route from your
NEXT_PUBLIC_APP_URL
All secrets stay in .env.production (gitignored) - no manual wrangler secret put commands needed!
# Copy template
cp .env.example .env.productionRequired variables (see setup guides for values):
| Variable | Description | Setup Guide |
|---|---|---|
GOOGLE_CLIENT_ID |
Google OAuth client ID | Google OAuth Setup |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | Google OAuth Setup |
AWS_ACCESS_KEY_ID |
AWS access key | AWS Credentials Setup |
AWS_SECRET_ACCESS_KEY |
AWS secret key | AWS Credentials Setup |
ADMIN_EMAIL |
Your email for admin access | Your email address |
NEXT_PUBLIC_APP_URL |
Your deployed URL | https://mc.yourdomain.com |
AWS_REGION |
AWS region | us-west-1 (or your region) |
CLOUDFLARE_DNS_API_TOKEN |
Cloudflare DNS API token | From Cloudflare dashboard |
CLOUDFLARE_ZONE_ID |
Cloudflare zone ID | From Cloudflare dashboard |
INSTANCE_ID |
EC2 instance ID | From AWS Console or CDK output |
Optional variables:
AUTH_SECRET- Auto-generated if missing (48-byte random)ALLOWED_EMAILS- Comma-separated list of allowed users- Other Cloudflare/GitHub/Google Drive settings
See .env.example for all available variables.
# One command deployment
pnpm deploy:cfThis script automatically:
- Generates
AUTH_SECRETif needed - Validates all required secrets are set
- Uploads secrets to Cloudflare Workers
- Builds the Next.js app
- Deploys with the correct route (extracted from
NEXT_PUBLIC_APP_URL)
Preview locally first (optional):
pnpm preview:cfThe build automatically validates that all required environment variables are set:
- Development: Warns about missing variables but continues
- Production: Fails the build if any required variables are missing
1. Update Google OAuth Redirect URI:
Go to Google Cloud Console and add:
https://mc.yourdomain.com/api/auth/callback/google
2. Test Your Deployment:
# Visit your control panel
open https://mc.yourdomain.com
# Sign in with Google
# Test server start/stop functionality3. Check Logs (if issues):
wrangler tailDeployment fails with "Missing required secrets":
- Check
.env.productionhas all required values - Make sure no values are still placeholders (
your-*)
OAuth redirect error:
- Verify redirect URI in Google Console matches your
NEXT_PUBLIC_APP_URL - Should be:
https://mc.yourdomain.com/api/auth/callback/google
Can't connect to AWS:
- Verify IAM user has required permissions
- Check
AWS_REGIONmatches where your instance is deployed
For more help, see:
