Container + systemd

Quick Reference

# Generate systemd unit from container (Podman)
podman generate systemd --new --name myapp > ~/.config/systemd/user/container-myapp.service

# Quadlet (Podman 4.4+) - declarative container unit
# ~/.config/containers/systemd/myapp.container
[Container]
Image=nginx:alpine
PublishPort=8080:80

# Enable and start
systemctl --user daemon-reload
systemctl --user enable --now container-myapp.service

# View logs
journalctl --user -u container-myapp.service -f

Container Service Models

Why systemd Integration?

Running containers as systemd services provides:

  • Automatic startup - Containers start on boot

  • Restart policies - Automatic recovery from failures

  • Dependency management - Start order, dependencies on other services

  • Logging - Integrated with journald

  • Resource control - cgroups integration

  • Security - User services, sandboxing options

Integration Methods

Method Description Best For

podman generate systemd

Generate unit file from running container

Quick migration

Quadlet

Declarative container definition

New deployments (Podman 4.4+)

Manual unit files

Hand-crafted systemd units

Custom requirements

Docker Compose + systemd

Compose as systemd service

Docker users

Podman Generate Systemd

Generate Unit Files

# First, create container how you want it
podman run -d --name web -p 8080:80 nginx:alpine

# Generate unit file
podman generate systemd --name web

# Generate with --new flag (recreates container each start)
podman generate systemd --new --name web

# Save to user systemd directory
mkdir -p ~/.config/systemd/user
podman generate systemd --new --name web > ~/.config/systemd/user/container-web.service

# Save to system directory (root)
sudo podman generate systemd --new --name web > /etc/systemd/system/container-web.service

Generate Options

# Specify restart policy
podman generate systemd --new --restart-policy=always --name web

# Set restart timeout
podman generate systemd --new --restart-sec=10 --name web

# Custom container name in unit
podman generate systemd --new --container-prefix=app --name web
# Creates: container-app-web.service

# Time to wait for container stop
podman generate systemd --new --stop-timeout=30 --name web

# Include after dependencies
podman generate systemd --new --after=network-online.target --name web

Enable and Manage

# Reload systemd
systemctl --user daemon-reload

# Enable (start on login)
systemctl --user enable container-web.service

# Start now
systemctl --user start container-web.service

# Enable and start
systemctl --user enable --now container-web.service

# Check status
systemctl --user status container-web.service

# View logs
journalctl --user -u container-web.service
journalctl --user -u container-web.service -f  # Follow

# Stop
systemctl --user stop container-web.service

# Disable
systemctl --user disable container-web.service

User Services and Lingering

# User services stop when user logs out
# Enable lingering to keep services running

loginctl enable-linger $USER

# Verify
loginctl show-user $USER | grep Linger

# Disable
loginctl disable-linger $USER

# Check user service directory
ls ~/.config/systemd/user/

# User runtime directory
echo $XDG_RUNTIME_DIR
# Usually /run/user/UID

Quadlet (Podman 4.4+)

Quadlet provides declarative container definitions that systemd manages directly.

Container Definition

# ~/.config/containers/systemd/nginx.container
# or /etc/containers/systemd/nginx.container (system-wide)

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=nginx

# Port mapping
PublishPort=8080:80

# Volume mounts
Volume=/var/www/html:/usr/share/nginx/html:ro,Z

# Environment
Environment=NGINX_HOST=localhost

# Resource limits
PodmanArgs=--memory 512m --cpus 1

[Service]
Restart=always

[Install]
WantedBy=default.target

Available Quadlet Types

Extension Purpose

.container

Single container

.volume

Named volume

.network

Container network

.kube

Kubernetes YAML

.image

Image pull/build

.pod

Pod definition

Volume Definition

# ~/.config/containers/systemd/data.volume

[Volume]
# Creates: systemd-data volume
Label=app=myapp

[Install]
WantedBy=default.target

Network Definition

# ~/.config/containers/systemd/app-network.network

