Skip to content

juev/podman-quadlet

Repository files navigation

Podman Quadlet

License: MIT Services Podman

Ready-to-use Podman Quadlet service definitions for systemd — a rootless, daemonless, compose-free way to run self-hosted services.

Each service is a plain .container file that systemd picks up natively via the Quadlet generator. No docker-compose, no podman-compose, no YAML — just INI files and systemctl.

.
├── configs/          Service configuration files (Caddyfile, YAML, TOML, JSON)
├── accessories/      Infrastructure and utility services
├── bookmarks/        Bookmark and read-later managers
├── git/              Git hosting
├── monitoring/       Monitoring, alerting, and observability stack
├── networks/         Shared Podman network definitions
├── notes/            Note-taking and knowledge base services
├── volumes/          Shared Podman volume definitions
├── rss/              RSS/Atom feed readers
├── vault/            Password managers
├── vpn/              VPN and proxy services
└── wiki/             Wiki engines

Why Quadlet

  • Native systemd integration — services start on boot, restart on failure, show up in journalctl
  • Rootless by default — no daemon, no root, no attack surface
  • Declarative.container files describe the desired state, systemd handles the rest
  • Auto-updatespodman auto-update pulls new images for containers with AutoUpdate=registry
  • No extra tools — ships with Podman 4.4+, nothing else to install

Quick Start

# 1. Copy Quadlet files and shared network
cp -r bookmarks/linkding ~/.config/containers/systemd/linkding/
cp networks/caddy.network ~/.config/containers/systemd/networks/

# 2. Create data directory and environment file
mkdir -p ~/volumes/linkding/data
cp bookmarks/linkding/env.example ~/volumes/linkding/.env
nano ~/volumes/linkding/.env

# 3. Reload and start
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user daemon-reload
systemctl --user start linkding

Available Services

Accessories — infrastructure and utility services (18)
Service Description
4get Privacy-respecting metasearch engine
baikal Lightweight CalDAV+CardDAV server
caddy HTTPS reverse proxy with automatic TLS (public + private Tailscale-only)
cloudflared Cloudflare Tunnel client
firefly Personal finances manager (+ PostgreSQL)
headplane Web UI for Headscale
headscale Self-hosted Tailscale control server
immich Self-hosted photo and video management (+ PostgreSQL + Valkey + ML)
jellyfin Free software media system
mtg MTPROTO proxy for Telegram
n8n Workflow automation tool
nullclaw AI assistant gateway with Telegram bot and model routing
open-webui AI chat interface with multi-model support (+ PostgreSQL + Valkey)
pocket-id Lightweight OIDC provider
searxng Privacy-respecting metasearch engine (+ Valkey)
versitygw S3-compatible gateway
webdav Basic WebDAV server
Bookmarks — bookmark and read-later services (8)
Service Description
betula Federated personal link collection manager
espial Open-source web-based bookmarking server
linkace Self-hosted link archive (+ MariaDB + Redis)
linkding Self-hosted bookmark manager
linkwarden Collaborative bookmark manager (+ PostgreSQL)
readeck Read-it-later service with full-text extraction
shiori Simple bookmarks manager written in Go
wallabag Read-later service (+ Redis)
Git — git hosting (1)
Service Description
forgejo Self-hosted lightweight software forge (Gitea fork)
Monitoring — monitoring and alerting (8)
Service Description
alertmanager Alert routing and notification manager for Prometheus
alloy OpenTelemetry Collector distribution by Grafana
grafana Observability dashboards and visualization
loki Log aggregation system by Grafana
ntfy Self-hosted push notification server
prometheus Metrics collection and alerting toolkit
prometheus-podman-exporter Prometheus exporter for Podman container metrics
snmp-exporter SNMP metrics exporter for Prometheus
Notes — note-taking services (3)
Service Description
getoutline Collaborative knowledge base (+ PostgreSQL + Redis)
silverbullet Note-taking app for the hacker mindset
standardnotes Encrypted notes (+ MySQL + Redis + LocalStack)
RSS — feed readers (4)
Service Description
freshrss Self-hosted RSS feed aggregator
fusion Lightweight self-hosted RSS aggregator
miniflux Minimalist feed reader (+ PostgreSQL)
yarr Yet another RSS reader
Vault — password storage (1)
Service Description
vaultwarden Bitwarden-compatible server written in Rust
VPN — VPN and proxy services (3)
Service Description
tor-socks-proxy Tor SOCKS5 proxy
v2ray Platform for building proxies to bypass network restrictions
wireguard Fast, modern VPN with state-of-the-art cryptography
Wiki — wiki engines (4)
Service Description
dokuwiki Simple wiki that does not require a database
mediawiki The wiki engine behind Wikipedia
mycorrhiza Filesystem and git-based wiki engine written in Go
wiki-js Powerful and extensible open-source wiki

File Layout

The repository mirrors the target server structure. Three types of files go to three different locations:

