RHEL 실전 #3 컨테이너 워크로드: Podman, systemd (quadlet)

8 분 소요

#1 웹 서버와 #2 DB에서는 nginx와 PostgreSQL을 RHEL 위에 직접 설치하고 systemd로 등록했습니다. 같은 워크로드를 이번에는 컨테이너로 다시 올리겠습니다. RHEL은 Docker 대신 Podman을 표준 컨테이너 엔진으로 채택하고 있고, Podman은 데몬 없이 동작하며 일반 사용자 권한으로도 컨테이너를 돌릴 수 있습니다. 여기에 quadlet을 더하면 컨테이너를 systemd 서비스처럼 다룰 수 있어, 앞 글들에서 익힌 운영 감각을 그대로 가져올 수 있습니다.

이번 글의 흐름은 세 단계입니다. 먼저 Podman으로 컨테이너를 띄우는 기본을 잡고, 다음으로 일반 사용자로 돌리는 rootless 운영을 보고, 마지막으로 quadlet으로 systemd에 통합해 부팅 자동 시작까지 연결하겠습니다.

Podman 설치와 첫 컨테이너 #

RHEL 9 이상에는 Podman이 기본 저장소에 들어 있습니다. dnf로 설치합니다.

# 설치
sudo dnf install -y podman

# 버전 확인
podman version

Podman의 명령 체계는 Docker와 거의 같아, Docker를 쓰던 사람이면 그대로 옮겨 올 수 있습니다. #1의 nginx를 컨테이너로 띄워 보겠습니다.

# 이미지 받기 (Red Hat 레지스트리)
podman pull registry.access.redhat.com/ubi9/nginx-124

# 호스트 8080을 컨테이너 8080으로 연결해 실행
podman run -d --name web -p 8080:8080 \
  registry.access.redhat.com/ubi9/nginx-124 \
  nginx -g "daemon off;"

-d는 백그라운드 실행, --name은 컨테이너 이름, -p는 포트 연결입니다. 컨테이너가 떴는지는 podman ps로 확인하고, curl -I http://localhost:8080으로 로컬 응답을 받아 봅니다. 이미지 출처로는 Red Hat이 제공하는 UBI(Universal Base Image) 계열을 권장합니다. RHEL과 동일한 기반에서 빌드되어 호환성과 보안 업데이트 측면에서 운영 환경에 가장 잘 맞습니다.

볼륨과 환경 변수로 DB 올리기 #

#2의 PostgreSQL을 컨테이너로 올리면 컨테이너의 장점이 분명해집니다. 데이터는 호스트의 볼륨에 남기고, 컨테이너 자체는 언제든 갈아 끼울 수 있습니다.

# 데이터를 둘 호스트 디렉터리
mkdir -p ~/pgdata

# PostgreSQL 컨테이너 실행
podman run -d --name db \
  -p 5432:5432 \
  -e POSTGRESQL_USER=appuser \
  -e POSTGRESQL_PASSWORD=secret \
  -e POSTGRESQL_DATABASE=appdb \
  -v ~/pgdata:/var/lib/pgsql/data:Z \
  registry.redhat.io/rhel9/postgresql-16

-e로 초기 사용자와 데이터베이스를 환경 변수로 넘기고, -v로 호스트의 ~/pgdata를 컨테이너의 데이터 디렉터리에 연결합니다. 볼륨 경로 끝의 :Z가 RHEL에서 특히 중요합니다.

SELinux 볼륨 마운트와 :Z #

RHEL은 SELinux가 enforcing이라, 컨테이너 프로세스는 기본적으로 호스트 파일에 접근하지 못합니다. 볼륨 옵션에 :Z를 붙이면 Podman이 해당 디렉터리에 컨테이너 전용 SELinux 레이블(container_file_t)을 붙여 줘서, 컨테이너가 읽고 쓸 수 있게 됩니다.

  • :Z(대문자)는 해당 컨테이너만 쓰는 비공개 레이블로, 단일 컨테이너 데이터에 적합합니다.
  • :z(소문자)는 여러 컨테이너가 공유하는 레이블입니다. 같은 디렉터리를 여러 컨테이너가 함께 쓸 때 씁니다.

:Z를 빠뜨리면 컨테이너 로그에 권한 오류가 뜨고 DB가 초기화되지 못합니다. #1에서 본 SELinux 문제와 같은 맥락이며, 컨테이너에서는 이 한 글자로 해결됩니다.

호스트의 8080,5432를 외부에 열려면 #1과 동일하게 firewalld에서 포트를 개방합니다. 컨테이너라고 해서 방화벽이 달라지지는 않습니다.

# 포트 영구 개방 후 반영
sudo firewall-cmd --add-port=8080/tcp --permanent
sudo firewall-cmd --add-port=5432/tcp --permanent
sudo firewall-cmd --reload

