Deploy Crafty Controller on a VPS
A self-hosted multi-server Minecraft control panel — Java + Bedrock, file manager, scheduled backups, REST API, and role-based access — fronted by Nginx with a real Let's Encrypt certificate.
At a Glance
| Project | Crafty Controller (by Arcadia Technology) |
| License | GPLv3 |
| Recommended Plan | Cloud VPS 4 GB+ (small vanilla world); 8 GB+ for Paper/modded |
| OS | Ubuntu 24.04 LTS (also Debian 12, Rocky/Alma 9) |
| Install Path | Docker Compose (recommended) or native Python |
| Estimated Setup Time | 45–60 minutes |
Sizing rule of thumb
- Vanilla, 5–10 players: 4 GB / 2 vCPU / 60 GB SSD — allocate 2.5 GB to JVM
- Paper or modded, 10–20 players: 8 GB / 4 vCPU / 100 GB SSD
- 2–4 servers, mixed Java/Bedrock: 16 GB / 6 vCPU / 200 GB SSD
- Heavy modpack (ATM, RLCraft): 16 GB+ / 6+ vCPU NVMe — clock speed beats core count
Disk grows fast: a single world with view-distance=10 often hits 5–15 GB; with 7 dailies + 4 weekly backups plan on 4×–6× live world size.
Initial Server Setup
ssh root@your.vps.ip
apt update && apt -y upgrade
apt -y install curl ca-certificates gnupg lsb-release ufw fail2ban unattended-upgradesadduser crafty-admin
usermod -aG sudo crafty-admin
mkdir -p /home/crafty-admin/.ssh
cp /root/.ssh/authorized_keys /home/crafty-admin/.ssh/
chown -R crafty-admin:crafty-admin /home/crafty-admin/.ssh
chmod 700 /home/crafty-admin/.ssh
chmod 600 /home/crafty-admin/.ssh/authorized_keysPermitRootLogin no
PasswordAuthentication nosystemctl restart ssh
dpkg-reconfigure --priority=low unattended-upgradesConfigure the Firewall
Define every port up front, then enable UFW once. Note we do not open 8443 — the panel sits behind Nginx on 443.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp comment 'HTTP for Lets Encrypt'
ufw allow 443/tcp comment 'HTTPS reverse proxy'
ufw allow 25500:25600/tcp comment 'Minecraft Java port range'
ufw allow 19132/udp comment 'Minecraft Bedrock'
ufw allow 8123/tcp comment 'Dynmap (optional)'
ufw enable25500–25600 matches Crafty's default port pool for new servers. Narrow it only if you'll never run more than one world.
Install Docker Engine
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker crafty-admin
# log out and back in for the group change to take effectRun Crafty Controller
mkdir -p ~/crafty/docker/{backups,logs,servers,config,import}
cd ~/craftyservices:
crafty:
container_name: crafty_container
image: registry.gitlab.com/crafty-controller/crafty-4:latest
restart: always
environment:
- TZ=America/New_York
ports:
- "127.0.0.1:8443:8443" # HTTPS panel — localhost only
- "8123:8123" # Dynmap
- "19132:19132/udp" # Bedrock
- "25500-25600:25500-25600" # Java port range
volumes:
- ./docker/backups:/crafty/backups
- ./docker/logs:/crafty/logs
- ./docker/servers:/crafty/servers
- ./docker/config:/crafty/app/config
- ./docker/import:/crafty/importdocker compose up -d
docker compose logs -f
# Ctrl+C once you see "Crafty has started, head to https://...:8443"
docker exec crafty_container cat /crafty/app/config/default-creds.txtSave the random password. Username is admin. We'll change it through the UI after the proxy is up.
Reverse Proxy with Nginx + Let's Encrypt
Crafty's self-signed cert on 8443 is fine for a LAN homelab — on a public VPS you want a real cert. Point an A record for crafty.yourdomain.com at the VPS, then:
sudo apt install -y nginx certbot python3-certbot-nginxserver {
listen 80;
server_name crafty.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name crafty.yourdomain.com;
client_max_body_size 2048M;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass https://127.0.0.1:8443;
proxy_ssl_verify off; # Crafty's internal cert is self-signed
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/crafty /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d crafty.yourdomain.com
sudo certbot renew --dry-runDon't skip the WebSocket headers. Crafty's per-server terminal view uses one — without Upgrade/Connection the dashboard loads but the in-browser console stays blank.
First Login + Panel Hardening
Browse to https://crafty.yourdomain.com, log in as admin, then:
- Change the admin password under Panel Config → User Management
- Enable two-factor auth on the admin account
- Create separate accounts for co-admins with the lowest role that does the job
- Under Panel Config → Server Settings, set External IP/DNS so player join links generate correctly
Create Your First Minecraft Server
From the dashboard click Create New Server:
- Server Type: Minecraft Java
- Source: Download Server JAR — Vanilla latest, or Paper for plugin support
- RAM: set min == max to avoid GC thrash. 4 GB plan → 2560M; 8 GB → 5120M; leave 1.5 GB for OS + Crafty + cache
- Port: 25565
Crafty downloads the jar, generates eula.txt, and creates the entry. Hit Start and watch the terminal pane.
Optional friendly DNS — an SRV record means players don't need to type the port:
_minecraft._tcp.yourdomain.com. 86400 IN SRV 0 5 25565 mc.yourdomain.com.Backups (Panel + Off-Host)
In the server's Backups tab, set a daily 4 a.m. schedule, enable compression, max 7 backups, exclude logs/* and cache/*. Backups land in ~/crafty/docker/backups/<server-uuid>/.
Layer Restic on top to ship them off-host:
sudo apt install -y restic
export B2_ACCOUNT_ID="your-key-id"
export B2_ACCOUNT_KEY="your-application-key"
export RESTIC_PASSWORD="a-long-passphrase"
export RESTIC_REPOSITORY="b2:your-bucket-name:crafty-backups"
restic init0 5 * * * crafty-admin /usr/bin/restic --quiet backup \
/home/crafty-admin/crafty/docker/backups \
/home/crafty-admin/crafty/docker/servers \
/home/crafty-admin/crafty/docker/config \
&& /usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --pruneSource the B2 + Restic env vars from /etc/restic/crafty.env (chmod 600) via a wrapper script. Test the restore: restic restore latest --target /tmp/restore-test.
Operational Hygiene
- fail2ban for SSH:
systemctl enable --now fail2ban - Whitelist for private servers: set
white-list=trueinserver.properties— kills 99% of scanner traffic - Online mode: leave
online-mode=trueunless you specifically need cracked clients - Monitoring: Netdata installs in one line and gives per-container Docker stats
Modern Minecraft (1.20.5+) needs Java 21. Older modpacks often want Java 17 or 8. Crafty's Docker image ships all three:
Java 8: /usr/lib/jvm/java-8-openjdk/jre/bin/java
Java 17: /usr/lib/jvm/java-17-openjdk/bin/java
Java 21: /usr/lib/jvm/java-21-openjdk/bin/java
# confirm what's actually present in your image:
docker exec crafty_container ls /usr/lib/jvm/Updating Crafty
cd ~/crafty
docker compose pull
docker compose up -dCrafty handles its own DB migrations on container start. Watch the logs after a major version bump. All servers, configs, and backups live in volume mounts so they survive container recreation. Take a Restic snapshot before a major version jump anyway — "good about migrations" isn't the same as "I tested rolling back at 11 p.m. on a Saturday."
Troubleshooting Cheat Sheet
- Panel loads but server terminal is blank: WebSocket headers missing in nginx — re-check
Upgrade/Connection - Container starts then exits: volume permissions —
sudo chown -R 1000:1000 ~/crafty/docker - Failed to bind to port: port outside Crafty's allowed range — pick something in 25500–25600
- Players can't connect: test with
nc -vz your.vps.ip 25565— if that fails, UFW; if it succeeds, world is still loading
