Skip to content

Instantly share code, notes, and snippets.

@mattiasb
Last active May 25, 2025 21:41
Show Gist options
  • Save mattiasb/6729a4d5be34f502f21303472f86e035 to your computer and use it in GitHub Desktop.
Save mattiasb/6729a4d5be34f502f21303472f86e035 to your computer and use it in GitHub Desktop.
Simple Quadlet server

A simple Quadlet example

Suppose we want to set up two apps (freshrss and Jellyfin) behind a reverse proxy (nginx).

Then we'd do something like this with Podman and the quadlet generator:

Base container template

/etc/containers/systemd/[email protected]:

[Unit]
Description=LinuxServer Container %i
After=local-fs.target

[Container]
ContainerName=%i
Image=lscr.io/linuxserver/%i:latest
AutoUpdate=registry
Volume=/etc/%i:/config:Z
Network=ctr.network
Environment=TZ=Europe/Stockholm

[Service]
TimeoutStartSec=30min
Restart=on-failure
RestartSec=1s
RestartSteps=5
RestartMaxDelaySec=2m 30s

[Install]
WantedBy=default.target

Network

/etc/containers/systemd/ctr.network:

[Network]
Subnet=192.168.100.0/24
Gateway=192.168.100.1

Freshrss

Just a symlink needed:

$ ln -s /etc/containers/systemd/ctr@{,freshrss}.container

Jellyfin

$ ln -s /etc/containers/systemd/ctr@{,jellyfin}.container

/etc/containers/systemd/[email protected]/1.conf:

[Unit]
# Jellyfin needs to wait for this media library mount to be ready
Requires=media.mount
After=media.mount

[Container]
Volume=/path/to/jellyfin/library:/config
Volume=/media/series:/data/series
Volume=/media/movies:/data/movies
Environment=JELLYFIN_PublishedServerUrl=http://192.168.0.5

nginx

$ ln -s /etc/containers/systemd/ctr@{,nginx}.container

/etc/containers/systemd/[email protected]/1.conf:

[Container]
Image=docker.io/library/nginx:stable-alpine
Volume=
Volume=/etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro,Z
PublishPort=80:80

/etc/nginx/nginx.conf:

user  nginx;
worker_processes  auto;

error_log  stderr notice;
pid    /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include     /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main
    '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  stdout  main;

    sendfile    on;
    keepalive_timeout  65;

    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }

    upstream freshrss {
        server freshrss:80;
    }
    upstream jellyfin {
        server jellyfin:8096;
    }

    server {
        listen     80;
        server_name  server.lan;
        location / {
            root   /usr/share/nginx/html;
            index  index.html;
        }
    }

    server {
        listen     80;
        server_name  jellyfin.lan;
        location / {
            proxy_pass http://jellyfin/;
            proxy_set_header Host              $http_host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    server {
        listen     80;
        server_name  freshrss.lan;
        location / {
            proxy_pass http://freshrss/;
            proxy_set_header Host              $http_host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Conclusion

For just two applications and one reverse proxy the above becomes a little bit chatty compared to a docker-compose.yml file with YAML anchors. But when you scale up to many more containers it's fairly similar.

Of note though we're able to reliably depend on media.mount to be mounted before starting our Jellyfin container. That kind of thing you can't do with regular docker-compose.yml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment