← ~/blog

Self-Hosting on Fedora CoreOS #2: Uptime Kuma with Quadlet

Where we left off

In the first post I made the case for Fedora CoreOS as the foundation of your homelab: an immutable OS that updates itself and runs containers as its native workload. Today we stop talking and deploy our first real app - Uptime Kuma, a lightweight monitoring dashboard that pings your services and tells you when something is down.

It's the perfect first app: a single container, one port, one volume, and immediately useful. Once it's up, it will watch everything else we deploy in this series.

Why Quadlet and not docker compose

On a Debian box you'd reach for docker compose up -d and move on. On CoreOS we do it differently: containers run as systemd services through a mechanism called Quadlet, which is built into Podman.

The idea: you drop a small unit file describing the container into /etc/containers/systemd/, and Podman generates a full systemd service from it at boot. Why bother?

  • It survives reboots for free. systemd starts your container like any other service - no restart: always hacks, no login required.
  • The OS manages it. systemctl status, journalctl, dependency ordering, restart policies - all the tooling you already know.
  • No extra software. No compose binary, no root daemon. Podman and systemd are already on every CoreOS machine.

The Quadlet file

SSH into your CoreOS machine and create the file:

# /etc/containers/systemd/uptime-kuma.container
[Unit]
Description=Uptime Kuma monitoring dashboard

[Container]
Image=docker.io/louislam/uptime-kuma:1
PublishPort=3001:3001
Volume=uptime-kuma-data:/app/data
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

Four lines do the real work:

  • Image - the container to run. Always pin at least a major tag (:1), never bare latest.
  • PublishPort - Uptime Kuma listens on 3001; we expose it on the host.
  • Volume - a named Podman volume for the app's data. This is what makes upgrades and reboots painless: the container is disposable, the volume is not.
  • AutoUpdate=registry - remember the pitch about a self-maintaining server? With this label, Podman's auto-update timer checks the registry for a newer image, pulls it, and restarts the service. Your monitoring dashboard updates itself.

Load and start it:

sudo systemctl daemon-reload
sudo systemctl start uptime-kuma.service

That's it. Check it with systemctl status uptime-kuma and open http://<your-host>:3001 - you'll get Uptime Kuma's setup screen. Create the admin account and add your first monitor (the CoreOS host itself is a good start).

Turning on auto-updates

The AutoUpdate=registry label only marks the container as updatable. The thing that actually performs updates is a systemd timer, off by default:

sudo systemctl enable --now podman-auto-update.timer

Once a day Podman will check for a new uptime-kuma:1 image and roll the service if one exists. Combined with the OS updating itself through Zincati, the machine now maintains both its OS and its apps without you.

What about the firewall?

Here's something that surprises people: Fedora CoreOS ships without a host firewall enabled. The philosophy is that a container host exposes only the ports containers publish, and network policy belongs at the edge (your router, cloud security group, or Tailscale ACLs). So port 3001 is reachable on your LAN as soon as the service starts - nothing to open.

If you have added firewalld to your setup, allow the port declaratively in your Butane config so it's part of the machine's recipe:

variant: fcos
version: 1.5.0
storage:
  files:
    - path: /etc/firewalld/zones/public.xml
      mode: 0644
      contents:
        inline: |
          <?xml version="1.0" encoding="utf-8"?>
          <zone>
            <service name="ssh"/>
            <port protocol="tcp" port="3001"/>
          </zone>

Day-0 version: bake it into Butane

Everything we just did by hand can live in the Butane config you provision the machine with, which is the CoreOS way - the machine's entire state described in one file:

variant: fcos
version: 1.5.0
storage:
  files:
    - path: /etc/containers/systemd/uptime-kuma.container
      mode: 0644
      contents:
        inline: |
          [Unit]
          Description=Uptime Kuma monitoring dashboard

          [Container]
          Image=docker.io/louislam/uptime-kuma:1
          PublishPort=3001:3001
          Volume=uptime-kuma-data:/app/data
          AutoUpdate=registry

          [Service]
          Restart=always

          [Install]
          WantedBy=multi-user.target
systemd:
  units:
    - name: podman-auto-update.timer
      enabled: true

Reprovision from this config and the machine comes up with monitoring already running. That's the payoff of the immutable approach: your server is a build artifact, not a pet.

A tip if YAML isn't how you want to spend your Friday: Daedalus, the image wizard I built, generates the base of this config for you - users with SSH keys, packages, systemd units, auto-update settings - through a guided UI with a live preview. Generate the skeleton there, then paste the Quadlet file from this post into the storage.files section, and you have the full day-0 config without writing the boilerplate by hand.

Next up

Now that something is watching our services, we can deploy things worth watching. In the next post we'll set up a reverse proxy with automatic HTTPS, so every app we add from here on gets a clean hostname and a certificate instead of an ugly :port number.


This is post 2 of 13 in the Self-Hosting on Fedora CoreOS series.