Getting Started with Podman Quadlets

March 31, 2026
Written by
Reviewed by

Getting Started with Podman Quadlets

For a while, my home server ran Ubuntu with a single Docker Compose file that grew every time I added a service. I tried splitting it into separate stacks and adding management tools to handle auto-updates, monitoring, and restarts, but each layer added its own complexity. Recently, I rebuilt my server on Fedora CoreOS and migrated from Docker to Podman, and Quadlets quickly became my favorite part of the setup.

With Podman Quadlets, each container gets a declarative systemd unit file, and systemd handles auto-updates, restarts, and boot ordering the same way it does for every other service. If you use Docker or Docker Compose today, the concepts will feel familiar. In this post, you will translate a Docker Compose service into a Podman Quadlet using Syncthing, an open source file synchronization tool, as the example.

Prerequisites

To follow along with this tutorial, you will need:

  • A Linux system with Podman installed (version 4.4 or later for Quadlet support)
  • Basic familiarity with containers (Docker or Podman)
  • Basic knowledge of systemd (systemctl, journalctl)

What is Podman?

Podman is a container engine that is CLI-compatible with Docker. Podman is OCI (Open Container Initiative)-compatible, so any image you pull from Docker Hub, GitHub Container Registry, or any other OCI registry works without changes.

You can run most docker commands by replacing docker with podman. The official documentation even suggests aliasing them:

alias docker=podman
Two architectural details set Podman apart:
  • No daemon: Podman uses a fork/exec model, where each podman run call spawns the container as a direct child process, monitored by a lightweight process called conmon. There is no long-running background service that all containers depend on.
  • Rootless natively: Podman runs containers as your regular user when invoked without sudo. It uses Linux user namespaces to map UIDs inside the container to an unprivileged sub-UID range on the host. Container storage lives at ~/.local/share/containers/ instead of /var/lib/docker/. Running sudo podman gives you rootful behavior, but rootless is the default path.

Because there is no daemon, there is nothing built into Podman to restart your containers after a reboot. You could reach for podman compose to keep a familiar workflow, but it is a thin wrapper around an external compose provider like docker compose or podman-compose (a separate community-maintained Python project, not a Podman-maintained tool), and it does not natively integrate with systemd the way Quadlets do. Podman Quadlets offer a cleaner alternative: they define containers as systemd unit files so the OS manages them directly.

What are Quadlets?

Quadlets are declarative configuration files that tell Podman's systemd generator how to run containers. They use the same INI format as regular systemd unit files, with an added [Container] section for Podman-specific options.

When you run systemctl daemon-reload or the system boots, a generator binary reads your Quadlet files and produces standard .service unit files. From that point on, systemd manages the container like any other service.

Quadlet files use specific extensions depending on what they define:

  • .container for container definitions
  • .volume for named volumes
  • .network for network definitions
  • .build, .pod, .kube, and more are also supported

For a rootless setup, place your Quadlet files in ~/.config/containers/systemd/. Create this directory if it does not exist:

mkdir -p ~/.config/containers/systemd

Writing your first Quadlet

To see how Quadlets work in practice, you will set up a Syncthing container. Syncthing makes a good example because it needs environment variables, a volume mount, and host networking, which covers the most common Quadlet configuration options.

The Docker Compose equivalent

If you have used Docker Compose before, you might define this service like this:

services:
  syncthing:
    image: syncthing/syncthing
    container_name: syncthing
    hostname: syncthing
    environment:
      - PUID=1000
      - PGID=1000
      - STGUIADDRESS=
    volumes:
      - /home/user/sync:/var/syncthing
    network_mode: host
    restart: unless-stopped

This pulls the official Syncthing image, sets user and group IDs, and restricts the web GUI to localhost. It mounts a directory for configuration and synced files, shares the host network for LAN device discovery, and restarts automatically. The Quadlet you are about to write accomplishes the same thing, but through systemd.

Creating the container unit file

Create a file called syncthing.container in ~/.config/containers/systemd/:

[Unit]
Description=Syncthing file synchronization
After=network-online.target

[Container]
Image=docker.io/syncthing/syncthing:latest
ContainerName=syncthing
AutoUpdate=registry
Network=host
Environment=PUID=1000
Environment=PGID=1000 
Environment=STGUIADDRESS=
Volume=/home/user/sync:/var/syncthing:Z

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

The following sections break down each part.

The [Unit] section

