이미지는 읽기 전용 파일 묶음이고 컨테이너는 그 이미지를 실행한 프로세스입니다. 이미지 하나로 컨테이너를 여러 개 띄울 수 있습니다.
| 개념 | 비유 |
|---|---|
| 이미지 | jar 파일 |
| 컨테이너 | 실행 중인 java 프로세스 |
| 레지스트리 | 이미지 저장소 (사내 Nexus 등) |
| 볼륨 | 컨테이너 밖에 두는 폴더 |
컨테이너를 지우면 안의 파일도 사라집니다. 로그와 데이터는 볼륨으로 밖에 둡니다.
이미지를 만드는 레시피입니다. docker 의 Dockerfile 과 같은 문법이고 이름만 다릅니다.
| 지시어 | 뜻 |
|---|---|
FROM |
베이스 이미지 |
USER |
실행 사용자 |
WORKDIR |
작업 폴더 |
COPY |
파일 넣기 |
ENV |
환경 변수 |
EXPOSE |
문서용 포트 표시 |
ENTRYPOINT |
시작 명령 |
FROM registry.access.redhat.com/ubi9/openjdk-21-runtime 이 RHEL 계열의 공식 자바 베이스입니다. JDK 가 이미 들어 있어 jar 만 복사하면 됩니다.
컨테이너의 첫 프로세스가 PID 1 이고 podman stop 은 그 프로세스에 TERM 을 보냅니다. 셸이 PID 1 이면 자바에 신호가 전달되지 않습니다.
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /opt/myapp/app.jar"]exec 가 셸을 자바로 교체해 자바가 PID 1 이 됩니다. 운영 묶음 03 의 완전 종료 흐름이 컨테이너에서도 그대로 동작하려면 이 형태가 필요합니다. $JAVA_OPTS 를 펼치기 위해 sh -c 를 거칩니다.
podman build -t myapp:1.0 . # 현재 폴더의 Containerfile
podman images # 목록
podman tag myapp:1.0 myapp:latest # 별칭
podman rmi myapp:1.0 # 삭제
podman image prune -f # 안 쓰는 중간 이미지 정리-t 이름:태그 에서 태그를 버전으로 씁니다. latest 만 쓰면 어느 버전이 돌고 있는지 알 수 없어 롤백이 어렵습니다.
podman run -d --name myapp -p 8080:8080 \
-v /opt/myapp/logs:/opt/myapp/logs:Z \
-e JAVA_OPTS='-Xmx512m' \
--restart=on-failure myapp:1.0| 옵션 | 뜻 |
|---|---|
-d |
백그라운드 |
--name |
이름 |
-p 호스트:컨테이너 |
포트 연결 |
-v 호스트:컨테이너:Z |
볼륨, SELinux 라벨 |
-e |
환경 변수 |
--restart |
죽으면 재시작 |
:Z 가 없으면 SELinux 가 컨테이너의 폴더 접근을 막아 Permission denied 가 납니다. 10 레슨의 SELinux 규칙이 여기서 볼륨 라벨로 나타납니다.
| 명령 | 뜻 |
|---|---|
podman ps |
실행 중 목록 |
podman ps -a |
정지된 것 포함 |
podman logs -f --tail 50 myapp |
로그 따라가기 |
podman stats --no-stream |
CPU·메모리 |
podman exec -it myapp sh |
안에서 셸 |
podman inspect myapp |
상세 JSON |
podman logs 는 앱의 stdout 을 보여 줍니다. 파일 로그를 쓰는 앱은 볼륨으로 뺀 폴더를 tail -f 합니다. exec 로 들어가 jps·jstack 을 쓸 수 있어 운영 묶음 09 의 진단이 그대로 됩니다.
podman stop -t 30 myapp # TERM 후 30초 기다리고 KILL
podman start myapp
podman restart myapp
podman rm -f myapp # 실행 중이어도 삭제-t 기본값은 10초입니다. Spring 의 완전 종료 시간보다 길게 줍니다. 컨테이너를 지워도 이미지와 볼륨은 남습니다.
podman run -d --memory=1g -e JAVA_OPTS='-XX:MaxRAMPercentage=75' myapp:1.0JDK 21 은 컨테이너 메모리 한도를 인식합니다. -Xmx 를 고정하는 대신 MaxRAMPercentage 로 한도의 비율을 주면 한도를 바꿔도 JVM 이 따라갑니다. 한도 없이 띄우면 호스트 전체 메모리를 기준으로 삼아 운영 묶음 04 의 OOM killer 문제가 그대로 생깁니다.
podman 4.4 이상은 .container 파일을 두면 systemd 유닛이 자동 생성됩니다. 운영 묶음 03 의 유닛 파일과 같은 형식에 [Container] 절이 추가된 것입니다.
# /etc/containers/systemd/myapp.container
[Unit]
Description=MyApp container
After=network-online.target
[Container]
Image=localhost/myapp:1.0
ContainerName=myapp
PublishPort=8080:8080
Volume=/opt/myapp/logs:/opt/myapp/logs:Z
Environment=JAVA_OPTS=-Xmx512m
[Service]
Restart=on-failure
TimeoutStopSec=40
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -fpodman run 옵션이 [Container] 키로 바뀌었을 뿐입니다. 이후 systemctl·journalctl 로 다른 서비스와 똑같이 다룹니다.
일반 사용자로 컨테이너를 띄울 수 있습니다. 앱이 뚫려도 그 사용자 권한뿐이라 안전합니다.
podman run -d -p 8080:8080 myapp:1.0 # myapp 계정으로 그대로
loginctl enable-linger myapp # 로그아웃해도 유지
systemctl --user enable --now myapp # 사용자 단위 유닛1024 미만 포트는 rootless 로 못 엽니다. 80 은 앞단 nginx 나 sysctl net.ipv4.ip_unprivileged_port_start=80 으로 해결합니다.
podman save -o myapp-1.0.tar myapp:1.0 # 반출: 파일로
sha256sum myapp-1.0.tar > myapp-1.0.tar.sha256
# 매체로 이동
sha256sum -c myapp-1.0.tar.sha256 # 반입: 확인
podman load -i myapp-1.0.tar # 등록
podman images | grep myapp베이스 이미지도 같은 방식으로 한 번 반입해 두면 서버에서 빌드할 수 있습니다. 사내 레지스트리가 있으면 podman tag 후 podman push registry.corp/myapp:1.0 으로 올려 여러 서버가 pull 합니다.
podman ps -a --filter status=exited # 정지된 컨테이너
podman container prune -f # 정지된 것 삭제
podman image prune -a -f # 안 쓰는 이미지 전부
podman system df # 이미지·컨테이너·볼륨 용량버전마다 이미지를 남기면 수 GB 씩 쌓입니다. 운영 묶음 06 의 디스크 점검에 podman system df 를 추가합니다.