What Repo path Server path Tracked in git?
Quadlet units <category>/<service>/*.container ~/.config/containers/systemd/<service>/ yes
Shared networks networks/*.network ~/.config/containers/systemd/networks/ yes
Shared volumes volumes/*.volume ~/.config/containers/systemd/volumes/ yes
Config files configs/<service>/ ~/.config/containers/systemd/configs/<service>/ yes
Environment <category>/<service>/env.example ~/volumes/<service>/.env no (secrets)
Data volumes ~/volumes/<service>/ no
  • configs/ — service configuration files (Caddyfile, config.yaml, settings.yml, etc.). Edit example.org placeholders and copy to the server. These are safe to track in git.
  • env.example — template for .env files containing secrets (passwords, tokens, API keys). Copy to ~/volumes/<service>/.env and fill in real values. Never commit .env files.
  • volumes/ — persistent data directories on the server (databases, uploads, caches). Not part of this repo.

Deployment

Prerequisites

  • Podman 4.4+ with Quadlet support
  • systemd user session with lingering enabled (see User lingering below)
  • Privileged ports allowed for rootless users (see Privileged ports below)

Step by Step

  1. Copy .container and .network files to ~/.config/containers/systemd/<service>/
  2. Copy shared networks: cp networks/caddy.network ~/.config/containers/systemd/networks/
  3. Copy config files (if the service has them): cp -r configs/<service> ~/.config/containers/systemd/configs/
  4. Create data directories: mkdir -p ~/volumes/<service>/data
  5. Create .env from template and fill in real values:
cp <category>/<service>/env.example ~/volumes/<service>/.env
nano ~/volumes/<service>/.env
  1. Reload and start:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user daemon-reload
systemctl --user start <service>

Docker Compose vs Quadlet

Docker Compose Podman Quadlet
docker-compose up -d systemctl --user start <service>
docker-compose down systemctl --user stop <service>
restart: always [Service] Restart=always
volumes: mapping Volume= directive
networks: section Network=<name>.network referencing a .network file
environment: Environment= or EnvironmentFile=
ports: "127.0.0.1:8080:80" PublishPort=127.0.0.1:8080:80

Common Commands

export XDG_RUNTIME_DIR=/run/user/$(id -u)

systemctl --user daemon-reload              # reload unit files
systemctl --user start <service>            # start
systemctl --user stop <service>             # stop
systemctl --user restart <service>          # restart
systemctl --user status <service> --no-pager  # status
journalctl --user -u <service> -f           # follow logs

Rootless Podman Gotchas

Important. These are real-world issues encountered in production. Read this section before deploying.

User lingering

By default, systemd kills all user processes on logout. To keep services running after you disconnect from SSH, enable lingering:

sudo loginctl enable-linger $USER

Without this, all your containers will stop as soon as you log out.

Privileged ports

Rootless Podman cannot bind to ports below 1024 by default. Services like Caddy (ports 80, 443) will fail to start. To allow unprivileged users to bind low ports:

sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80

To make it permanent:

echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/podman-privileged-ports.conf
sudo sysctl --system
XDG_RUNTIME_DIR via SSH

SSH does not forward XDG_RUNTIME_DIR. Without it, systemctl --user commands fail. Always set it explicitly:

export XDG_RUNTIME_DIR=/run/user/$(id -u)
Bind mount inode trap

Rootless Podman mounts files by inode. Replacing a config file (new inode) means the container still sees the old content.

Creates a new inode (container does NOT see the change):

  • sed -i, cat > file, python open('w')

Writes to existing inode (container sees the change):

  • tee, dd, writing to an already-open fd

After replacing a file with a new inode — restart the container:

systemctl --user restart <service>
File ownership and user namespaces

Rootless Podman remaps UIDs. For services that write to bind-mounted volumes, add to [Container]:

UserNS=keep-id

This maps the host user to UID 0 inside the container. If the process runs as UID 1000:

UserNS=keep-id:uid=1000,gid=1000
Directory creation

Podman does not auto-create directories for bind mounts (unlike Docker Compose). Create them before starting:

mkdir -p ~/volumes/<service>/data
Network (pasta) and IP changes

The pasta network session initializes at container start and does not update when the host IP changes. Fix: full stop and start of all containers through systemd (not podman stop — systemd will restart them immediately):

systemctl --user stop <service1> <service2> ...
systemctl --user start <service1> <service2> ...
Shared networks

Quadlet has no external networks. Define a shared .network file and reference it:

# caddy.network
[Network]
NetworkName=caddy
# any .container file
[Container]
Network=caddy.network
Caddy config reload

Graceful reload without dropping connections:

podman exec caddy caddy reload --config /etc/caddy/Caddyfile

Only works if the Caddyfile was updated in-place (same inode). Otherwise restart the container.

Quadlet File Reference

A minimal .container file:

[Unit]
Description=My Service
After=network-online.target

[Container]
Image=docker.io/library/myimage:latest
PublishPort=127.0.0.1:8080:80
Volume=%h/volumes/myservice/data:/data
EnvironmentFile=%h/volumes/myservice/.env
Network=caddy.network
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target
Directive Meaning
%h Expands to the user's home directory
EnvironmentFile= Reads environment variables from a file (use for secrets)
AutoUpdate=registry Enables automatic image updates via podman auto-update
After=<dep>.service Ensures dependent containers start first
WantedBy=default.target Starts the service on user login (with lingering)

Contributing

  1. Fork the repository
  2. Add a new service directory with .container file(s) and env.example
  3. Follow existing conventions:
    • Config files in configs/<service>/, data volumes in %h/volumes/<service>/
    • Secrets in EnvironmentFile= (pointing to %h/volumes/<service>/.env), not inline
    • App containers on caddy.network, DB/Redis on internal <service>.network only
    • AutoUpdate=registry for public images
  4. Open a pull request

License

MIT

About

Podman Quadlet service definitions for systemd — rootless, compose-free alternative to docker-compose

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages