DEV Community

Cover image for /server start: How I Let My Friends Control Our Minecraft Server via Discord
Fabricio Flores for AWS

Posted on

/server start: How I Let My Friends Control Our Minecraft Server via Discord

BUZZ BUZZ BUZZ

My phone is getting blown up in the middle of the night with calls, texts, and pings from my friends wanting to hop on our Minecraft server. Turns out I had accidentally turned off my ancient laptop that was hosting it. I begrudgingly get up from my cozy bed to boot up the server. Interrupted sleep, all to preserve the sacred annual 2-week Minecraft phase...

So I put my thinking cap on. Is there a way that I could host the server in the cloud and let my friends turn it on and off themselves?

In this post, I'll show you how to build a Discord bot that can interact with your AWS resources. We'll set up a Minecraft server on EC2 as our example, but this same pattern works for any AWS service.

Before we dive in, here's what you'll need:

1. Create Discord Application

  1. Go to Discord Developer Portal
  2. Create New Application
  3. Go to Bot → then click Reset Token and save it securely
  4. Go to OAuth2 → URL Generator → Select bot + applications.commands
  5. Copy Generated URL and add bot to your server
  6. Note your Application ID and Public Key from General Information

2. Create PyNaCl Lambda Layer

Discord requires signature verification on every request. The PyNaCl library handles this, but it has native dependencies — meaning we can't just pip install it on our Mac/Windows and upload to Lambda. We need to build it for Amazon Linux.

Build the layer zip (requires Docker)

Open up your terminal of choice and run:

mkdir -p pynacl_layer
docker run --platform linux/amd64 --rm -v "$(pwd)/pynacl_layer:/out" \
  public.ecr.aws/sam/build-python3.12 bash -c \
  "pip install PyNaCl -t /tmp/python/lib/python3.12/site-packages/ && cd /tmp && zip -r /out/pynacl_layer.zip python"
Enter fullscreen mode Exit fullscreen mode

Make sure Docker Desktop is running first!
You'll get a "Cannot connect to Docker daemon" error otherwise.

Upload it to Lambda

Head to the Lambda console and navigate to Layers → Create layer.

  • Name: pynacl-layer
  • Upload: Choose the pynacl_layer.zip file from your local machine
  • Compatible runtimes: Python 3.12

Click Create.

Configuring the PyNaCl layer

3. Create Lambda Function

Lambda lets us run code on-demand in the cloud. This is where we'll handle incoming Discord commands and control our EC2 instance.

Now go to Functions → Create function.

Basic settings:

  • Function name: discord-bot (or whatever you like)
  • Runtime: Python 3.12
  • Architecture: x86_64 (must match your layer!)

Click Create function.

Once created, scroll to the bottom of the page. You'll see a Layers section. Click Add a layer, select Custom layers, pick pynacl-layer from the dropdown, and hit Add.

Add your Discord public key:

  1. Go to Configuration tab → Environment variables
  2. Click Edit → Add environment variable
  3. Key: DISCORD_PUBLIC_KEY
  4. Value: your public key from Discord Developer Portal (General Information page)
  5. Click Save

Increase the timeout:

  1. Still in Configuration → General configuration
  2. Click Edit
  3. Set Timeout to 15 seconds (to reduce risk if we encounter latency)
  4. Click Save

Lambda Code (lambda_function.py)

Paste this into the Lambda code editor. It verifies that requests are actually coming from Discord, then starts, stops, or checks the status of your EC2 instance based on the command. We're using boto3, the AWS SDK for Python.

import json
import os
import boto3
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError

PUBLIC_KEY = os.environ['DISCORD_PUBLIC_KEY']
# INSTANCE_ID = os.environ['INSTANCE_ID']  # Uncomment in step 8

ec2 = boto3.client('ec2')

