사내 서버를 Rocky 9로 올리면서 Docker를 그대로 쓸지 고민하는 상황이 종종 생긴다. RHEL 계열에는 이미 Podman이 기본 저장소에 들어 있고, 데몬을 띄우지 않는 구조라 운영 부담이 다르다. 예전에 dockerd가 죽으면서 컨테이너가 통째로 내려가 당황했던 기억이 있는 사람이라면 데몬리스 구조가 왜 편한지 바로 이해할 것이다. 이 글은 그 전환을 실제 명령 순서대로 따라가 본다.
요약: RHEL, Rocky 9에서는 dnf install podman만으로 데몬 없이 컨테이너를 돌릴 수 있다. podman-docker 패키지를 깔면 docker 명령이 그대로 Podman으로 매핑되고, 일반 사용자 권한(rootless)으로 컨테이너를 실행한다. 부팅 시 자동 기동은 podman generate systemd 대신 지금 권장되는 방식인 Quadlet(.container 파일)과 systemctl --user로 잡는다.
1. Podman이 Docker와 다른 점
Podman은 상주 데몬 없이 동작하는 컨테이너 엔진이다. Docker는 dockerd라는 루트 권한 데몬이 항상 떠 있고 docker CLI가 그 데몬에 소켓으로 요청을 보낸다. Podman은 이 중간 데몬이 없다. podman run을 실행하면 그 프로세스가 직접 컨테이너를 띄우고, 컨테이너는 실행한 사용자의 자식 프로세스로 붙는다.
여기서 두 가지 실무적 차이가 나온다. 첫째, 데몬이 없으니 데몬이 죽어서 전체가 내려가는 사고가 없다. 둘째, 루트가 아닌 일반 사용자로도 컨테이너를 돌릴 수 있다. 이를 rootless 모드라고 한다. 사용자 네임스페이스(user namespace)를 써서 컨테이너 안의 root를 호스트의 일반 UID로 매핑하기 때문에, 컨테이너가 탈출해도 호스트에서는 그 사용자 권한밖에 없다.
2. 설치와 버전 확인
RHEL, Rocky, AlmaLinux 9는 기본 AppStream 저장소에 Podman이 들어 있다. 별도 저장소 추가 없이 설치한다.
$ sudo dnf install -y podman $ podman --version podman version 5.2.2
버전 번호는 배포판 업데이트 상태에 따라 다를 수 있다. RHEL 9.x 마이너 버전마다 백포트되는 Podman 버전이 다르므로 podman --version으로 실제 값을 확인한다. Quadlet은 Podman 4.4부터 들어왔고, 5.x에서는 기본 구성 요소라 별도 설치가 필요 없다.
3. docker 명령을 그대로 쓰기
손에 익은 docker 명령을 바꾸고 싶지 않다면 podman-docker 패키지를 깐다. 이 패키지는 /usr/bin/docker를 Podman으로 연결하는 래퍼를 설치한다.
$ sudo dnf install -y podman-docker $ docker run --rm docker.io/library/hello-world Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg. ... Hello from Docker!
맨 윗줄 Emulate Docker CLI using podman 메시지가 매번 뜨는 게 거슬리면 안내대로 빈 파일을 하나 만든다.
$ sudo touch /etc/containers/nodocker
패키지를 깔지 않고 개인 셸에서만 쓰겠다면 alias docker=podman을 ~/.bashrc에 넣어도 된다. 다만 스크립트나 다른 도구가 /usr/bin/docker를 직접 부르는 경우가 있어, 서버 전역으로 맞추려면 podman-docker 패키지 쪽이 확실하다.
한 가지 주의할 점은 이미지 이름이다. Docker는 태그 앞에 레지스트리를 생략하면 Docker Hub를 기본으로 붙이지만, Podman은 레지스트리를 명시하지 않으면 검색 목록을 물어보거나 실패할 수 있다. 위 예시처럼 docker.io/library/를 붙여 완전한 이름으로 적는 습관이 편하다.
4. rootless 동작 확인
일반 사용자로 로그인한 상태에서 바로 컨테이너를 띄워 본다. sudo가 없다는 점이 핵심이다.
$ id -un deploy $ podman run -d --name web -p 8080:80 docker.io/library/nginx:alpine $ podman ps CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES 3f1c2b9a4d5e docker.io/library/nginx:alpine nginx -g daemon o... Up 3 seconds 0.0.0.0:8080->80/tcp web
rootless 모드가 제대로 잡혔는지 보려면 podman info로 확인한다.
$ podman info --format '{{.Host.Security.Rootless}}'
true
여기서 자주 걸리는 부분이 서브 UID, GID 범위다. rootless 컨테이너는 /etc/subuid와 /etc/subgid에 해당 사용자에게 할당된 UID 대역이 있어야 동작한다. 최근 배포판은 사용자 생성 시 자동으로 넣어 주지만, 오래된 계정이나 LDAP 계정은 비어 있을 수 있다.
$ grep deploy /etc/subuid /etc/subgid /etc/subuid:deploy:100000:65536 /etc/subgid:deploy:100000:65536
이 줄이 없으면 podman run이 cannot find UID/GID 류 에러로 실패한다. 이럴 때는 루트로 범위를 추가한다.
$ sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 deploy $ podman system migrate
5. 1024 미만 포트를 못 여는 문제
rootless 컨테이너로 80이나 443을 직접 열려고 하면 막힌다. 1024 미만은 특권 포트라 일반 사용자가 바인딩할 수 없기 때문이다.
$ podman run -d -p 80:80 docker.io/library/nginx:alpine Error: rootlessport cannot expose privileged port 80, ... add CAP_NET_BIND_SERVICE ... or choose a larger port number
해결 방법은 두 가지다. 하나는 컨테이너를 8080 같은 상위 포트로 띄우고 앞단 Nginx나 방화벽에서 80을 8080으로 넘기는 방식이다. 다른 하나는 커널 파라미터로 특권 포트 시작 값을 낮추는 방식이다. 후자는 루트 권한으로 한 번만 설정하면 된다.
$ echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/90-rootless-ports.conf $ sudo sysctl --system
이렇게 하면 일반 사용자도 80 이상 포트를 열 수 있다. 보안 정책상 특권 포트 경계를 내리는 게 부담스러운 환경이라면 앞단 리버스 프록시 방식을 권한다. 이 서버가 온전히 컨테이너 앞단 역할까지 겸한다면 sysctl 방식이 간단하다.
6. Quadlet으로 컨테이너를 서비스로 만들기
컨테이너를 부팅 시 자동으로 띄우려면 systemd에 연결해야 한다. 예전에는 podman generate systemd로 유닛 파일을 뽑아 썼는데, 이 명령은 현재 공식 문서에서 deprecated로 표시되어 있다. 급한 버그만 고치고 새 기능은 넣지 않는다. 지금 권장되는 방식은 Quadlet이다.
Quadlet은 systemd 제너레이터다. .container, .pod, .network, .volume 같은 선언형 파일을 읽어 부팅 시점에 systemd 서비스 유닛을 자동 생성한다. 직접 [Service] 섹션을 손으로 짜는 대신, 컨테이너 정의만 적으면 나머지는 Quadlet이 만들어 준다.
rootless 사용자는 파일을 ~/.config/containers/systemd/에 둔다. nginx를 예로 web.container를 만든다.
$ mkdir -p ~/.config/containers/systemd $ vi ~/.config/containers/systemd/web.container
[Unit] Description=nginx rootless container After=network-online.target [Container] Image=docker.io/library/nginx:alpine PublishPort=8080:80 Volume=%h/web/html:/usr/share/nginx/html:ro,Z Environment=TZ=Asia/Seoul [Service] Restart=always [Install] WantedBy=default.target
[Container] 섹션 키는 CLI 옵션과 대응한다. Image는 필수이고, PublishPort는 -p, Volume은 -v, Environment는 -e에 해당한다. %h는 사용자 홈 디렉터리로 치환되는 systemd 지정자다. SELinux가 켜진 RHEL 계열에서는 볼륨 마운트에 :Z를 붙여 레이블을 맞춰야 컨테이너가 파일을 읽을 수 있다. 이걸 빠뜨리면 컨테이너 안에서 Permission denied가 난다.
7. 유닛 로드와 자동 기동
파일을 놓았다고 바로 서비스가 되는 게 아니다. systemd에 사용자 데몬을 다시 읽으라고 알려야 Quadlet이 유닛을 생성한다.
$ systemctl --user daemon-reload
$ systemctl --user start web.service
$ systemctl --user status web.service
● web.service - nginx rootless container
Loaded: loaded (/home/deploy/.config/containers/systemd/web.container; generated)
Active: active (running) since ...
서비스 이름 규칙을 알아 두면 헷갈리지 않는다. web.container 파일은 web.service 유닛을 만든다. 실제 컨테이너 이름은 기본적으로 systemd-web이 되며, [Container]에 ContainerName=web을 넣으면 원하는 이름으로 바꿀 수 있다. Quadlet이 생성한 유닛은 파일이 자동 생성물이라 systemctl --user enable 대상이 아니고, 자동 기동 여부는 .container 파일 안의 [Install] WantedBy=default.target으로 정한다.
마지막으로 놓치기 쉬운 게 lingering이다. 사용자 서비스는 그 사용자가 로그인해 있는 동안만 살아 있는 게 기본이다. 로그아웃하거나 서버가 재부팅되면 세션이 없어 컨테이너도 안 뜬다. 사용자가 로그인하지 않아도 부팅 시 서비스가 뜨게 하려면 linger를 켠다.
$ sudo loginctl enable-linger deploy $ loginctl show-user deploy --property=Linger Linger=yes
이걸 켜야 rootless 컨테이너가 진짜 서버 서비스처럼 부팅과 함께 자동으로 올라온다. 여기서 한 번 걸리는 사람이 많은데, 서비스는 잘 만들어 놓고 linger를 안 켜서 재부팅 후 컨테이너가 안 뜬다며 당황하는 경우다.
8. 정리
RHEL, Rocky 9에서 컨테이너를 새로 시작한다면 Docker를 별도로 붙일 이유가 크지 않다. dnf install podman podman-docker로 데몬 없는 엔진과 docker 명령 호환을 동시에 얻고, rootless로 권한 노출을 줄인다. 부팅 자동 기동은 Quadlet .container 파일과 systemctl --user, 그리고 loginctl enable-linger 세 가지를 함께 잡으면 끝난다. 특권 포트와 SELinux 레이블, subuid 범위만 미리 챙기면 운영 중에 당황할 일이 줄어든다.