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:


← Back to Homepage