DB 포트(5432)를 외부에 직접 여는 것은 운영에서 권장하지 않습니다. #2에서 다룬 대로 접근 범위를 좁히거나, 같은 호스트의 웹 컨테이너만 접근하도록 두는 편이 안전합니다.

rootless 컨테이너: 일반 사용자로 운영 #

Podman은 root 없이 일반 사용자 권한으로 컨테이너를 돌릴 수 있습니다. 이를 rootless라고 부르며, 컨테이너가 뚫려도 호스트 root로 번지지 않으므로 보안상 기본 권장입니다. 앞의 podman run 명령을 sudo 없이 일반 사용자로 실행하면 그대로 rootless로 동작합니다. rootless에는 두 가지 제약을 기억해야 합니다.

  • 1024 미만의 특권 포트는 일반 사용자가 바로 열지 못합니다. 그래서 80 대신 8080처럼 1024 이상 포트로 띄우고, 필요하면 호스트의 reverse proxy나 net.ipv4.ip_unprivileged_port_start 조정으로 80에 노출합니다.
  • 컨테이너는 그 사용자에게 종속됩니다. root가 만든 컨테이너와 일반 사용자가 만든 컨테이너는 서로 보이지 않습니다. podman ps는 현재 사용자의 컨테이너만 보여 줍니다.

rootless 컨테이너의 이미지와 저장소는 시스템 전역이 아니라 사용자 홈(~/.local/share/containers) 아래에 들어갑니다. 사용자별로 격리되는 이 구조가 rootless 보안의 바탕입니다.

quadlet으로 systemd에 통합 #

podman run으로 띄운 컨테이너는 재부팅하면 사라집니다. 운영 환경에서는 컨테이너도 #1의 nginx처럼 systemd 서비스로 관리해야 자동 재시작과 부팅 시작이 됩니다. RHEL 9.4 이상에서는 quadlet이 그 표준 방법입니다.

quadlet은 .container 같은 선언형 파일을 두면 systemd가 그것을 읽어 서비스 유닛으로 만들어 주는 구조입니다. 직접 유닛 파일을 작성하지 않고, 컨테이너 정의만 적으면 됩니다. rootless로 쓰려면 사용자 디렉터리에 파일을 둡니다.

# 사용자 quadlet 디렉터리 생성
mkdir -p ~/.config/containers/systemd

이 디렉터리에 web.container 파일을 만듭니다.

# ~/.config/containers/systemd/web.container
[Unit]
Description=Nginx web container

[Container]
Image=registry.access.redhat.com/ubi9/nginx-124
PublishPort=8080:8080
Exec=nginx -g "daemon off;"

[Service]
Restart=always

[Install]
WantedBy=default.target

[Container] 섹션이 quadlet의 핵심입니다. 주요 키는 다음과 같습니다.

  • Image: 띄울 이미지. podman run의 이미지 인자에 해당합니다.
  • PublishPort: 포트 연결. -p에 해당합니다.
  • Volume: 볼륨 마운트. -v에 해당하며 :Z도 그대로 붙입니다.
  • Environment: 환경 변수. -e에 해당합니다.

DB 컨테이너도 같은 방식으로 db.container를 만듭니다. [Container]VolumeEnvironment를 더 적는 점만 다릅니다.

# ~/.config/containers/systemd/db.container
[Container]
Image=registry.redhat.io/rhel9/postgresql-16
PublishPort=5432:5432
Volume=%h/pgdata:/var/lib/pgsql/data:Z
Environment=POSTGRESQL_USER=appuser
Environment=POSTGRESQL_PASSWORD=secret
Environment=POSTGRESQL_DATABASE=appdb

[Service]
Restart=always

[Install]
WantedBy=default.target

%h는 사용자 홈 디렉터리를 가리키는 systemd 지정자입니다. 파일을 둔 뒤에는 systemd가 새 정의를 인식하도록 daemon-reload를 한 다음 서비스를 시작합니다.

# quadlet 파일을 systemd가 읽도록 갱신
systemctl --user daemon-reload

# 서비스 시작 (파일명 web.container → web 서비스)
systemctl --user start web
systemctl --user start db
systemctl --user status web

quadlet은 web.container 파일에서 web.service라는 systemd 서비스를 자동 생성합니다. 그래서 systemctl --user start web처럼 확장자 없는 이름으로 다룹니다. 컨테이너가 systemd 서비스가 되었으므로, enable 없이도 [Install] WantedBy=default.target에 따라 사용자 세션 시작 시 함께 올라옵니다.

부팅 자동 시작: linger #

