본문 바로가기
일반IT/리눅스

Podman Quadlet 완전 정리 — systemd로 컨테이너를 선언적으로 운영하는 방법

by gasbugs 2026. 8. 7.

 

podman run 한 줄로 컨테이너를 띄우는 일은 쉽습니다. 그러나 서버 재부팅 후 자동 시작, 비정상 종료 시 재시작, 실행 순서, 로그 조회, 리소스 제한까지 운영 문제를 붙이기 시작하면 셸 스크립트는 빠르게 복잡해집니다.

 

Podman Quadlet은 이 간극을 메웁니다. .container, .network, .volume 같은 짧은 선언 파일을 작성하면 systemd generator가 일반 서비스 유닛으로 변환합니다. 이후에는 컨테이너도 systemctl로 시작·중지하고 journalctl로 관찰할 수 있습니다.

 

결론부터 말하면 한 대 또는 소수의 Linux 서버에서 컨테이너를 OS 서비스처럼 운영하고 싶다면 Quadlet이 가장 자연스러운 선택입니다. Kubernetes처럼 여러 노드에 워크로드를 스케줄링하지는 않지만, systemd가 이미 제공하는 부팅 순서, 재시작 정책, 의존성, 로그와 권한 모델을 그대로 활용할 수 있습니다.

Quadlet이란 무엇인가

Podman 공식 문서는 Quadlet을 컨테이너, Pod, 볼륨, 네트워크와 이미지를 systemd 유닛 파일로 선언적으로 관리하는 기능으로 설명합니다. 여기서 중요한 단어는 선언적systemd입니다.

 

일반적인 podman run 명령은 “지금 이 옵션으로 컨테이너를 실행하라”는 절차입니다. Quadlet 파일은 “이 호스트에는 이 이미지, 포트, 볼륨과 재시작 정책을 가진 서비스가 있어야 한다”는 원하는 상태를 기록합니다. 실제 실행 명령은 Quadlet generator가 만듭니다.

 

과거의 podman generate systemd는 이미 만든 컨테이너나 Pod를 바탕으로 긴 .service 파일을 생성하는 방식이었습니다. 현재 공식 명령 목록에서는 이 기능이 deprecated로 표시되며, 새 구성에는 Quadlet 사용이 권장됩니다.

동작 원리: 원본은 Quadlet, 실행 주체는 systemd

Quadlet은 상시 실행되는 별도 데몬이 아닙니다. systemd가 부팅되거나 systemctl daemon-reload를 수행할 때 Podman의 generator가 검색 경로에서 Quadlet 파일을 읽습니다. 그리고 systemd가 이해하는 서비스 유닛을 런타임에 생성합니다. 이 과정과 검색 경로는 podman-systemd.unit 공식 문서에 정의되어 있습니다.

그림: Quadlet 생성 흐름 · Podman systemd unit 공식 문서를 바탕으로 재구성

예를 들어 web.container는 web.service가 되고, app.network는 app-network.service, data.volume은 data-volume.service가 됩니다. 생성 결과는 transient unit이므로 직접 편집하지 않습니다. 설정을 바꾸려면 원본 Quadlet이나 .d/*.conf drop-in을 수정하고 다시 daemon-reload 해야 합니다.

 

이 구조 덕분에 역할이 분명해집니다.

  • 사람이 관리하는 원본: *.container, *.pod, *.network 등
  • 변환 시점: 부팅 또는 systemctl daemon-reload
  • 수명주기 관리: systemctl start, stop, restart, status
  • 로그와 장애 분석: journalctl, systemd-analyze, Podman 명령

지원하는 Quadlet 파일 유형

현재 공식 문서에는 다음 여덟 가지 유형이 정의되어 있습니다.

확장자 전용 섹션 역할
.container [Container] 단일 컨테이너를 서비스로 실행
.pod [Pod] 여러 컨테이너가 참여할 Pod 생성
.network [Network] Podman 네트워크 생성
.volume [Volume] 영속 볼륨 생성·보장
.image [Image] 이미지를 미리 가져와 캐시
.build [Build] Containerfile로 로컬 이미지 빌드
.kube [Kube] Kubernetes YAML을 podman kube play로 실행
.artifact [Artifact] OCI artifact를 가져와 다른 유닛에서 사용

Quadlet 파일은 일반 systemd 문법과 전용 섹션을 함께 씁니다. [Unit], [Service], [Install]은 systemd가 처리하고, [Container] 같은 전용 섹션은 Podman generator가 podman run 옵션으로 변환합니다. 따라서 Restart=, TimeoutStartSec= 같은 익숙한 systemd 정책과 Image=, PublishPort=, Volume= 같은 컨테이너 설정을 한 파일에서 조합할 수 있습니다.

파일은 어디에 두는가

운영 주체가 root인지 일반 사용자인지에 따라 검색 경로가 달라집니다.

실행 방식 주로 사용하는 경로 systemctl 명령
rootless 사용자 서비스 ~/.config/containers/systemd/ systemctl --user ...
rootful 시스템 서비스 /etc/containers/systemd/ sudo systemctl ...
rootful 임시 테스트 /run/containers/systemd/ sudo systemctl ...
배포판 제공 rootful 유닛 /usr/share/containers/systemd/ sudo systemctl ...
관리자가 사용자별 배포 /etc/containers/systemd/users/${UID}/ 해당 사용자의 systemctl --user ...

rootless에는 $XDG_RUNTIME_DIR/containers/systemd/, $XDG_CONFIG_HOME/containers/systemd/, /etc/containers/systemd/users/와 /usr/share/containers/systemd/users/ 계열도 포함됩니다. 동일한 이름이 여러 경로에 있으면 우선순위가 적용되므로, 운영 파일과 임시 테스트 파일의 이름 충돌을 피해야 합니다.

 

Quadlet은 cgroup v2를 요구합니다. 시작 전에 다음 두 명령으로 환경을 확인하는 편이 좋습니다.

podman --version
podman info --format '{{.Host.CgroupsVersion}}'

두 번째 결과가 v2여야 합니다. 또한 Quadlet은 systemd 기능이므로 macOS나 Windows 호스트에서 직접 실행되는 것이 아닙니다. 이 환경에서는 대상 Linux 서버 또는 Podman Machine 내부의 Linux VM을 기준으로 설정해야 합니다.

실습: rootless Nginx를 Quadlet으로 실행하기

가장 안전한 출발점은 일반 사용자 권한의 rootless 서비스입니다. 다음 예제는 로컬 주소 127.0.0.1:8080에만 Nginx를 노출하고, 홈 디렉터리의 정적 파일을 읽기 전용으로 마운트합니다.

 

먼저 디렉터리와 테스트 페이지를 만듭니다.

mkdir -p ~/.config/containers/systemd ~/quadlet/site
printf '<h1>Hello from Podman Quadlet</h1>\n' > ~/quadlet/site/index.html

~/.config/containers/systemd/web.container 파일을 작성합니다.

[Unit]
Description=Rootless Nginx managed by Podman Quadlet

[Container]
Image=docker.io/library/nginx:alpine
PublishPort=127.0.0.1:8080:80
Volume=%h/quadlet/site:/usr/share/nginx/html:ro,Z
LogDriver=journald

[Service]
Restart=on-failure
TimeoutStartSec=900

[Install]
WantedBy=default.target

각 항목의 의미는 다음과 같습니다.

  • Image=는 짧은 이름 대신 레지스트리까지 포함한 이미지를 지정합니다.
  • PublishPort=127.0.0.1:8080:80은 모든 인터페이스가 아니라 로컬 호스트에만 바인딩합니다.
  • %h는 systemd의 사용자 홈 디렉터리 specifier입니다.
  • ro,Z는 읽기 전용 마운트와 SELinux private relabel을 뜻합니다. 여러 컨테이너가 같은 경로를 공유한다면 z의 의미를 검토해야 합니다.
  • 첫 이미지 pull이 systemd의 기본 시작 제한 시간을 넘을 수 있으므로 TimeoutStartSec=900으로 여유를 둡니다. 공식 문서도 느린 pull·build를 위해 이 설정을 예시로 듭니다.
  • [Install] WantedBy=default.target은 사용자 systemd가 시작될 때 이 서비스를 원하는 상태로 만듭니다.

파일을 읽게 하고 서비스를 시작합니다.

systemctl --user daemon-reload
systemctl --user start web.service
systemctl --user status web.service --no-pager
curl http://127.0.0.1:8080

로그와 컨테이너 상태는 다음처럼 확인합니다.

journalctl --user -u web.service -n 50 --no-pager
podman ps --filter name=systemd-web

ContainerName=을 따로 지정하지 않으면 Quadlet은 일반적으로 systemd- 접두사가 붙은 이름을 사용해 사용자가 수동으로 만든 컨테이너와의 충돌을 줄입니다.

systemctl enable을 실행하지 않는 이유

여기서 가장 자주 하는 실수가 생성된 web.service에 systemctl --user enable을 실행하는 것입니다. Quadlet이 만든 서비스는 transient unit이므로 일반 서비스처럼 enable할 수 없습니다. 대신 generator가 원본 Quadlet의 [Install]을 읽고 enable과 같은 연결을 생성합니다.

 

따라서 부팅·로그인 시 자동 시작이 필요하면 원본 파일에 다음을 둔 뒤 daemon-reload 합니다.

[Install]
WantedBy=default.target

rootless user manager는 보통 로그인 세션과 함께 시작됩니다. 로그인 전부터 서비스를 올리고 로그아웃 뒤에도 유지해야 한다면 관리 정책을 확인한 후 systemd lingering을 사용합니다. loginctl 공식 문서는 lingering을 활성화하면 해당 사용자의 user manager를 부팅 시 생성하고 로그아웃 후에도 유지한다고 설명합니다.

sudo loginctl enable-linger USER_NAME

lingering은 사용자가 로그인하지 않아도 장기 실행 프로세스를 유지할 수 있게 하므로, 필요한 서비스 계정에만 적용하는 편이 좋습니다.

네트워크와 볼륨을 별도 Quadlet으로 연결하기

규모가 커지면 네트워크와 볼륨도 선언 파일로 분리할 수 있습니다. app.network와 data.volume을 만들고 컨테이너에서 파일 이름으로 참조하면, Quadlet은 Podman 인자뿐 아니라 생성 서비스 사이의 의존성도 추가합니다.

그림: Quadlet 리소스 참조와 의존성 · Podman systemd unit 공식 문서를 바탕으로 재구성

app.network는 다음처럼 최소한으로 만들 수 있습니다.

[Network]
Label=app=quadlet-demo

data.volume도 별도 이름을 강제하지 않으면 Quadlet이 충돌을 피하는 이름을 생성합니다.

[Volume]
Label=app=quadlet-demo

컨테이너에서는 리소스의 실제 Podman 이름이 아니라 Quadlet 파일 이름을 참조합니다.

[Container]
Image=docker.io/library/alpine:latest
Exec=sh -c "while true; do date >> /data/heartbeat.log; sleep 60; done"
Network=app.network
Volume=data.volume:/data

[Service]
Restart=always

[Install]
WantedBy=default.target

이 파일을 worker.container로 저장하고 systemctl --user daemon-reload를 수행하면 worker.service가 네트워크·볼륨 서비스 뒤에 시작되도록 관계가 만들어집니다. 사용자가 긴 Requires=와 After= 목록을 수동으로 맞추지 않아도 되는 것이 Quadlet의 큰 장점입니다.

 

같은 방식으로 Pod=backend.pod, Image=app.build, Image=base.image처럼 다른 Quadlet을 참조할 수 있습니다. 복잡한 서비스 간 실행 순서는 [Unit]의 Requires=, After=, Before=로 명시할 수 있으며, Quadlet 파일 이름 사이의 의존성도 생성 서비스 이름으로 변환됩니다.

설치·수정·삭제의 운영 흐름

수동으로 검색 경로에 복사하는 방식 외에, 지원되는 Podman 버전에서는 podman quadlet 명령을 사용할 수 있습니다.

podman quadlet install ./web.container
podman quadlet list
podman quadlet print web.container
podman quadlet rm web.container

podman quadlet install은 현재 사용자 경로에 파일을 설치하고 기본적으로 systemd reload도 수행합니다. 여러 Quadlet과 설정 파일을 하나의 application으로 설치하는 기능도 있으므로, 배포 도구에서는 개별 파일 복사보다 일관된 패키징에 유용합니다.

 

수동 관리라면 수정 순서는 단순합니다.

# 1. 원본 web.container 수정
# 2. 생성 서비스 갱신
systemctl --user daemon-reload

# 3. 실행 중인 서비스에 새 설정 적용
systemctl --user restart web.service

daemon-reload는 정의를 다시 생성할 뿐 이미 실행 중인 컨테이너를 자동으로 교체하지 않습니다. 변경 적용을 위해서는 restart가 별도로 필요합니다.

장애 분석: service not found부터 확인한다

Quadlet 문법이 잘못되었거나 설치된 Podman보다 새로운 옵션을 사용하면 generator가 .service를 만들지 못할 수 있습니다. 이때 Unit web.service not found만 보고 이미지 문제로 오해하기 쉽습니다.

 

먼저 공식 문서가 안내하는 검증 명령을 실행합니다.

systemd-analyze --user --generators=true verify web.service

생성 결과와 오류를 함께 보고 싶다면 generator의 dry-run을 사용합니다. 배포판에 따라 실행 파일 경로가 다를 수 있으므로 실제 설치 경로도 확인해야 합니다.

/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

그 다음 계층별로 확인합니다.

# 생성된 systemd 정의 확인
systemctl --user cat web.service

# systemd 상태와 최근 로그
systemctl --user status web.service --no-pager
journalctl --user -u web.service -b --no-pager

# Podman이 본 컨테이너 상태
podman ps --all --filter name=systemd-web
podman inspect systemd-web

첫 실행만 90초 근처에서 실패한다면 이미지 pull 시간을 의심하고, 포트 바인딩 오류라면 기존 프로세스나 컨테이너가 같은 포트를 사용 중인지 확인합니다. SELinux 환경에서 bind mount 접근이 거부되면 무조건 보안 레이블을 끄기보다 :Z 또는 공유 목적의 :z가 맞는지부터 점검해야 합니다.

자동 업데이트는 편리하지만 배포 정책이다

Quadlet의 [Container]에는 AutoUpdate=registry 또는 AutoUpdate=local을 지정할 수 있습니다. Podman auto-update 문서에 따르면 registry 정책은 원격 이미지 digest가 달라졌는지 확인하고, 새 이미지를 pull한 뒤 해당 systemd 서비스를 재시작합니다. 이 정책에는 레지스트리를 포함한 완전한 이미지 이름이 필요합니다.

[Container]
Image=quay.io/example/myapp:stable
AutoUpdate=registry

업데이트 가능 여부는 실제 변경 없이 먼저 확인할 수 있습니다.

podman auto-update --dry-run

Podman은 podman-auto-update.service와 timer를 제공하지만, 자동 갱신을 켜기 전에 다음을 결정해야 합니다.

  • 가변 태그를 즉시 받아도 되는가, 아니면 digest로 버전을 고정해야 하는가
  • 새 이미지가 실패하면 누가 감지하고 롤백하는가
  • 데이터 마이그레이션과 하위 호환성이 보장되는가
  • 사설 레지스트리 인증 파일의 수명과 권한은 적절한가

자동 업데이트는 보안 패치를 빠르게 반영할 수 있지만, 검증되지 않은 변경을 운영에 자동 배포할 수도 있습니다. 중요 서비스에서는 CI에서 이미지를 검증하고 digest를 갱신하는 방식이 더 예측 가능할 수 있습니다.

보안과 안정성을 높이는 설정

Quadlet이 rootless를 쉽게 만든다고 해서 컨테이너가 자동으로 안전해지는 것은 아닙니다. 이미지, 포트, 마운트, capability와 비밀의 경계는 여전히 설계해야 합니다.

1. rootless를 기본값으로 둔다

특권 포트, 장치 접근, 호스트 네트워크처럼 root가 꼭 필요한 기능이 없다면 일반 사용자 Quadlet으로 시작합니다. rootful Quadlet에 [Service] User=를 넣어 rootless로 바꾸는 방식은 지원되지 않습니다. rootless는 사용자 검색 경로와 systemctl --user를 사용해야 합니다.

2. 포트 노출 범위를 명시한다

PublishPort=8080:80은 모든 IPv4 인터페이스에 열릴 수 있습니다. 리버스 프록시 뒤의 서비스나 로컬 관리 UI라면 127.0.0.1:8080:80처럼 바인딩 주소를 명시합니다.

3. 비밀을 Environment=에 직접 쓰지 않는다

Quadlet은 Podman secret을 연결하는 Secret=을 지원합니다. 비밀번호와 토큰을 선언 파일, 생성 서비스, 상태 출력에 평문으로 남기기보다 podman secret create로 별도 관리하고 필요한 컨테이너에만 전달합니다.

4. 쓰기·권한·자원을 줄인다

애플리케이션이 허용한다면 다음 옵션을 검토합니다.

[Container]
NoNewPrivileges=true
DropCapability=all
ReadOnly=true
Memory=512m
PidsLimit=256

ReadOnly=true를 적용할 때는 애플리케이션이 써야 하는 경로를 volume이나 tmpfs로 분리해야 합니다. capability를 모두 제거한 뒤에는 실제 필요한 권한만 AddCapability=으로 되돌립니다.

5. 이미지 이름과 변경 전략을 고정한다

프로덕션에서는 짧은 이미지 이름을 피하고 레지스트리, 저장소, 태그 또는 digest를 명시합니다. 태그 기반 자동 업데이트와 digest 기반 재현성은 서로 다른 운영 전략이므로 한 파일에 무심코 섞지 않습니다.

Quadlet, Compose, Kubernetes 중 무엇을 선택할까

방식 잘 맞는 상황 장점 한계
podman run·셸 스크립트 일회성 테스트, 디버깅 빠르고 직접적 재부팅·의존성·상태 관리가 수동
Podman Quadlet 단일 Linux 호스트의 장기 실행 서비스 systemd 통합, rootless, 선언 파일, journal 다중 노드 스케줄러가 아님
Compose 개발·테스트용 다중 컨테이너 묶음, Compose 생태계 서비스 관계를 한 파일로 표현, 높은 친숙도 호스트 부팅·systemd 운영은 별도 통합 필요
Kubernetes 다중 노드, 복제, 롤링 배포, 서비스 디스커버리 클러스터 수준 자동화와 생태계 구축·운영 복잡도와 비용이 큼

Quadlet은 “작은 Kubernetes”가 아닙니다. 노드 장애 시 다른 서버로 재배치하거나 여러 복제본의 desired state를 클러스터 전체에서 맞추지 않습니다. 대신 systemd가 이미 표준인 Linux 호스트에서 컨테이너를 다른 OS 서비스와 같은 방식으로 운영하게 해줍니다.

 

특히 홈랩, 엣지 장비, 빌드 서버, 내부 도구, 단일 VM의 웹 서비스, Kubernetes를 올리기에는 과한 소규모 운영 환경에 잘 맞습니다.

자주 하는 실수 여섯 가지

  1. 생성된 .service를 편집한다. 다음 daemon-reload에서 다시 만들어지므로 원본 Quadlet이나 drop-in을 수정해야 합니다.
  2. systemctl enable을 실행한다. Quadlet 원본의 [Install]에 WantedBy=를 둡니다.
  3. rootless 파일을 /etc/systemd/system에 둔다. ~/.config/containers/systemd/와 systemctl --user를 사용합니다.
  4. 짧은 이미지 이름과 열린 포트를 그대로 쓴다. 레지스트리를 포함하고 필요한 주소에만 바인딩합니다.
  5. daemon-reload만 하고 적용됐다고 생각한다. 실행 중인 서비스는 별도로 restart해야 합니다.
  6. Quadlet을 클러스터 오케스트레이터로 기대한다. 호스트 간 스케줄링과 고가용성이 필요하면 Kubernetes 등 다른 계층이 필요합니다.

마무리

Podman Quadlet의 핵심은 컨테이너를 systemd 바깥의 특별한 대상으로 두지 않는 데 있습니다. 사람이 관리하는 짧은 선언 파일은 generator가 서비스로 바꾸고, systemd는 부팅, 순서, 재시작과 로그를 담당하며, Podman은 컨테이너·네트워크·볼륨을 실행합니다.

 

처음에는 web.container 하나로 시작해 보세요. 그 다음 네트워크와 볼륨을 별도 Quadlet으로 나누고, rootless·포트 범위·비밀·리소스 제한을 다듬으면 됩니다. 단일 Linux 호스트에서 운영 복잡도를 크게 늘리지 않으면서도 재현 가능한 컨테이너 서비스를 만들고 싶다면 Quadlet은 매우 실용적인 기준점입니다.

공식 참고 자료