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 hoursand9,000 GB hoursper month, and says that for Always Free tenancies this is equivalent to2 OCPUsand12 GBof 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:
- It combines Java and Bedrock: Minecraft Java + Bedrock Server Together – Geyser + Floodgate
- Works on ARM
- Although it was designed for Docker, it also works with Podman.
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:
| Purpose | Protocol | Port |
|---|---|---|
| Minecraft Java Edition | TCP | 25565 |
| Bedrock via Geyser | UDP | 19132 |
# Ports
PublishPort=25565:25565/tcp
PublishPort=19132:19132/udp
Make sure the same ports line up in all places:
PublishPortin the Quadlet file- the VM firewall, if enabled
- OCI Security List or Network Security Group
- routing through the Internet Gateway in the Public Subnet
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
- Oracle Cloud Infrastructure Documentation: Always Free Resources
- Podman Documentation: podman-systemd.unit, Quadlet and rootless search paths
- Docker Hub: 05jchambers/legendary-minecraft-geyser-floodgate
- GitHub, linked from the Docker Hub description: TheRemote/Legendary-Java-Minecraft-Paper