Architecture illustration of a Minecraft server on Oracle Cloud Infrastructure with Podman

Minecraft Server on OCI Free Tier with Rootless Podman

My ten-year-old son wanted to play Minecraft online with his friends. That is a normal enough request, but I did not want to just throw a public server onto the internet and hope for the best.

The requirement was simple: it should be a small, controlled place where only known players can join. No random discovery, no open lobby, no “let’s see who shows up”. Just an allowlisted server for a few kids.

The other part was mine. I had been meaning to spend more time with Podman in a setup that was not just a lab note. Docker is familiar territory. Rootless Podman, Quadlet, and user-level systemd services are more interesting to me right now. A Minecraft server is a good excuse because it is small, stateful, network-facing, and actually used by someone who will complain if it breaks.

So this is the setup: A Minecraft server on an Oracle Cloud Free Tier with an Ampere A1 ARM VM, running as a non-root user with Podman Quadlet and systemd --user.

What I Wanted

The server should do a few things and not much more:

  • serve Minecraft Java Edition on TCP 25565
  • allow Bedrock clients through Geyser/Floodgate on UDP 19132
  • keep the world data outside the container
  • restart cleanly through systemd
  • run without root
  • only allow known players

There is no Kubernetes here. No load balancer – well, this will be another post. No service mesh. No “platform engineering” cosplay for five kids building castles. A single VM and one container are enough.

A brief overview how to setup your VCN and attach a public IP with the help of the wizard in the OCI you may find in my other blog post: WireGuard VPN over IPv6 with OSPF on Oracle Cloud Free Tier – Part I: OCI Preparation. More details will be available in the vendor documentation: VCNs and Subnets.

Please make sure to understand the difference between a Public and a Private Subnet. In the Minecraft example I make use of the Public Subnet, for simplicity. A Private Subnet deployment needs a separate ingress design, such as a public Load Balancer or a reverse proxy. I will cover a Private Subnet deployment in a separate post.

Why OCI Free Tier?

Oracle OCI is unusually useful for small always-on homelab services. The Ampere A1 VM shape gives you ARM compute with enough memory to run more than a toy process.

The OCI metadata service reports this VM as VM.Standard.A1.Flex in eu-frankfurt-1, with 4 OCPUs and 24 GB of memory.

Oracle’s current Always Free documentation describes Ampere A1 resources as 1,500 OCPU hours and 9,000 GB hours per month, and says that for Always Free tenancies this is equivalent to 2 OCPUs and 12 GB of memory. Capacity and account history matter. Treat the host specs as what this VM reports, not as a promise that every new tenancy can provision the same shape today.

For this use case, the production VM has plenty of headroom. I set the Minecraft heap to about 9 GB with MaxMemory=8960. That is still not meant as a large public server configuration, but it is comfortable for a small allowlisted group.

The other useful bits are normal Cloud plumbing:

  • public IPv4 and IPv6 on the VM
  • a VCN with explicit ingress rules
  • attached block storage for persistent world data
  • ARM compute without running a noisy box at home

Do not confuse Free Tier with “no need for backups”. If the world matters, back it up somewhere outside the VM.

Prepare Persistent Storage

The storage layout is also worth noting. The boot disk is the normal 50 GB root disk. Minecraft data lives on an attached disk mounted under /mnt/oraclevdb2; the active world path for this server is:

/mnt/oraclevdb2/podmanvolumes/minecraft_1

In addition to the boot disk I attached a 100 GB block volume, /mnt/oraclevdb2 is a 75 GB partition. And the other 25GB are reserved for swap.

/dev/oracleoci/oraclevdb1  /mnt/oraclevdb1    ext4    defaults,_netdev,noatime  0  2
/mnt/oraclevdb1/swapfile  swap swap   defaults  0  0
/dev/oracleoci/oraclevdb2  /mnt/oraclevdb2    ext4    defaults,_netdev,noatime  0  2

Please make sure to understand the parameter in detail. What works for me, may not work for you! Be aware of iSCSI and the requirement to use UUIDs!

Disk /dev/sda: 46.58 GiB, 50010783744 bytes, 97677312 sectors
Disk model: BlockVolume
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 1048576 bytes
Disklabel type: gpt
Disk identifier: xxx
Device Start End Sectors Size Type
/dev/sda1 206848 97677278 97470431 46.5G Linux filesystem
/dev/sda15 2048 204800 202753 99M EFI System
Disk /dev/sdb: 99 GiB, 106300440576 bytes, 207618048 sectors
Disk model: BlockVolume     
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 1048576 bytes
Disklabel type: gpt
Disk identifier: xxx
Device        Start       End   Sectors Size Type
/dev/sdb1      2048  50333695  50331648  24G Linux filesystem
/dev/sdb2  50333696 207615999 157282304  75G Linux filesystem