[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1
Label=app=myapp

[Install]
WantedBy=default.target

Using Quadlet

# Place files in quadlet directory
mkdir -p ~/.config/containers/systemd/

# Create container definition
cat > ~/.config/containers/systemd/web.container << 'EOF'
[Container]
Image=nginx:alpine
PublishPort=8080:80

[Service]
Restart=always

[Install]
WantedBy=default.target
EOF

# Generate and reload
systemctl --user daemon-reload

# List generated units
systemctl --user list-unit-files | grep web

# Start
systemctl --user start web.service

# Enable
systemctl --user enable web.service

Quadlet Container Options

[Container]
# Image and name
Image=docker.io/library/nginx:alpine
ContainerName=web

# Network
PublishPort=8080:80
PublishPort=8443:443
Network=host
Network=app-network.network

# Volumes
Volume=/host/path:/container/path:ro
Volume=data.volume:/var/lib/data
Mount=type=tmpfs,destination=/tmp

# Environment
Environment=KEY=value
EnvironmentFile=/path/to/env
Secret=mysecret,type=env,target=API_KEY

# User
User=1000
Group=1000
UserNS=keep-id

# Security
ReadOnly=true
NoNewPrivileges=true
DropCapability=ALL
AddCapability=NET_BIND_SERVICE
SecurityLabelType=container_t

# Health check
HealthCmd=/usr/bin/curl -f http://localhost/health
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3

# Entrypoint and command
Exec=/usr/bin/nginx -g "daemon off;"

# Labels and annotations
Label=app=web
Annotation=io.containers.autoupdate=registry

# Additional podman args
PodmanArgs=--memory 512m

Pod Definition

# ~/.config/containers/systemd/app.pod

[Pod]
PodName=app
PublishPort=8080:80
PublishPort=5432:5432

[Install]
WantedBy=default.target
# ~/.config/containers/systemd/web.container

[Container]
Image=nginx:alpine
Pod=app.pod

[Install]
WantedBy=default.target

Manual Unit Files

Basic Container Unit

# /etc/systemd/system/container-web.service
# or ~/.config/systemd/user/container-web.service

[Unit]
Description=Nginx Web Container
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Restart=always
RestartSec=10

# Container lifecycle
ExecStartPre=-/usr/bin/podman rm -f web
ExecStart=/usr/bin/podman run --rm --name web -p 8080:80 nginx:alpine
ExecStop=/usr/bin/podman stop -t 10 web

[Install]
WantedBy=multi-user.target

With Dependencies

# container-app.service
[Unit]
Description=Application Container
After=network-online.target container-db.service
Requires=container-db.service
Wants=network-online.target

[Service]
Type=simple
Restart=always
ExecStartPre=-/usr/bin/podman rm -f app
ExecStart=/usr/bin/podman run --rm --name app \
    --network=app-net \
    -e DATABASE_URL=postgres://db:5432/app \
    myapp:latest
ExecStop=/usr/bin/podman stop -t 30 app

[Install]
WantedBy=multi-user.target

Rootless User Service

# ~/.config/systemd/user/container-web.service

[Unit]
Description=Web Container (Rootless)

[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/podman run --rm --name web -p 8080:80 nginx:alpine
ExecStop=/usr/bin/podman stop -t 10 web

# User slice for cgroups
Slice=user.slice

[Install]
WantedBy=default.target

Docker with systemd

Docker Compose as Service

# /etc/systemd/system/docker-compose-app.service

[Unit]
Description=Docker Compose Application
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Individual Docker Container

# /etc/systemd/system/docker-web.service

[Unit]
Description=Web Container (Docker)
Requires=docker.service
After=docker.service

[Service]
Type=simple
Restart=always
ExecStartPre=-/usr/bin/docker rm -f web
ExecStart=/usr/bin/docker run --rm --name web -p 8080:80 nginx:alpine
ExecStop=/usr/bin/docker stop -t 10 web

[Install]
WantedBy=multi-user.target

Auto-Update Containers

Podman Auto-Update

# Label container for auto-update
podman run -d --name web \
    --label "io.containers.autoupdate=registry" \
    nginx:alpine

# Generate unit with auto-update support
podman generate systemd --new --name web

# Enable auto-update timer
systemctl enable --now podman-auto-update.timer

# Check timer
systemctl list-timers podman-auto-update

# Manual update
podman auto-update

# Dry run
podman auto-update --dry-run

Quadlet Auto-Update

[Container]
Image=nginx:alpine
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

Logging and Monitoring

Journald Integration

# Container logs go to journald by default

# View logs
journalctl --user -u container-web.service
journalctl -u container-web.service  # System service

# Follow logs
journalctl --user -u container-web.service -f

# Since boot
journalctl --user -u container-web.service -b

# Last hour
journalctl --user -u container-web.service --since "1 hour ago"

# Export logs
journalctl --user -u container-web.service -o json > logs.json

Resource Monitoring

# Service resource usage
systemctl --user status container-web.service

# cgroup info
systemd-cgls /user.slice/user-1000.slice/user@1000.service

# Container stats
podman stats web

# System resource usage
systemd-cgtop

Troubleshooting

Service Won’t Start

# Check service status
systemctl --user status container-web.service

# View recent logs
journalctl --user -u container-web.service -n 50

# Check unit file syntax
systemd-analyze verify ~/.config/systemd/user/container-web.service

# Check if container exists
podman ps -a | grep web

# Try running container manually
podman run --rm nginx:alpine

Dependency Issues

# List dependencies
systemctl --user list-dependencies container-web.service

# Check ordering
systemctl --user show container-web.service | grep -E "After|Before|Requires"

# Verify dependent services
systemctl --user status network-online.target

User Service Issues

# Check lingering
loginctl show-user $USER | grep Linger

# Enable lingering
loginctl enable-linger $USER

# Check XDG_RUNTIME_DIR
echo $XDG_RUNTIME_DIR
ls -la $XDG_RUNTIME_DIR

# User daemon status
systemctl --user status

Quadlet Issues

# Check quadlet generator
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

# Verify files in correct location
ls ~/.config/containers/systemd/

# Check for syntax errors
cat ~/.config/containers/systemd/web.container

# Reload and check
systemctl --user daemon-reload
systemctl --user list-unit-files | grep web

Quick Command Reference

# Generate unit (Podman)
podman generate systemd --new --name CONTAINER

# Quadlet location
~/.config/containers/systemd/          # User
/etc/containers/systemd/               # System

# User service management
systemctl --user daemon-reload
systemctl --user enable --now SERVICE
systemctl --user status SERVICE
journalctl --user -u SERVICE -f

# System service management
sudo systemctl daemon-reload
sudo systemctl enable --now SERVICE
journalctl -u SERVICE -f

# Enable lingering
loginctl enable-linger $USER

# Auto-update
podman auto-update
systemctl enable --now podman-auto-update.timer