Crafty Controller 4.11.0 on AWS User Guide
Overview
This guide covers launching and using Crafty Controller 4.11.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg game server listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Crafty Controller is a free, open source web panel for creating and running Minecraft servers. From the browser you create Java or Bedrock servers of any common flavour, start and stop them, watch the live console and send commands, edit server files, schedule tasks, take and restore backups, and give other people their own accounts with roles that limit what they can touch.
The cloudimg image adds:
- A unique admin password, proved before the panel opens. Crafty makes its
adminaccount on the first start of each instance with a random 64 character password. First boot proves that password signs in and that a wrong one is refused, moves it into/root/crafty-credentials.txt(mode 0600, root), destroys Crafty's own copy, and only then starts nginx. The image holds no account, password or database. - HTTPS from first boot. nginx serves the panel on port 443 with a certificate made for this instance, redirects port 80 to it, refuses requests for any other host name, carries the live console WebSocket, refuses cross-site requests, and slows down password guessing on the sign-in route.
- A container that cannot touch the host. Crafty runs from a container built by cloudimg from upstream's 4.11.0 image, pinned by digest. It runs as
crafty(uid 64010), a dedicated system account with no shell, a locked password, no sudo and no other group, not as theubuntulogin user. The container has no Linux capabilities, cannot gain privileges, sees only its own 12 state directories under/var/lib/craftyand has no access to the Docker socket. - Patched dependencies, checked at every start. The upstream image ships Python packages and Ubuntu libraries with published security fixes outstanding. The cloudimg container raises twelve Python packages to fixed releases (hash pinned) and applies the Ubuntu updates, without changing any Crafty code. Every time the container starts it reads the versions actually loaded and refuses to start Crafty if any is below its floor. Step 4 shows the check.
- Game servers that cannot reach your network. The game servers Crafty runs need the internet (to download server files and talk to Mojang), but a firewall chain on the container's bridge rejects private and link local destinations, and the instance metadata service is blocked for every container. The AMI requires IMDSv2.
- The lockout recovery closed to the network. Upstream's unauthenticated "Forgot Password" recovery creates a temporary superuser. nginx refuses it from the network; you can still use it from the instance itself (see Troubleshooting).
- Exactly the ports you need. TCP 443 (and 80, which redirects) for the panel, TCP 25565 for the default Minecraft Java server, and TCP 22 for SSH. You choose further game ports in one file.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Crafty Controller 4.11.0 (GPL-3.0) in the cloudimg container cloudimg/crafty-controller:4.11.0-cloudimg, Docker Engine, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The game server software itself (the Minecraft server jar and the Java runtime it needs) is downloaded by Crafty when you create a server.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (Crafty downloads server files when you create a server).
- A public IPv4 address on the instance (auto assigned or an Elastic IP), so you and your players can reach it.
- A security group that allows TCP 443 and 80 (the panel) from the networks you manage from, TCP 25565 (the default Minecraft Java server) from your players, and TCP 22 (SSH) from your own networks only.
- Size the instance for the game servers you will run, not for the panel. A Minecraft Java server wants 2 to 4 GB of memory for a handful of players; m5.large (8 GB) runs one or two comfortably.
Connecting to your instance
| Version | SSH user |
|---|---|
| Crafty Controller 4.11.0 on Ubuntu 24.04 | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg game server listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Crafty Controller 4.11.0 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80), HTTPS (443) and the Minecraft port (25565) to anywhere. Restrict SSH to your own IP address, and 80 and 443 to the networks you manage from.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. It creates a security group with the rules above, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR ranges with your own:
SG=$(aws ec2 create-security-group --group-name crafty \
--description "Crafty Controller" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 25565 --cidr 0.0.0.0/0
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=crafty}]'
You do not need --metadata-options: the AMI already requires IMDSv2. If you want the address to survive a stop and start, associate an Elastic IP.
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot takes two to three minutes. It sets the name the panel is served as (the instance's public IP address until you give it a DNS name), makes the TLS certificate for it, starts Crafty on loopback, proves the admin password, writes /root/crafty-credentials.txt and only then opens nginx. These are the entries in that file, then the names of every key it holds (the password itself is not shown here):
sudo grep -E '^CRAFTY_(URL|USERNAME)=' /root/crafty-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/crafty-credentials.txt | tr -d '='
sudo stat -c '%a %U:%G' /root/crafty-credentials.txt
CRAFTY_URL=https://34.201.60.154/
CRAFTY_USERNAME=admin
CRAFTY_URL
CRAFTY_USERNAME
CRAFTY_PASSWORD
600 root:root
| Value | What it is for |
|---|---|
CRAFTY_URL |
The address of this instance's panel |
CRAFTY_USERNAME |
admin, the superuser account |
CRAFTY_PASSWORD |
The admin password, generated on this instance |
To read the password, run sudo cat /root/crafty-credentials.txt in your own session and store it in your password manager. The file is not updated when you change the password in the panel.
Step 3: Confirm Crafty is running
Docker, the metadata firewall unit, Crafty and nginx are active. nginx serves 80 and 443, the panel itself listens only on loopback port 8443, and the one game port published by default is 25565:
systemctl is-active docker.service cloudimg-docker-imds-deny.service crafty.service nginx.service
sudo docker ps --format '{{.Names}} {{.Image}} {{.Status}}' | sed -E 's/Up [^(]*/Up/'
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/crafty-host.conf)
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'login page: HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/login"
curl -s -o /dev/null -w 'port 80: HTTP %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
active
active
active
active
crafty cloudimg/crafty-controller:4.11.0-cloudimg Up
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:25565 users:(("docker-proxy"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8443 users:(("docker-proxy"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:25565 users:(("docker-proxy"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
ok
login page: HTTP 200
port 80: HTTP 301
Step 4: Check the Crafty version and its patched dependencies
The panel reports its own version, and the container's version floor check reads the version of each raised package from the running Python interpreter, from the libraries pip carries inside itself, and from the container's Ubuntu package database. The container runs the same check at every start and refuses to start Crafty (exit code 78, logged in sudo journalctl -u crafty) if any line fails:
sudo docker exec crafty cat /crafty/app/config/version.json | tr -d ' \n'; echo
sudo docker exec crafty /crafty/.venv/bin/python3 -I /opt/cloudimg/crafty-version-floor.py
{"major":4,"minor":11,"sub":0}
OK py pillow running=13.0.0 floor=13.0.0
OK py tornado running=6.6.1 floor=6.6.1
OK py anyio running=4.14.2 floor=4.14.2
OK py certifi running=2026.10.23 floor=2026.10.23
OK py cryptography running=50.0.2 floor=50.0.2
OK py pyOpenSSL running=26.4.0 floor=26.4.0
OK py orjson running=3.11.9 floor=3.11.9
OK py PyJWT running=2.15.1 floor=2.15.1
OK py requests running=2.34.2 floor=2.34.2
OK py urllib3 running=2.8.0 floor=2.8.0
OK py setuptools running=84.2.1 floor=84.2.1
OK py pip running=26.3 floor=26.3
OK pip-vendored urllib3 running=2.8.0 floor=2.8.0
OK pip-vendored certifi running=2026.10.23 floor=2026.10.23
OK pip-vendored msgpack running=1.2.4 floor=1.2.4
OK deb openssl running=3.0.13-0ubuntu3.16 floor=3.0.13-0ubuntu3.16
OK deb libssl3t64 running=3.0.13-0ubuntu3.16 floor=3.0.13-0ubuntu3.16
OK deb apt running=2.8.4ubuntu0.1 floor=2.8.4ubuntu0.1
OK deb libapt-pkg6.0t64 running=2.8.4ubuntu0.1 floor=2.8.4ubuntu0.1
OK deb libbrotli1 running=1.1.0-2ubuntu0.1 floor=1.1.0-2ubuntu0.1
VERSION_FLOOR PASS failures=0
The container's build recipe, the list of raised packages and the GPL-3.0 licence text are on the instance in /usr/share/doc/crafty-controller/.
Step 5: Sign in to the panel
Open CRAFTY_URL (for example https://34.201.60.154/) in your browser. The certificate is self signed and made for this instance, so the browser warns the first time; accept it, or give the instance a DNS name and your own certificate (step 8). Sign in as admin with CRAFTY_PASSWORD. Leave the authenticator code empty until you turn on two factor sign-in for your account.

The dashboard shows the host's CPU and memory use, the space left for backups, and how many servers and players are online. A new instance has no servers yet:

Open your account from the avatar at the top right to change your password and to turn on two factor sign-in.
Step 6: Create a game server
Choose Create New Server. Pick Minecraft-Java or Minecraft-Bedrock, the server type (for example Vanilla, Paper or Fabric) and version, give it a name, and set the memory and the port under Quick Settings. Keep the port at 25565 for your first server, because that is the port the instance publishes. Choose Build Server!; Crafty downloads the server files and the server appears on the dashboard. You can also import an existing server from a zip archive on the right.

Open the server, accept the Minecraft EULA when Crafty asks, and start it. The Terminal tab shows the live console and takes commands; Files, Backups, Schedules and Config do what they say. Players connect from Minecraft with the instance's address (port 25565 is the default, so the address alone is enough).
Stop a server before you delete it. Deleting a server that is still stopping can leave Crafty ignoring start, stop and backup actions for every server until the panel restarts. If that happens, run sudo systemctl restart crafty.
Publish more game ports
Only the ports listed in /etc/cloudimg/crafty-game-ports are published from the container to the instance. A fresh instance publishes 25565/tcp:
grep -v '^#' /etc/cloudimg/crafty-game-ports
sudo docker port crafty | sort
25565/tcp
25565/tcp -> 0.0.0.0:25565
25565/tcp -> [::]:25565
8443/tcp -> 127.0.0.1:8443
To run a second server, or a Bedrock server (UDP 19132), add one line per port or range (PORT/tcp, PORT/udp or FIRST-LAST/tcp, between 1024 and 65535, at most 101 ports per line), open the same ports in the instance's security group, and restart Crafty. Restarting stops the game servers that are running; Crafty starts again on its own, and servers set to auto start come back with it.
sudo nano /etc/cloudimg/crafty-game-ports
sudo systemctl restart crafty
Step 7: Check the security controls
The container account is a locked system account in no group but its own, the container runs as it with no capabilities and no way to gain privileges, the state directories belong to it, and only those 12 directories are mounted (no Docker socket):
id crafty
sudo passwd -S crafty | cut -d' ' -f1,2
sudo docker inspect --format 'user={{.Config.User}} cap_drop={{.HostConfig.CapDrop}} security_opt={{.HostConfig.SecurityOpt}} privileged={{.HostConfig.Privileged}}' crafty
P=$(sudo docker inspect --format '{{.State.Pid}}' crafty); sudo grep -E '^(Uid|CapEff):' /proc/$P/status
sudo stat -c '%a %U:%G %n' /var/lib/crafty/config /var/lib/crafty/servers /var/lib/crafty/backups
sudo docker inspect --format '{{range .Mounts}}{{.Source}}{{println}}{{end}}' crafty | grep -c '^/var/lib/crafty/'
sudo docker inspect --format '{{range .Mounts}}{{.Source}}{{println}}{{end}}' crafty | grep -q docker.sock && echo "docker socket mounted" || echo "no docker socket mounted"
uid=64010(crafty) gid=64010(crafty) groups=64010(crafty)
crafty L
user=64010:64010 cap_drop=[ALL] security_opt=[no-new-privileges:true] privileged=false
Uid: 64010 64010 64010 64010
CapEff: 0000000000000000
2770 crafty:crafty /var/lib/crafty/config
2770 crafty:crafty /var/lib/crafty/servers
2770 crafty:crafty /var/lib/crafty/backups
12
no docker socket mounted
The certificate is a plain server certificate made for this instance, the firewall drops metadata traffic from every container and rejects private destinations from Crafty's bridge, the host units are denied the metadata addresses, the Crafty container cannot reach the metadata service, and the lockout recovery is refused from the network:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/crafty-host.conf)
openssl s_client -connect 127.0.0.1:443 -servername "$H" </dev/null 2>/dev/null |
openssl x509 -noout -subject -enddate -ext basicConstraints,extendedKeyUsage
sudo iptables -S DOCKER-USER
sudo iptables -S CLOUDIMG-CR-EGRESS | grep -oE '\-d [0-9./]+ -j REJECT' | awk '{print $2}' | paste -sd' '
for u in nginx docker crafty; do echo "$u.service IPAddressDeny=$(systemctl show -p IPAddressDeny --value $u.service)"; done
sudo docker exec crafty /crafty/.venv/bin/python3 -I -c 'import urllib.request as u
try: u.urlopen("http://169.254.169.254/latest/meta-data/", timeout=5); print("metadata reachable")
except Exception as e: print("metadata from the Crafty container:", type(e).__name__)'
curl -sk -o /dev/null -w 'lockout recovery from the network: HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/v2/crafty/resetPass"
subject=CN = 34.201.60.154, O = cloudimg
notAfter=Feb 13 04:00:30 2029 GMT
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication
-N DOCKER-USER
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
-A DOCKER-USER -i cr-web0 -j CLOUDIMG-CR-EGRESS
0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 224.0.0.0/4 240.0.0.0/4
nginx.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32
docker.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32
crafty.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32
metadata from the Crafty container: URLError
lockout recovery from the network: HTTP 403
The private ranges include your VPC, so a game server plugin cannot probe other instances; replies to players are not affected. The VPC's DNS resolver is the one private address the container may use.
Step 8: Serve the panel under your own DNS name
Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell the instance the name. The helper re-issues the self-signed certificate for it, updates the name nginx accepts, the cross-site guard and Crafty's own base URL, records the name in /etc/cloudimg/crafty-hostname (kept across reboots), reloads nginx and updates CRAFTY_URL:
sudo crafty-set-hostname games.example.com
crafty-set-hostname: Crafty Controller is served at https://games.example.com/
Until you set a name, the instance follows its own public IP address: if it comes back from a stop and start with a new address, the certificate and settings are updated at boot.
To use a certificate from your own CA or from Let's Encrypt instead, set the name with crafty-set-hostname first so the automatic refresh leaves your certificate alone, replace /etc/ssl/crafty/crafty.crt and /etc/ssl/crafty/crafty.key (keep the key mode 0600, owner root), and run sudo systemctl reload nginx.
Step 9: Add users and roles
Open Panel Settings. Under Panel Config, Add New User creates an account with its own password, and Add New Role creates a role that grants chosen permissions (for example console, files, backups or schedules) on chosen servers. Give each person their own account rather than sharing admin; only superusers can change panel settings and branding.

Maintenance
- Logs:
sudo docker logs crafty(the panel),/var/lib/crafty/logs/(Crafty's own logs),/var/log/nginx/for HTTPS access, andsudo journalctl -u crafty-firstboot.service -u crafty-postboot.servicefor first boot. - Restart:
sudo systemctl restart crafty(this also stops running game servers), orsudo systemctl restart nginx. - Where your data is: everything Crafty keeps is under
/var/lib/crafty: its database and settings inconfig, the game servers inservers, backups inbackups. Take EBS snapshots of the root volume, and use each server's Backups tab for scheduled backups you can restore from the panel. Crafty keeps backups on the same disk by default, so copy them off the instance as well. - Operating system updates: Ubuntu's unattended security upgrades stay on, so nginx and the rest of Ubuntu's own packages keep receiving security patches. Docker Engine comes from Docker's own apt repository, which unattended upgrades do not cover: update it, together with everything else, with
sudo apt update && sudo apt upgradein a maintenance window. Upgrading Docker restarts it and Crafty with it, so running game servers stop briefly; check withsystemctl is-active craftyafterwards. - Upgrading Crafty: the cloudimg container is built from upstream's image pinned by digest, and first boot never pulls an image. To move to a later Crafty release, launch the newer cloudimg version from the listing and move
/var/lib/craftyacross (after backing it up), or contact cloudimg support.
Troubleshooting
The panel does not answer on port 443. On a new instance nginx waits until first boot has proved the admin password, by design. Check with sudo journalctl -u crafty-postboot.service --no-pager | tail -20 and sudo systemctl status crafty nginx.
Crafty does not start. sudo journalctl -u crafty --no-pager | tail -30 shows the reason. A line starting FAIL followed by dependency version floor FAILED means a package inside the container is older than the fixed release, so Crafty refuses to run; contact cloudimg support. A start-up check message about /var/lib/crafty means a state directory's owner was changed: each must stay owned by crafty:crafty (uid 64010).
Start, stop or backup buttons do nothing. This happens after a server was deleted while it was still stopping. Run sudo systemctl restart crafty, and stop servers before deleting them.
Players cannot connect. Check that the server is running in the panel, that its port is listed in /etc/cloudimg/crafty-game-ports and shown by sudo docker port crafty, and that the security group allows it (TCP for Java, UDP for Bedrock).
You lost the admin password. The panel's Forgot Password button is refused from the network. From an SSH session on the instance, start Crafty's recovery on its loopback port; it creates a temporary superuser named anti-lockout-user for one hour and prints its password to Crafty's console, which is kept in the container log and in the crafty service journal:
curl -sk https://127.0.0.1:8443/api/v2/crafty/resetPass; echo
sudo docker logs --since 5m crafty 2>&1 | grep -A1 'Username: anti-lockout-user'
{"status":"ok","data":"Lockout account has been activated for 1 hour. Please find temporary credentials in the terminal"}
Sign in as that user, set a new password for admin under Panel Settings > Users, then update /root/crafty-credentials.txt with sudo nano (or delete that file) so it does not hold a stale password.
A request answers 400 Bad Request. Its path contains a backslash, an encoded slash, backslash or dot, or a . or .. segment, which nginx refuses by design. Use the plain path.
The browser says the sign-in is blocked or answers 429. nginx slows down repeated sign-in attempts from one address. Wait a minute and try again with the exact CRAFTY_URL.
Support
cloudimg provides 24/7 support for this image by email and live chat: launching and connecting, game servers and ports, users and roles, backups, TLS and hostnames, and upgrades.
Crafty Controller is developed by the Crafty Controller team and contributors and is released under the GNU General Public License v3.0. The licence text, a pointer to the corresponding source and the cloudimg container's build recipe ship on the instance in /usr/share/doc/crafty-controller/. Minecraft is a trademark of Mojang Synergies AB. Ubuntu is a registered trademark of Canonical Ltd. Docker is a trademark of Docker, Inc. All other product and company names are trademarks or registered trademarks of their respective holders. cloudimg is not affiliated with or endorsed by them.