def verify_signature(event):
    """Verify the request is actually from Discord"""
    body = event.get("body", "")
    headers = event.get("headers", {})
    signature = headers.get("x-signature-ed25519") or headers.get("X-Signature-Ed25519", "")
    timestamp = headers.get("x-signature-timestamp") or headers.get("X-Signature-Timestamp", "")

    verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))
    verify_key.verify(f"{timestamp}{body}".encode(), bytes.fromhex(signature))

def handle_server(action):
    """Start, stop, or check the EC2 instance"""
    if action == "start":
        ec2.start_instances(InstanceIds=[INSTANCE_ID])
        return "🟢 Starting server..."
    elif action == "stop":
        ec2.stop_instances(InstanceIds=[INSTANCE_ID])
        return "🔴 Stopping server..."
    elif action == "status":
        response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
        state = response['Reservations'][0]['Instances'][0]['State']['Name']
        emoji = "🟢" if state == "running" else "🔴" if state == "stopped" else "🟡"
        return f"{emoji} Server is {state}"
    return "Unknown action"

def lambda_handler(event, context):
    """Entry point for Lambda"""
    try:
        verify_signature(event)
    except Exception:
        return {"statusCode": 401, "body": "Invalid signature"}

    body = json.loads(event.get("body", "{}"))

    # Discord PING check
    if body.get("type") == 1:
        return {"statusCode": 200, "body": json.dumps({"type": 1})}

    # Slash command
    if body.get("type") == 2:
        command_name = body.get("data", {}).get("name")

        if command_name == "server":
            options = body.get("data", {}).get("options", [])
            action = options[0].get("value") if options else None
            message = handle_server(action)
            return {"statusCode": 200, "body": json.dumps({"type": 4, "data": {"content": message}})}

    return {"statusCode": 200, "body": json.dumps({"type": 4, "data": {"content": "Unknown command"}})}
Enter fullscreen mode Exit fullscreen mode

Hit Deploy to save your changes. That's it for the Lambda function for now. We'll come back to it later once we set up our EC2 instance.

4. Create API Gateway

Discord needs a public URL to send commands to. API Gateway gives us that. It acts as the front door to our Lambda function and exposes it as an HTTPS endpoint.

Head to the API Gateway console and create a REST API. Name it something like discord-endpoint.

  1. Create resource /event
  2. Create POST method on /event
  3. Check Lambda proxy integration
  4. Select your Lambda function
  5. Deploy API to a stage (e.g., "prod")
  6. Note the Invoke URL: https://xxx.execute-api.YOUR_REGION.amazonaws.com/prod/event

5. Register Discord Endpoint

Now we tell Discord where to send commands when someone uses our bot.

  1. Go to Discord Developer Portal → Your App → General Information
  2. Set Interactions Endpoint URL to your API Gateway URL
  3. Discord will send a test PING - if verification passes, it saves

6. Register Slash Commands

Discord doesn't know what commands our bot supports yet. We need to register them.

To find your Guild ID, right-click your server name in Discord and click Copy Server ID. If you don't see that option, enable Developer Mode first: Settings → Advanced → Developer Mode.

Run this locally in your terminal:

curl -X POST "https://discord.com/api/v10/applications/YOUR_APP_ID/guilds/YOUR_GUILD_ID/commands" \
  -H "Authorization: Bot YOUR_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "server", "type": 1, "description": "Control game server", "options": [{"name": "action", "description": "What to do", "type": 3, "required": true, "choices": [{"name": "start", "value": "start"}, {"name": "stop", "value": "stop"}, {"name": "status", "value": "status"}]}]}'
Enter fullscreen mode Exit fullscreen mode

7. Create EC2 Instance

EC2 is a virtual machine in the cloud. We'll set up a Minecraft server on it, but you could run whatever you want here.

Head to the EC2 console and launch a new instance.

  • AMI: Amazon Linux 2023
  • Instance type: t3.medium (go larger for more demanding games)
  • Key pair: Create or select one for SSH access
  • Security group: Under Network settings, create a new security group. By default, SSH has Anywhere selected — change this to My IP