Check swap with

swapon -s
Filename	        	    Type		Size		Used		Priority
/mnt/oraclevdb1/swapfile    file		24578352	0	      	-2

Container Image

The official Minecraft Bedrock Dedicated Server does not provide a native Linux ARM64 build. During my research I noticed James A. Chambers Project Official Minecraft Bedrock Dedicated Server on Raspberry Pi* and his Git Repository TheRemote/Legendary-Java-Minecraft-Geyser-Floodgate. This image combines the Java server with Geyser and Floodgate to support Bedrock clients.

The cool stuff:

docker.io/05jchambers/legendary-minecraft-geyser-floodgate:latest

The Docker Hub page describes it as a Legendary Java Minecraft + Geyser + Floodgate + Paper dedicated server image with Bedrock support. It also links to the James A. Chambers project and related GitHub repositories.

I did not rewrite the container logic. The point of this post is the OCI and rootless Podman setup around an existing container image. That is also why the image name still starts with docker.io: Podman can pull from Docker-compatible registries just fine.

Install Podman on Ubuntu

The VM is Ubuntu 24.04.4 LTS on aarch64, running Oracle’s Ubuntu kernel 6.17.0-1011-oracle. Podman is installed from the normal Ubuntu repositories, so there is no separate repository setup needed just to install Podman:

sudo apt-get update
sudo apt-get install -y podman uidmap slirp4netns fuse-overlayfs dbus-user-session netavark aardvark-dns

The exact dependency set can vary a bit by Ubuntu release, but these are the pieces I care about for rootless containers:

podman:                     the container engine
uidmap:                     subordinate UID/GID mapping for rootless containers
slirp4netns:                rootless user-mode networking
fuse-overlayfs:             rootless overlay storage support
dbus-user-session:          user systemd and DBus plumbing
netavark and aardvark-dns:  Podman networking and DNS components

After installation, check that Podman sees a rootless-capable environment:

podman --version
podman info --format '{{.Host.CgroupsVersion}}'
grep "^$(whoami):" /etc/subuid /etc/subgid

I expect cgroup v2 on a modern Ubuntu image. I also expect entries in /etc/subuid and /etc/subgid for the non-root user. If those mappings are missing, rootless containers may fail in confusing ways. On a normal Ubuntu system, creating the user usually handles this, but I still check it explicitly.

On the VM the relevant mappings are present:

podman version 4.9.3
v2
/etc/subuid:user:231072:65536
/etc/subgid:user:231072:65536

If the Minecraft service should run after logout, enable lingering for that user:

sudo loginctl enable-linger <user>

That part is easy to forget. Without lingering, a user-level systemd service may be tied too closely to login sessions. On the VM, loginctl show-user user -p Linger returns Linger=yes.

Configure Podman to Pull from Docker Hub

In this setup the Quadlet file uses a fully qualified image name:

Image=docker.io/05jchambers/legendary-minecraft-geyser-floodgate:latest

That is the least ambiguous option. Podman knows exactly which registry to use, and no extra short-name search configuration is needed for this service.

If you want Podman to resolve unqualified image names through Docker Hub, configure unqualified-search-registries. On the production host this is configured system-wide in /etc/containers/registries.conf:

[[registriy]]
unqualified-search-registries = ["docker.io"]

For a single-user homelab VM, a user-local config is also fine:

mkdir -p ~/.config/containers
vi ~/.config/containers/registries.conf

Rootless Podman with Quadlet

The finished Quadlet file lives on my server here:

~/.config/containers/systemd/mc.container

That location matters. For rootless Quadlet, Podman reads user unit files from paths such as ~/.config/containers/systemd/. You do not make a Quadlet rootless by putting User= into a system service. You run it in the user’s systemd context and manage it with systemctl --user.

This is the actual container definition:

[Unit]
Description=Minecraft (Podman Quadlet)
Wants=network-online.target
After=network-online.target
[Container]
Image=docker.io/05jchambers/legendary-minecraft-geyser-floodgate:latest
ContainerName=mc1
PodmanArgs=--tty
PodmanArgs=--interactive
Pull=newer
# Env
Environment=TZ=Europe/Berlin
Environment=MaxMemory=8960
Environment=ScheduleRestart=3:30
# Volume
Volume=/mnt/oraclevdb2/podmanvolumes/minecraft_1:/minecraft:Z
# Network, must exist already
Network=minecraft_default
# Ports
PublishPort=25565:25565/tcp
PublishPort=19132:19132/udp
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target

