Run Container Workloads via Podman Compose on Hetzner Cloud using Fedora CoreOS
For running containers in the Cloud for your projects, you won't need sophisticated configurations, hardware or a big budget. You can use podman-compose, to get a graphical user-interface to debug and view your live instances, deploy via a simple docker-compose.yml file and be certain your TLS encryption is set up by using something like Caddy as reverse proxy.
To host your containers cheaply in the Cloud you can use Hetzner Cloud which has amazing pricing compared to major cloud providers for higher resources at low rates. Using an immutable operating system like Fedora CoreOS, the upstream basis of RHEL CoreOS (which is used by most Fortune 500 companies), or something like Flatcar Linux, will get you that last bit of reliable and secure infrastructure everyone dreams about. In fact it might even be more reliable and secure than the managed cloud offerings of current providers, which might use Ubuntu etc.
In this setup we will use Fedora CoreOS for running Podman, as both Podman and RHEL CoreOS are actively maintained and developed by RedHat. Thus Fedora CoreOS has the best free open-source compatiblity support for Podman now and future-wise. Podman is similar to Docker and very compatible to most of Docker, in that you can use it to run your containers by just providing Dockerfiles and docker-compose.yml files. The advantage of Podman is that, it is fully open-source, is running without a daemon (less overhead) and all containers are in rootless mode, which ensures maximum security.
First we have to install a few things to be able to go through with all steps. For simplicity I will assume that Go, Homebrew and Python are installed on your local machine.
go install github.com/apricote/hcloud-upload-image@latest # https://github.com/apricote/hcloud-upload-image
go install github.com/hetznercloud/cli/cmd/hcloud@latest # https://github.com/hetznercloud/cli
brew install butane # https://docs.fedoraproject.org/lt/fedora-coreos/producing-ign/
brew install podman # https://podman.io/docs/installation
pip3 install podman-compose # https://github.com/containers/podman-compose
Next we will create an ssh key called hetzner_ed25519. You can follow this tutorial on GitHub on how to create one.
Because Fedora CoreOS is immutable by nature, it can be customized before creation with an Ignition file (.ign). We will create a human-readable butane (.bu) file called fedora-coreos-config.bu
, which we will later convert to an Ingnition file via the command-line.
First we have to make sure that we provide it with a public ssh key, so that we can log into it. We will be using the user core for all configurations.
passwd:
users:
- name: core
ssh_authorized_keys:
- ssh-ed25519 <your-public-ssh-key>
Next we have to make sure that we are able to connect to the podman socket from outside the cloud server. We can create the symlink files which enable the socket and use /var/lib/systemd/linger/core
to keep this user service alive even when the core user is not logged in.
storage:
# Enable lingering
files:
- path: /var/lib/systemd/linger/core
mode: 0644
contents: { inline: "" }
directories:
- path: /home/core/.config
mode: 0755
user:
name: core
- path: /home/core/.config/systemd
mode: 0755
user:
name: core
- path: /home/core/.config/systemd/user
mode: 0755
user:
name: core
- path: /home/core/.config/systemd/user/sockets.target.wants
mode: 0755
user:
name: core
# Symlink podman.socket
links:
- path: /home/core/.config/systemd/user/sockets.target.wants/podman.socket
target: /usr/lib/systemd/user/podman.socket
user:
name: core
We will later create a reverse proxy container running Caddy that will receive the http and https connections to our server from port 80 and 443 and redirect them to the assigned container by domain name. But in Fedora CoreOS and other Unix operating systems ports under 1024 are privileged. Thus we can not bind to them from a rootless container. All containers in Podman are rootless, unless we would run it from sudo / root user, which we don't do and is less secure. So we will forward the http and https ports to the non privileged ports on 8080 and 8443 using iptables.
storage:
files:
- path: /etc/systemd/system/iptables-forward.service
mode: 0644
contents:
inline: |
[Unit]
Description=Set up iptables rules for port forwarding
After=network.target
[Service]
Type=oneshot
ExecStartPre=/bin/sh -c 'iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 || true'
ExecStartPre=/bin/sh -c 'iptables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443 || true'
ExecStart=/usr/sbin/iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
ExecStart=/usr/sbin/iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemd:
units:
- name: iptables-forward.service
enabled: true
The complete fedora-coreos-config.bu
looks as follows:
variant: fcos
version: 1.6.0
passwd:
users:
- name: core
ssh_authorized_keys:
- ssh-ed25519 <your-public-ssh-key>
storage:
# Enable lingering
files:
- path: /var/lib/systemd/linger/core
mode: 0644
contents: { inline: "" }
- path: /etc/systemd/system/iptables-forward.service
mode: 0644
contents:
inline: |
[Unit]
Description=Set up iptables rules for port forwarding
After=network.target
[Service]
Type=oneshot
ExecStartPre=/bin/sh -c 'iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 || true'
ExecStartPre=/bin/sh -c 'iptables -t nat -D PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443 || true'
ExecStart=/usr/sbin/iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
ExecStart=/usr/sbin/iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
# Create systemd user directories
directories:
- path: /home/core/.config
mode: 0755
user:
name: core
- path: /home/core/.config/systemd
mode: 0755
user:
name: core
- path: /home/core/.config/systemd/user
mode: 0755
user:
name: core
- path: /home/core/.config/systemd/user/sockets.target.wants
mode: 0755
user:
name: core
# Symlink podman.socket
links:
- path: /home/core/.config/systemd/user/sockets.target.wants/podman.socket
target: /usr/lib/systemd/user/podman.socket
user:
name: core
systemd:
units:
- name: iptables-forward.service
enabled: true
Next we convert it to an Ignition file called fedora-coreos-config.ign
via
butane -o fedora-coreos-config.ign fedora-coreos-config.bu
We create a snapshot of Fedora CoreOS on Hetzner Cloud as described in the official documentation on Fedora CoreOS for Hetzner Cloud. Make sure to create a Hetzner Cloud API token and insert it in the HCLOUD_TOKEN. In this example we use arm architecture.
IMAGE_NAME="fedora-coreos-41.20250213.0-hetzner.x86_64.raw.xz"
export HCLOUD_TOKEN="<your token>"
STREAM="stable"
HETZNER_ARCH="arm"
ARCH="aarch64"
hcloud-upload-image upload \
--architecture "$HETZNER_ARCH" \
--compression xz \
--image-path "$IMAGE_NAME" \
--labels os=fedora-coreos,channel="$STREAM" \
--description "Fedora CoreOS ($STREAM, $ARCH)"
This could take a moment. You can check if the snapshot was created on Hetzner Cloud using
hcloud image list --type=snapshot --selector=os=fedora-coreos
Now upload your ssh key to Hetzner.
SSH_PUBKEY="ssh-ed25519 <your-public-key>"
SSH_KEY_NAME="hetzner-ed25519.pub"
hcloud ssh-key create --name "$SSH_KEY_NAME" --public-key "$SSH_PUBKEY"
If the snapshot was created and the ssh key uploaded, copy the image id and ssh key name, and insert it for IMAGE_ID and SSH_KEY_NAME. Check that you use the right configuration settings like datacenter (hcloud datacenter list
) or server type (hcloud server-type list
) for your budget and usage. In this example we create an instance in Nürnberg.
IMAGE_ID="<your-image-id>"
SSH_KEY_NAME="hetzner_ed25519.pub"
DATACENTER="nbg1-dc3"
TYPE="cax11"
NAME="fedora-coreos-2vcpu-4gb-arm64-ampere-nbg1-dc3"
IGNITION_CONFIG="./fedora-coreos-config.ign"
hcloud server create \
--name "$NAME" \
--type "$TYPE" \
--datacenter "$DATACENTER" \
--image "$IMAGE_ID" \
--ssh-key "$SSH_KEY_NAME" \
--user-data-from-file "$IGNITION_CONFIG"
Next check the IP of your newly created server.
ssh core@"$(hcloud server ip "$NAME")"
Use this IP to add the remote connection to Podman on your local machine.
podman system connection add remote-hetzner --identity ~/.ssh/hetzner_ed25519 ssh://core@<server-ip>/run/user/1000/podman/podman.sock
On MacOS you can setup your ssh config like this in the file at ~/.ssh/config
Host hetzner
HostName <server-ip>
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/hetzner_ed25519
User core
Port 22
Then you can just ssh into your instance.
ssh hetzner
Once you have ssh'd into the instance you can create the Caddyfile in /var/home/core
or just ~
. As per the documentation only data in /etc
and /var
are persisted by Fedora CoreOS across updates and reboots.
nano Caddyfile
Now you can put into it the routing for your containers by domain name. Make sure to set up an A record for your domain pointing to the server's IP at your domain provider or the provider where your nameservers points to.
nginx.containers.<your-domain>.<your-domains-tld> {
reverse_proxy nginx:80
}
This Caddyfile will be used by the Caddy reverse proxy we are about to deploy.
For this we will first create an alias in your .profile or .zprofile etc. This alias will be used to run Podman commands on the your server instance instead of locally including podman-compose commands on the remote server.
alias podre="PODMAN_COMPOSE_PROVIDER='/Users/<user>/Library/Python/<python-version>/bin/podman-compose' CONTAINER_HOST='ssh://core@<server-ip>/run/user/1000/podman/podman.sock' podman --connection=remote-hetzner"
Next we will create a docker-compose.yml file.
For demonstration purposes we will create the Caddy reverse proxy and an NGINX server, to which the former will direct to via the specified proxy-network. The NGINX server will display and host a small website. But as you will see only the Caddy container will receive the connections on port 8080 & 8443 and ultimately forward them to the NGINX container.
Caddy will also make sure to provision the TLS certificates required for a https connections. Make sure not to restart Caddy too often as it can not allocate limitless certificates from the corresponding authorities. Delete the containers you don't need individually and leave Caddy on and if Caddy has issues finding a new route, just restart it.
services:
caddy:
container_name: caddy
image: docker.io/caddy:latest
ports:
- "8080:80"
- "8443:443"
volumes:
- /var/home/core/Caddyfile:/etc/caddy/Caddyfile:Z
- caddy_data:/data
networks:
- proxy-network
nginx:
container_name: nginx
image: docker.io/nginx:latest
networks:
- proxy-network
environment:
- YOUR_ENV_VARIABLE_NAME=YOUR_ENV_VARIABLE_VALUE
networks:
proxy-network:
driver: bridge
volumes:
caddy_data:
To deploy this run the command, from the same directory where the docker-compose.yml file sits.
podre compose up -d
Now you can check out the system resources your containers use via the top command.
top
Even though I host 3 containers I noticed, I don't even use 1% of my CPU yet and looking on the ram usage, I can expect to run hundreds of containers more on the cheapest plan offered by Hetzner. Please mind though all my containers run Golang, a super fast language.
To get the best experience, it is definitely worth to install Podman Desktop for a graphical user-interface and log activity of your containers or run commands inside your containers with an easy-to-use desktop app.
Sources:
- https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
- https://docs.fedoraproject.org/lt/fedora-coreos/provisioning-hetzner/
- https://github.com/containers/podman-compose/issues/849
- https://docs.fedoraproject.org/en-US/fedora-coreos/tutorial-user-systemd-unit-on-boot/