여기에 한 가지 함정이 있습니다. rootless 사용자 서비스는 그 사용자가 로그인해 있을 때만 동작하고, 로그아웃하거나 재부팅 후 로그인하지 않으면 컨테이너도 내려갑니다. 사용자가 로그인하지 않아도 서비스가 부팅 시 올라오게 하려면 linger를 켜야 합니다.

# 현재 사용자의 linger 활성화 (root 권한 필요)
sudo loginctl enable-linger $USER

# 확인
loginctl show-user $USER | grep Linger

enable-linger를 켜면 해당 사용자의 systemd 사용자 세션이 부팅 시점부터 백그라운드로 살아 있어, 로그인 여부와 무관하게 quadlet 컨테이너가 자동으로 시작됩니다. rootless로 서버를 운영할 때 반드시 함께 잡아야 하는 설정입니다.

시스템 전역 quadlet #

사용자가 아니라 시스템 전역 서비스로 컨테이너를 운영하려면 파일을 /etc/containers/systemd/에 둡니다. 형식은 동일하고, 관리 명령에서 --user만 빠집니다.

# 시스템 전역 quadlet 파일 배치
sudo cp web.container /etc/containers/systemd/

# 시스템 systemd 갱신,시작
sudo systemctl daemon-reload
sudo systemctl start web

전역 quadlet은 root 권한으로 동작하므로 linger가 필요 없고, 일반 systemd 서비스처럼 부팅 시 자동 시작됩니다. 다만 컨테이너가 root로 돌게 되어 rootless의 보안 이점은 사라집니다. 단일 사용자 서버라면 rootless + linger를, 시스템 수준 서비스로 묶어 관리해야 한다면 전역 quadlet을 택하면 됩니다.

로그와 진단 #

컨테이너가 뜨지 않거나 오류가 날 때 보는 곳은 두 군데입니다. 컨테이너 자체 로그는 podman으로, systemd 통합 후의 서비스 로그는 journalctl로 봅니다.

# 컨테이너 표준 출력 로그 (실시간은 -f)
podman logs web
podman logs -f web

# quadlet 서비스 로그 (rootless / 전역)
journalctl --user -u web
sudo journalctl -u web

DB 볼륨 권한 문제처럼 SELinux가 막는 경우는 podman logs에 권한 오류로 드러나고, 호스트 측 audit 로그(/var/log/audit/audit.log)에서 AVC denied로도 확인됩니다. 이때는 #1과 마찬가지로 볼륨에 :Z가 붙었는지부터 점검하면 대부분 풀립니다.

운영 포인트 #

  • Podman은 데몬이 없습니다. Docker처럼 상주 데몬에 의존하지 않으므로, 각 컨테이너가 systemd 서비스로 독립적으로 관리됩니다. quadlet이 그 통합 지점입니다.
  • rootless를 기본으로 두십시오. 일반 사용자로 띄우면 컨테이너 침해가 호스트 root로 번지지 않습니다. 특권 포트와 사용자 종속이라는 제약만 인지하면 됩니다.
  • 볼륨에는 :Z를 붙이십시오. SELinux enforcing 환경에서 호스트 디렉터리를 마운트할 때 거의 필수입니다. 단일 컨테이너는 :Z, 공유는 :z입니다.
  • 부팅 자동 시작은 linger입니다. rootless quadlet은 loginctl enable-linger를 켜야 사용자 로그인과 무관하게 부팅 시 올라옵니다.
  • 포트는 여전히 firewalld입니다. 컨테이너든 직접 설치든, 외부 노출에는 firewalld 개방이 필요합니다.

정리 #

이번 글에서 잡은 것:

  • Podman 기본. podman pull,podman run으로 컨테이너를 띄우고, -p,-v,-e로 포트,볼륨,환경 변수를 연결
  • rootless. 일반 사용자로 운영해 보안 격리를 얻고, 특권 포트와 사용자 종속 제약을 인지
  • SELinux 볼륨. 호스트 마운트에는 :Z(비공개),:z(공유) 레이블을 부여
  • quadlet. ~/.config/containers/systemd/*.container에 컨테이너를 선언하고 systemctl --user daemon-reload 후 시작
  • 부팅 자동 시작. rootless는 loginctl enable-linger, 전역은 /etc/containers/systemd/에 배치

다음: 모니터링 #

웹과 DB를 컨테이너로 올렸으니, 이제 이 워크로드들이 잘 돌고 있는지 들여다볼 차례입니다.

#4 모니터링: Cockpit, PCP에서는 RHEL의 웹 관리 콘솔인 Cockpit으로 시스템과 컨테이너를 한눈에 보는 법, 그리고 PCP(Performance Co-Pilot)로 성능 지표를 수집하고 추적하는 흐름을 한 사이클로 정리하겠습니다.

X