A few notes on the parts that are easy to skip over:

Pull=newer is convenient because the image is refreshed when a newer one exists. It is also a trade-off. For a kid’s Minecraft world, I still want backups before meaningful updates.

MaxMemory=8960 keeps Java from getting too optimistic. Small ARM VMs are perfectly usable, but they are still small VMs.

ScheduleRestart=3:30 uses the image’s restart scheduling. I like predictable daily restarts for Minecraft. It keeps the service boring.

Volume=/mnt/oraclevdb2/podmanvolumes/minecraft_1:/minecraft:Z keeps world data on the VM. The path must be writable by the rootless user. The :Z suffix is relevant on SELinux systems; on systems without SELinux it is not the interesting part.

TimeoutStartSec=900 is there because first start can include pulling a large image. Podman Quadlet units can hit systemd’s default startup timeout otherwise.

Important:

PodmanArgs=--tty
PodmanArgs=--interactive

These parameters are required to receive access to the console and to manage the Minecraft server itself. Without these parameters you are just able to manage the server in-game via your gaming client.

podman attach mc1
>

Create the Podman Network

The Quadlet references a network:

Network=minecraft_default

Create it once as the same non-root user:

podman network create minecraft_default

Then reload and start the user service:

systemctl --user daemon-reload
systemctl --user start mc.service

If the service should keep running after the SSH session ends, enable lingering for that user:

sudo loginctl enable-linger <user>

Basic checks:

systemctl --user status mc.service
journalctl --user -u mc.service -f
podman ps

OCI Networking

This setup needs two public ingress rules:

PurposeProtocolPort
Minecraft Java EditionTCP25565
Bedrock via GeyserUDP19132
# Ports
PublishPort=25565:25565/tcp
PublishPort=19132:19132/udp

Make sure the same ports line up in all places:

I would not open broad port ranges “just in case”. Two explicit rules are enough for this setup. SSH is separate and should use key-based authentication. Password login on an internet-facing VM is just asking for noise.

Allowlist, Not Public Lobby

The Cloud part only makes the server reachable. It does not make it safe.

To ensure that only allowlisted players can join, edit the server.properties files in the minecraft home directory: /mnt/oraclevdb2/podmanvolumes/minecraft_1

white-list=true
enforce-whitelist=true

Players can then be added from the server console or by an operator:

>whitelist add PLAYERNAME
>whitelist list

With Bedrock players coming through Geyser/Floodgate, check the exact server-side player name before adding it. Depending on the Floodgate configuration, Bedrock names may have a prefix. If you allowlist the wrong string, the player still will not get in.

You can use usercache.json to verify known player names.

Also keep allowlist and operator status mentally separate. Allowlisted means “may join”. Operator means “may administer”. Kids do not need operator rights just to play. Well, they may disagree, of course. The LuckPerms plugin may help to sort this out.

Plugins can be installed in the plugins/ directory. Copy the plugin JAR file there and ensure that the rootless service user has the required ownership and permissions.

Some recommended plugins, for a start:

  • Multiverse*
  • LuckPerms
  • ViaVersion & ViaBackwards

Day-to-Day Operations

The service is intentionally boring:

systemctl --user status mc.service
journalctl --user -u mc.service -f
podman logs -f mc1
podman exec -it mc1 bash

After making changes to the mc.container Quadlet:

systemctl --user daemon-reload
systemctl --user restart mc.service

Manage the server via console:

podman attach mc
> version
[17:55:04 INFO]: Checking version, please wait...
[17:55:04 INFO]: This server is running Paper version 26.1.2-74-ver/26.1.2@e4e17fc (2026-07-06T16:51:09Z) (Implementing API version 26.1.2.build.74-stable)
You are running the latest version

To detach from an attached console without stopping the container, press Control + P and Control + Q.

What I Like About This Setup

OCI Free Tier gives enough resources to run something real. Podman gives me a rootless container runtime that integrates cleanly with systemd. Quadlet keeps the container definition as a normal text file instead of a half-remembered shell command.

The design is not clever, and that is the point. One VM, one container, explicit ports, persistent data, allowlist enabled.

Limits

I would not use this exact setup for a large public server. At that point I would care more about monitoring, DDoS exposure, update windows, backup retention, plugin review, resource planning, and probably a paid account with clearer operational expectations.

Free Tier capacity is also not a promise that your shape is always available in every region. Oracle documents both capacity constraints and idle reclamation behavior. Read those parts before assuming that “free” means “production-grade forever”.

For my use case, it works: a small allowlisted Minecraft server for my son and his friends, plus a useful reason to run rootless Podman on OCI instead of only reading about it.

Sources