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
- 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 —
.containerfiles describe the desired state, systemd handles the rest - Auto-updates —
podman auto-updatepulls new images for containers withAutoUpdate=registry - No extra tools — ships with Podman 4.4+, nothing else to install
# 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 linkdingAccessories — 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 |
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.orgplaceholders and copy to the server. These are safe to track in git. - env.example — template for
.envfiles containing secrets (passwords, tokens, API keys). Copy to~/volumes/<service>/.envand fill in real values. Never commit.envfiles. - volumes/ — persistent data directories on the server (databases, uploads, caches). Not part of this repo.
- 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)
- Copy
.containerand.networkfiles to~/.config/containers/systemd/<service>/ - Copy shared networks:
cp networks/caddy.network ~/.config/containers/systemd/networks/ - Copy config files (if the service has them):
cp -r configs/<service> ~/.config/containers/systemd/configs/ - Create data directories:
mkdir -p ~/volumes/<service>/data - Create
.envfrom template and fill in real values:
cp <category>/<service>/env.example ~/volumes/<service>/.env
nano ~/volumes/<service>/.env- Reload and start:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user daemon-reload
systemctl --user start <service>| 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 |
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 logsImportant. 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 $USERWithout 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=80To make it permanent:
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/podman-privileged-ports.conf
sudo sysctl --systemXDG_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-idThis maps the host user to UID 0 inside the container. If the process runs as UID 1000:
UserNS=keep-id:uid=1000,gid=1000Directory creation
Podman does not auto-create directories for bind mounts (unlike Docker Compose). Create them before starting:
mkdir -p ~/volumes/<service>/dataNetwork (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.networkCaddy config reload
Graceful reload without dropping connections:
podman exec caddy caddy reload --config /etc/caddy/CaddyfileOnly works if the Caddyfile was updated in-place (same inode). Otherwise restart the container.
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) |
- Fork the repository
- Add a new service directory with
.containerfile(s) andenv.example - 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>.networkonly AutoUpdate=registryfor public images
- Config files in
- Open a pull request