Running several game servers on one VPS is cost effective as long as each server gets its own ports, a fair share of CPU and memory, and cannot bring down the others when it misbehaves. Docker Compose handles all three in a single file. In this tutorial you will plan capacity, then run a Minecraft server and a Valheim server side by side on Ubuntu 24.04 with per-server resource limits, and add a daily backup job. The same pattern works for any game that has a maintained Docker image.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS (x86_64), for example a CubePath VPS. For the two example servers, plan on at least 4 vCPU, 12 GB of RAM and 40 GB of SSD storage.
  • A non-root user with sudo privileges.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository. Check with docker compose version.

Step 1 - Planning capacity

Before adding servers, estimate what each one needs at its expected player count. Game servers are usually limited by one fast core for the main loop and by RAM, rarely by bandwidth. Typical starting points:

ServerPlayersCPURAMPorts
Minecraft (Paper)10-202 vCPU4-6 GB25565/tcp
Valheimup to 102 vCPU4 GB2456-2457/udp
Terraria8-161 vCPU1-2 GB7777/tcp
CS210-202 vCPU2-4 GB27015/tcp+udp

Add up the RAM limits of all servers and keep at least 1 to 2 GB free for the operating system, Docker and the page cache. CPU can be shared more loosely, because servers are rarely all at peak at the same time, but more servers than cores means they compete during busy hours. Check what you have:

nproc
free -h
df -h /

Step 2 - Creating the directory layout

Keep each server's data in its own directory under one project folder. That makes backups, moves and deletions simple. Create the layout:

sudo mkdir -p /srv/games/minecraft /srv/games/valheim/config /srv/games/valheim/data
sudo chown -R "$USER":"$USER" /srv/games
cd /srv/games

The resulting structure:

  • /srv/games/compose.yaml: the Compose file for all servers.
  • /srv/games/minecraft/: the Minecraft world, plugins and server.properties.
  • /srv/games/valheim/config/: Valheim worlds and the image's own world backups.
  • /srv/games/valheim/data/: server binaries that the container downloads.

Step 3 - Writing the Compose file

This guide uses two widely used, actively maintained images: itzg/minecraft-server for Minecraft and lloesche/valheim-server for Valheim. Create the Compose file:

nano /srv/games/compose.yaml

Paste the following, replacing your_server_name and your_valheim_password (at least 5 characters, and not part of the server name):

services:
  minecraft:
    image: itzg/minecraft-server
    container_name: minecraft
    restart: unless-stopped
    ports:
      - "25565:25565/tcp"
    environment:
      EULA: "TRUE"
      TYPE: "PAPER"
      MEMORY: "4G"
      MAX_PLAYERS: "20"
      VIEW_DISTANCE: "8"
    volumes:
      - ./minecraft:/data
    cpus: 2.0
    mem_limit: 5g
    stop_grace_period: 1m
    tty: true
    stdin_open: true

  valheim:
    image: ghcr.io/lloesche/valheim-server
    container_name: valheim
    restart: unless-stopped
    ports:
      - "2456-2457:2456-2457/udp"
    environment:
      SERVER_NAME: "your_server_name"
      WORLD_NAME: "Dedicated"
      SERVER_PASS: "your_valheim_password"
      SERVER_PUBLIC: "true"
    volumes:
      - ./valheim/config:/config
      - ./valheim/data:/opt/valheim
    cap_add:
      - SYS_NICE
    cpus: 2.0
    mem_limit: 5g
    stop_grace_period: 2m

The important settings:

  • Ports: each server publishes its own ports on the host. Two servers of the same game need different host ports, for example 25566:25565/tcp for a second Minecraft server.
  • cpus: the maximum CPU time the container may use, in cores. It is a ceiling, not a reservation.
  • mem_limit: the hard memory limit. For Minecraft, set it about 1 GB above MEMORY (the Java heap), because the JVM uses memory outside the heap too. If a container exceeds its limit, the kernel kills that container only, and Docker restarts it.
  • stop_grace_period: how long Docker waits for a clean shutdown before killing the process. Both servers save their world on shutdown, so give them time.
  • EULA: "TRUE": accepts the Minecraft EULA, which is required to start the server. Read it at minecraft.net before accepting.

Validate the file:

docker compose config --quiet && echo OK
OK

Step 4 - Opening the firewall

Docker writes its own iptables rules for published ports, so these ports are reachable from the Internet even when UFW would block them. Add matching UFW rules anyway so that ufw status documents what is really open:

sudo ufw allow 25565/tcp
sudo ufw allow 2456:2457/udp

The practical consequence: only publish ports you actually want to expose. For services that should stay private, such as RCON, do not map them to the host, or bind them to localhost with "127.0.0.1:25575:25575".

Step 5 - Starting the servers

Download the images and start both servers in the background:

cd /srv/games
docker compose up -d

List the containers:

docker compose ps
NAME        IMAGE                             SERVICE     STATUS         PORTS
minecraft   itzg/minecraft-server             minecraft   Up 2 minutes   0.0.0.0:25565->25565/tcp
valheim     ghcr.io/lloesche/valheim-server   valheim     Up 2 minutes   0.0.0.0:2456-2457->2456-2457/udp

The first start takes several minutes, because the Minecraft container downloads the server jar and the Valheim container downloads the dedicated server through SteamCMD. Follow the logs:

docker compose logs -f minecraft

Minecraft is ready when the log shows a line like Done (12.345s)! For help, type "help". Press Ctrl+C to stop following. For Valheim:

docker compose logs -f valheim

Wait for Game server connected, then connect from the game clients to your_server_ip:25565 and your_server_ip:2456.

Step 6 - Checking resource usage

docker stats shows live CPU and memory use against each container's limit:

docker stats --no-stream
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT   MEM %     NET I/O
3f2a1c9e8b7d   minecraft   38.12%    4.3GiB / 5GiB       86.00%    12MB / 45MB
9c8d7e6f5a4b   valheim     61.47%    2.9GiB / 5GiB       58.00%    8MB / 21MB

CPU % is relative to one core, so 200% is the ceiling for a container with cpus: 2.0. If a server sits at its CPU ceiling during play, raise its limit or move it to a server with more cores. If memory stays close to the limit, raise mem_limit before the kernel starts killing the container. You can confirm a kill in the container state:

docker inspect -f '{{.State.OOMKilled}}' minecraft

false means the container has not been killed for exceeding its memory limit.

Pinning servers to cores

On servers with 6 or more cores, you can keep busy game servers from competing for the same cores with cpuset. Add it to each service, for example:

  minecraft:
    cpuset: "0,1"

  valheim:
    cpuset: "2,3"

Apply changes to the Compose file with:

docker compose up -d

Compose recreates only the containers whose configuration changed.

Step 7 - Managing the servers day to day

Compose commands take a service name, so each server can be handled on its own:

docker compose restart valheim
docker compose stop minecraft
docker compose start minecraft

Send a command to the Minecraft console through the image's built-in RCON client:

docker exec minecraft rcon-cli list

To update a server to the latest image and game version, pull and recreate it:

docker compose pull minecraft
docker compose up -d minecraft

The restart: unless-stopped policy starts all servers again after a reboot, as long as the Docker service is enabled (sudo systemctl is-enabled docker returns enabled).

Step 8 - Adding daily backups

The world data lives in /srv/games, so a backup is a compressed copy of that directory. Minecraft keeps writing to its world while running, so the script first tells it to flush and pause saving. Create the script:

sudo nano /usr/local/bin/backup-games
#!/usr/bin/env bash
set -euo pipefail

SRC="/srv/games"
DEST="/var/backups/games"
KEEP_DAYS=7
STAMP="$(date +%Y-%m-%d_%H%M)"

mkdir -p "$DEST"

trap 'docker exec minecraft rcon-cli save-on' EXIT
docker exec minecraft rcon-cli save-off
docker exec minecraft rcon-cli save-all flush

tar -czf "$DEST/games_$STAMP.tar.gz" \
    --exclude="valheim/data" \
    -C "$SRC" .

find "$DEST" -name 'games_*.tar.gz' -mtime +"$KEEP_DAYS" -delete

The Valheim data directory only contains server binaries that the container downloads again, so it is excluded. The Valheim image already writes its own world backups into valheim/config/backups, which this archive includes. Make the script executable and run it once:

sudo chmod 755 /usr/local/bin/backup-games
sudo /usr/local/bin/backup-games
ls -lh /var/backups/games
-rw-r--r-- 1 root root 312M Sep 24 10:40 games_2026-09-24_1040.tar.gz

Schedule it daily with a systemd timer. Create the service:

sudo nano /etc/systemd/system/backup-games.service
[Unit]
Description=Back up game server data
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-games

And the timer:

sudo nano /etc/systemd/system/backup-games.timer
[Unit]
Description=Daily game server backup

[Timer]
OnCalendar=*-*-* 04:30:00
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer and check the next run:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-games.timer
systemctl list-timers backup-games.timer

Backups on the same disk do not protect you from losing the server. Copy /var/backups/games to another location, such as object storage or a second server, on a regular basis.

Troubleshooting

A container keeps restarting: read its last log lines with docker compose logs --tail 50 service_name. For Minecraft, a missing EULA or a heap larger than mem_limit are the usual causes.

Bind for 0.0.0.0:25565 failed: port is already allocated: another container or process already uses that host port. Find it with sudo ss -tulpn | grep 25565 and give one of them a different host port.

Players cannot connect although the container is running: check that the published ports match what the game listens on inside the container (docker compose ps shows them), and that any external firewall in front of the server allows them.

All servers lag at the same time: the host is out of CPU or memory. Compare the sum of the limits with nproc and free -h, and check docker stats during peak hours. Move a server to its own VPS when the total demand exceeds the machine.

Conclusion

You planned capacity for several game servers, ran Minecraft and Valheim side by side with Docker Compose, gave each one its own ports and CPU and memory limits, and set up daily backups with a systemd timer. Adding another game is a matter of a new service block, a data directory and a free port. As next steps, copy the backups off the server, add host-level flood filtering for the game ports, and track long-term resource use to know when a server should move to its own machine.