[Unit]
Description=Syncthing file synchronization
After=network-online.target

This is standard systemd. After=network-online.target ensures the network is available before starting the container, the same way you would configure any networked service.

The [Container] section

[Container]
Image=docker.io/syncthing/syncthing:latest
ContainerName=syncthing
AutoUpdate=registry
Network=host
Environment=PUID=1000
Environment=PGID=1000 
Environment=STGUIADDRESS=
Volume=/home/user/sync:/var/syncthing:Z

Image is the fully-qualified image name. You must include the registry prefix (docker.io/) for the AutoUpdate=registry feature to work. Podman needs to know which registry to check for newer image digests.

ContainerName sets the container's name, equivalent to container_name in a Compose file.

AutoUpdate=registry enables automatic image updates. When the podman-auto-update timer runs, Podman compares the local image digest against the remote registry and pulls a new version if one is available.

Network=host shares the host's network stack with the container. Syncthing needs this to access the host's network interfaces for LAN device discovery. In a Compose file, this is network_mode: host.

Environment sets environment variables on a single line, space-separated. PUID and PGID control the user and group ID that Syncthing runs as inside the container. STGUIADDRESS set to an empty string restricts the web GUI to localhost.

Volume mounts the host directory into the container. The :Z suffix is an SELinux (Security-Enhanced Linux) relabel flag. On SELinux-enforcing systems like Fedora, this tells Podman to set the correct security context on the volume so the container can read and write to it. If your system does not use SELinux (like Ubuntu or Debian), you can omit the :Z.

The [Service] and [Install] sections

[Service]
Restart=on-failure
[Install]
WantedBy=default.target

These are standard systemd directives that pass through to the generated service file unchanged.

Restart=on-failure tells systemd to restart the container if it exits with a non-zero status. This is equivalent to restart: unless-stopped in a Compose file. You could also use Restart=always, but on-failure avoids restarting containers you intentionally stopped.

WantedBy=default.target is the rootless equivalent of WantedBy=multi-user.target. It tells systemd to start this service when the user session starts. Combined with lingering (covered below), this means the container starts at boot.

Using published ports instead of host networking

If you do not need LAN device discovery you can use explicit port mappings instead of host networking. Replace Network=host with PublishPort directives:

[Container]
Image=docker.io/syncthing/syncthing:latest
ContainerName=syncthing
AutoUpdate=registry
PublishPort=8384:8384
PublishPort=22000:22000/tcp
PublishPort=22000:22000/udp
PublishPort=21027:21027/udp
Environment=PUID=1000
Environment=PGID=1000 
Environment=STGUIADDRESS=
Volume=/home/user/sync:/var/syncthing:Z

Port 8384 is the Syncthing web GUI, port 22000 handles the sync protocol over both TCP and UDP, and port 21027 is used for device discovery broadcasts.

Adding a named volume

If you prefer a Podman-managed named volume instead of a bind mount, create a separate syncthing.volume file in the same directory:

[Volume]

A minimal .volume file with an empty [Volume] section creates a named volume with defaults. By default, the filename determines the volume name: syncthing.volume produces a volume called systemd-syncthing. You can also set the name explicitly and configure other options inside the [Volume] section. See the Podman Quadlet volume documentation for the full list of fields.

To use it in your container file, reference the .volume file by name:

Volume=syncthing.volume:/var/syncthing:Z

When the Quadlet generator sees this reference, it automatically creates a dependency between the container service and the volume service. The volume is created before the container starts.

I like to combine both approaches: a named volume for application configuration that Podman manages, and a bind mount for data I want to access outside the container. Your final syncthing.container file will look like this:

[Unit]
Description=Syncthing file synchronization
After=network-online.target

[Container]
Image=docker.io/syncthing/syncthing:latest
ContainerName=syncthing
AutoUpdate=registry
PublishPort=8384:8384
PublishPort=22000:22000/tcp
PublishPort=22000:22000/udp
PublishPort=21027:21027/udp
Environment=PUID=1000
Environment=PGID=1000 
Environment=STGUIADDRESS=
Volume=syncthing.volume:/var/syncthing/config:Z
Volume=/home/user/data:/data:z

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

The named volume holds Syncthing's configuration files, while the bind mount at /home/user/data keeps synced files on the host where you can browse and edit them directly.