Expand Advanced details and paste this into the User data field (this is a script that runs automatically when the instance is first created). First, go to minecraft.net/download/server and right-click the minecraft_server.jar link to copy the URL. Replace PASTE_THE_URL_HERE with it:

#!/bin/bash
dnf install -y java-21-amazon-corretto
mkdir -p /opt/minecraft
cd /opt/minecraft
curl -O PASTE_THE_URL_HERE
echo "eula=true" > eula.txt
chown -R ec2-user:ec2-user /opt/minecraft

cat > /etc/systemd/system/minecraft.service <<EOF
[Unit]
Description=Minecraft Server
After=network.target

[Service]
WorkingDirectory=/opt/minecraft
ExecStart=/usr/bin/java -Xmx1G -Xms1G -jar server.jar nogui
Restart=always
User=ec2-user

[Install]
WantedBy=multi-user.target
EOF

systemctl enable minecraft
systemctl start minecraft
Enter fullscreen mode Exit fullscreen mode

Click Launch instance, then note your Instance ID (e.g., i-0abc123def456). You'll need it for the next step.

Configure security group for Minecraft:

Click on your instance, then click the Security tab and click on the security group link. Go to Inbound rules → Edit inbound rules and add a rule:

Type Port Source
Custom TCP 25565 (Minecraft port) Anywhere (0.0.0.0/0)

Assign an Elastic IP:

Every time you stop and start your EC2 instance, the public IP changes. That means your friends would need a new IP every time. To fix this, assign an Elastic IP.

  1. In the EC2 console, go to Network & Security → Elastic IPs
  2. Click Allocate Elastic IP addressAllocate
  3. Select the new Elastic IP, click Actions → Associate Elastic IP address
  4. Choose your instance and click Associate

Now your server always has the same IP. This is the address you'll share with your friends.

8. Connect Lambda to Your EC2 Instance

Now that you have your EC2 instance, head back to your Lambda function.

Add the Instance ID environment variable:

  1. Go to ConfigurationEnvironment variables
  2. Click EditAdd environment variable
  3. Key: INSTANCE_ID
  4. Value: your EC2 instance ID
  5. Click Save
  6. Back in the code editor, uncomment the INSTANCE_ID line and hit Deploy

Add IAM permissions so Lambda can control your instance. IAM (Identity and Access Management) is how AWS controls what services are allowed to do. Go to ConfigurationPermissions, click the role name, hit the Add permissions dropdown, and create an inline policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            "Resource": "arn:aws:ec2:YOUR_REGION:YOUR_ACCOUNT_ID:instance/YOUR_INSTANCE_ID"
        },
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeInstances",
            "Resource": "*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

This gives Lambda permission to start, stop, and check the status of your EC2 instance. Notice we're scoping StartInstances and StopInstances to your specific instance ARN instead of using *, so Lambda can only touch this one instance. DescribeInstances uses * because it doesn't support resource-level permissions.

Want to restrict who can use the bot commands? Discord lets server admins control that through Server Settings → Integrations. Check out Discord's command permissions docs for details.

9. Try It Out!

Head over to your Discord server and test your bot:

  1. Type /server start and wait for the instance to boot up
  2. Type /server status to confirm it's running
  3. Open Minecraft, connect to your instance's public IP, and play!
  4. When you're done, do /server stop to shut down and save money.

Lambda and API Gateway fall under the AWS Free Tier. The main cost to watch is EC2 if you size up for more demanding games.

Using the Discord bot to control the server

We've now got our Minecraft server up and running. Let's hop in! Join the Minecraft server using your EC2 public IP as the server address: <your-ec2-public-ip>:25565

Playing Minecraft on the server

Your friends can start or stop the server at any time with the Discord bot.

Happy crafting!

Appendix: Cleanup

If you want to tear everything down, delete these resources:

Top comments (0)