# Podman Systemd & Quadlet > Run containers as systemd services — Quadlet is the modern, declarative approach. ## Overview | Method | Status | Use case | |--------|--------|----------| | `podman generate systemd` | Deprecated | Legacy, existing setups | | Quadlet `.container` files | **Current** | New deployments | ## Quadlet Quadlet converts declarative unit files into systemd services. Place files in the appropriate directory and `daemon-reload` — systemd does the rest. ### File Locations | User | Path | |------|------| | Rootless | `~/.config/containers/systemd/` | | Root | `/etc/containers/systemd/` | ### Unit Types | Extension | Purpose | |---------------|----------------------------| | `.container` | Container definition | | `.volume` | Named volume | | `.network` | Container network | | `.kube` | Kubernetes YAML deployment | | `.image` | Image pull/build | | `.build` | Image build instructions | ## .container — Single Service ```ini # ~/.config/containers/systemd/myapp.container [Unit] Description=My Application After=network-online.target [Container] Image=docker.io/library/myapp:latest ContainerName=myapp PublishPort=8080:80 Volume=myapp-data.volume:/app/data Volume=%h/git/myapp/config:/app/config:ro,Z Network=myapp.network Environment=APP_ENV=production Environment=TZ=Europe/Brussels EnvironmentFile=%h/git/myapp/.env Secret=db_pass User=1000 Exec=--port 8080 # Health check HealthCmd=curl -f http://localhost:80/health HealthInterval=30s HealthTimeout=5s HealthRetries=3 HealthStartPeriod=10s # Resource limits PodmanArgs=--memory 512m --cpus 1.5 [Service] Restart=on-failure RestartSec=5 TimeoutStartSec=60 TimeoutStopSec=30 [Install] WantedBy=default.target ``` ### Common Container Directives | Directive | Purpose | |-------------------|--------------------------------------------------| | `Image` | Container image reference | | `ContainerName` | Explicit container name | | `PublishPort` | Port mapping (`host:container`) | | `Volume` | Volume mount (named or bind) | | `Network` | Network to join (reference `.network` file) | | `Environment` | Key=value env var (repeatable) | | `EnvironmentFile` | Load env from file | | `Secret` | Mount secret from podman secret store | | `User` | Container user (UID or name) | | `Exec` | Arguments appended to entrypoint | | `Entrypoint` | Override image entrypoint | | `PodmanArgs` | Extra `podman run` flags | | `Label` | Container label (repeatable) | | `Tmpfs` | Tmpfs mount | | `ReadOnly=true` | Read-only root filesystem | | `AutoUpdate=registry` | Enable `podman auto-update` | | `Notify=healthy` | Mark service ready when healthcheck passes | | `HealthCmd` | Health check command | | `HealthInterval` | Time between health checks | | `HealthTimeout` | Max time per check | | `HealthRetries` | Failures before unhealthy | | `HealthStartPeriod` | Grace period after start | ### Specifiers | Specifier | Value | |-----------|-----------------------------| | `%h` | User home directory | | `%n` | Unit name | | `%N` | Unit name without suffix | | `%t` | Runtime directory (`$XDG_RUNTIME_DIR`) | | `%S` | State directory | ## .volume ```ini # ~/.config/containers/systemd/myapp-data.volume [Volume] VolumeName=myapp-data Label=app=myapp ``` Reference in `.container` as `Volume=myapp-data.volume:/container/path`. ## .network ```ini # ~/.config/containers/systemd/myapp.network [Network] NetworkName=myapp-net Subnet=10.89.1.0/24 Gateway=10.89.1.1 Label=app=myapp ``` Reference in `.container` as `Network=myapp.network`. ## .image ```ini # ~/.config/containers/systemd/myapp.image [Image] Image=docker.io/library/myapp:latest ``` ## .build ```ini # ~/.config/containers/systemd/myapp.build [Build] ImageTag=myapp:latest SetWorkingDirectory=unit File=Containerfile ``` Reference in `.container` as `Image=myapp.build`. ## .kube — Kubernetes YAML ```ini # ~/.config/containers/systemd/mystack.kube [Unit] Description=My application stack [Kube] Yaml=%h/git/myapp/pod.yml PublishPort=8080:80 Network=myapp.network [Service] Restart=on-failure [Install] WantedBy=default.target ``` ## Multi-Container Stack ```ini # ~/.config/containers/systemd/app.network [Network] NetworkName=app-net ``` ```ini # ~/.config/containers/systemd/app-db.volume [Volume] VolumeName=app-db-data ``` ```ini # ~/.config/containers/systemd/app-db.container [Unit] Description=PostgreSQL for app [Container] Image=docker.io/library/postgres:16-alpine ContainerName=app-db Volume=app-db.volume:/var/lib/postgresql/data Network=app.network Environment=POSTGRES_USER=appuser Environment=POSTGRES_DB=myapp EnvironmentFile=%h/.secrets/db.env HealthCmd=pg_isready -U appuser -d myapp HealthInterval=10s HealthRetries=5 [Service] Restart=on-failure [Install] WantedBy=default.target ``` ```ini # ~/.config/containers/systemd/app-web.container [Unit] Description=Application web server After=app-db.service Requires=app-db.service [Container] Image=docker.io/library/myapp:latest ContainerName=app-web PublishPort=8080:8000 Volume=%h/git/myapp/static:/app/static:ro,Z Network=app.network Environment=DB_HOST=app-db Environment=DB_NAME=myapp EnvironmentFile=%h/.secrets/db.env Notify=healthy HealthCmd=curl -f http://localhost:8000/health HealthInterval=30s HealthStartPeriod=15s [Service] Restart=on-failure RestartSec=5 [Install] WantedBy=default.target ``` ## Workflow ```bash # After creating / editing Quadlet files: systemctl --user daemon-reload # Start systemctl --user start app-web.service systemctl --user start app-db.service # Enable on boot (requires lingering) loginctl enable-linger $USER systemctl --user enable app-web.service systemctl --user enable app-db.service # Status / logs systemctl --user status app-web.service journalctl --user -u app-web.service -f # Stop systemctl --user stop app-web.service # Validate quadlet files (dry run) /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun ``` ## Rootless & Lingering Rootless (non-root) user services are tied to the user's login session by default. Without lingering, systemd kills all user services when the user logs out — containers included. ```bash # Enable lingering (services survive logout) loginctl enable-linger $USER # Check status loginctl show-user $USER --property=Linger ls /var/lib/systemd/linger/ # file per lingered user # Disable loginctl disable-linger $USER ``` | Linger state | Behavior | |---|---| | Disabled (default) | User services start on login, stop on logout | | Enabled | User services start at boot, persist after logout | Without lingering: - Containers stop when SSH session ends - Timers don't fire when logged out - `WantedBy=default.target` only activates at login With lingering: - `systemctl --user enable` services start at boot - Containers run permanently, like root services - Required for any production rootless container ### Rootless vs Root Quadlet | | Rootless | Root | |---|---|---| | Quadlet path | `~/.config/containers/systemd/` | `/etc/containers/systemd/` | | systemctl | `systemctl --user` | `systemctl` | | journalctl | `journalctl --user -u` | `journalctl -u` | | Ports < 1024 | Not allowed (unless sysctl) | Allowed | | Lingering | Required | Not applicable | | Security | No root needed | Runs as root | ## Auto-Update ```ini # In .container [Container] AutoUpdate=registry Label=io.containers.autoupdate=registry ``` ```bash # Check for updates podman auto-update --dry-run # Apply updates podman auto-update # Systemd timer (auto-runs daily) systemctl --user enable --now podman-auto-update.timer ``` ## Legacy: generate systemd ```bash # From existing container (deprecated) podman generate systemd --new --name myapp --files mv container-myapp.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now container-myapp.service ``` `--new` creates a new container each start (preferred over restarting existing). ## Secrets ```bash # Create secret echo -n "s3cret" | podman secret create db_pass - podman secret create app_key ./keyfile.txt # List / inspect podman secret ls podman secret inspect db_pass # Use in Quadlet # [Container] # Secret=db_pass # Mounted at /run/secrets/db_pass inside container # Remove podman secret rm db_pass ``` ## Gotchas - `daemon-reload` is required after any Quadlet file change — systemd caches unit definitions - Quadlet service names are derived from the filename: `myapp.container` becomes `myapp.service` - `After=` and `Requires=` reference the `.service` name, not the `.container` filename - Rootless containers need `loginctl enable-linger` or they stop when the user logs out - `%h` in Quadlet expands to home directory — don't use `$HOME` or `~` - Volume references must match the `.volume` filename: `Volume=mydata.volume:/path` - The Quadlet generator path varies by distro — check with `rpm -ql podman | grep generator` or `dpkg -L podman` - Quadlet `.build` requires the source directory — set `SetWorkingDirectory=unit` to use the Quadlet file's location - `Notify=healthy` requires a `HealthCmd` — without it, the service never reaches "ready" - `podman auto-update` only works with fully qualified image names (include registry) ## See Also - `podman` — core container commands - `podman-compose` — compose file orchestration - `systemd` — service management fundamentals - [Quadlet docs](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)