A note for users with SELinux enabled: the lowercase :z on the bind mount instead of the uppercase :Z used on the named volume. The uppercase :Z labels the volume as private to a single container, while the lowercase :z labels it as shared. Because you might access the data directory from the host or mount it into another container, the shared label is the safer choice here.

Starting and managing the service

With the Quadlet file in place, reload systemd so the generator picks it up:

systemctl --user daemon-reload

Quadlet files are only processed during daemon-reload or at boot. Editing a .container file does nothing until you reload.

Start the service:

systemctl --user start syncthing.service

The service name matches the filename: syncthing.container becomes syncthing.service.

Enable it to start automatically:

systemctl --user enable syncthing.service

Check the status:

systemctl --user status syncthing.service

View logs:

journalctl --user -u syncthing.service -f

Stop and restart work exactly as you would expect:

systemctl --user stop syncthing.service
systemctl --user restart syncthing.service

Enabling automatic image updates

The AutoUpdate=registry field in the Quadlet tells Podman to check for newer images. To activate this, enable the built-in auto-update timer:

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

This runs daily by default. You can also trigger a manual check:

podman auto-update --dry-run

The --dry-run flag shows what would be updated without making changes.

Enabling lingering for persistent services

By default, rootless systemd user services stop when the user logs out. On a server where you want containers to run continuously, enable lingering:

loginctl enable-linger $(whoami)  # loginctl manages systemd login sessions

This tells systemd to start your user session at boot and keep it running even when you are not logged in. Without this, your Syncthing container would stop every time your SSH session ends.

Debugging Quadlet generation

If a service fails to start or does not appear after daemon-reload, you can preview what the generator produces without actually loading it:

QUADLET_UNIT_DIRS=~/.config/containers/systemd \
  /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

This outputs the generated .service file so you can spot syntax errors or misconfigured fields.

A note on file ownership with rootless Podman

When running containers rootlessly, Podman uses Linux user namespaces to remap UIDs. A process running as UID 1000 inside the container maps to a sub-UID from the range defined in /etc/subuid on the host. When a container writes files to a bind-mounted volume, those files appear owned by that sub-UID on the host, not by your user account.

This means that if Syncthing creates a file inside the container as UID 1000, you might see it owned by something like UID 100999 when you list the directory on the host. This can cause permission issues if you need to access synced files outside the container.

The behavior varies depending on your OS and Podman version. Some distributions handle this transparently. For others, you may need to adjust PUID/PGID values or explore the --userns=keep-id flag, which maps your host UID directly into the container. Test on your specific setup and check the Podman rootless documentation if you encounter ownership issues.

Testing

Once the service is running, verify that Syncthing is working by opening the web GUI in your browser:

http://localhost:8384

If you set STGUIADDRESS= (empty string), the GUI is only available from localhost. You should see the Syncthing dashboard where you can add folders and connect devices.

Check that the container is running with Podman:

podman ps

You should see your syncthing container listed. Verify that the volume mount is correct by checking that Syncthing's configuration directory exists. If you used the bind mount approach:

ls /home/user/sync/config

If you used the named volume approach with syncthing.volume, inspect the volume to find its mount point and verify the contents:

podman volume inspect systemd-syncthing
podman run --rm -v systemd-syncthing:/data:Z busybox ls /data/config

The second command creates a temporary busybox container with the named volume mounted, lists the config directory contents, and removes the container when it exits. This is a common way to peek inside named volumes since they don't have a host path you can browse directly.

To confirm systemd integration is working, try rebooting and verifying that the container starts automatically (assuming you enabled lingering and the service):

systemctl reboot
# After reboot:
systemctl --user status syncthing.service

Conclusion

You built a Podman Quadlet from scratch, defining a Syncthing container as a systemd unit file with environment variables, volume mounts, and host networking. Your container is now managed with the same systemctl commands you use for every other service on the system, with proper restart behavior, dependency ordering, and boot integration. If you are coming from Docker Compose, the concepts map closely, but Quadlets remove the extra layer of orchestration by letting systemd manage containers directly.

With this approach, managing services becomes noticeably simpler. Each one is a single file, and every tool I need is already built into the OS. For more details on Quadlet syntax and options, check the Podman Quadlet documentation. If you are new to Podman itself, start with the Podman introduction.

Dylan Frankcom is a Software Engineer on Twilio's Developer Content team. He's passionate about building tools that help developers learn, create, and ship faster. You can reach him at dfrankcom [at] twilio.com, find him on LinkedIn , or follow him on Github .