[Docker] Rootless Docker 실전 활용 완벽 가이드 — Compose, 네트워크 우회, 트러블슈팅
이전 편에서 Rootless Docker의 systemd 서비스와 daemon.json 설정을 완료했습니다. 이번 마지막 편에서는 실제 서비스를 운용하는 데 필요한 실전 지식을 집중적으로 다룹니다. 볼륨 권한의 함정, Docker Compose 활용, 포트 제한 우회 방법, 그리고 현장에서 자주 마주치는 오류들의 해결책을 정리합니다.
1. 컨테이너 실행과 볼륨 마운트 — 권한의 함정
1.1 uid 매핑이 볼륨에 미치는 영향
Rootless 모드에서 컨테이너 내부의 uid 0(root)은 호스트에서는 subuid 시작값(예: uid 100000)으로 매핑됩니다. 이 특성 때문에 호스트 디렉토리를 마운트할 때 소유권 문제가 발생할 수 있습니다.
# 현재 사용자 uid 확인
id
# uid=1000(huni) gid=1000(huni)
# 현재 사용자 소유 디렉토리 마운트 → 정상 동작
mkdir -p ~/data/nginx/html
echo "<h1>Hello Rootless Docker</h1>" > ~/data/nginx/html/index.html
docker run -d -p 8080:80 \
-v ~/data/nginx/html:/usr/share/nginx/html:ro \
nginx:alpine
# 정상 동작: 현재 사용자 소유 파일 → 컨테이너 내부 root가 읽기 가능
문제 상황: root 소유 디렉토리 마운트
# 잘못된 예시: root 소유 디렉토리
sudo mkdir -p /opt/appdata
sudo chmod 755 /opt/appdata
docker run -d -v /opt/appdata:/app myapp
# 컨테이너 내부에서 /app에 쓰기 불가
# → 호스트에서 /opt/appdata의 소유자가 root(uid 0)이기 때문에
# Rootless 컨테이너의 uid 100000으로는 접근 불가
# 올바른 해결책: 현재 사용자 소유로 변경
sudo chown -R $USER:$USER /opt/appdata
docker run -d -v /opt/appdata:/app myapp # 정상 동작
1.2 컨테이너 내부 uid와 호스트 소유권 맞추기
일부 이미지는 특정 uid로 프로세스를 실행합니다(예: nginx는 uid 101). 이 경우 볼륨 권한을 정확히 설정해야 합니다.
# nginx 컨테이너 내부의 nginx 프로세스 uid 확인
docker run --rm nginx:alpine id nginx
# uid=101(nginx) gid=101(nginx)
# 호스트에서 uid 101은 subuid 100000 + 101 = 100101로 매핑됨
# 로그 디렉토리를 nginx가 쓰게 하려면:
mkdir -p ~/data/nginx/logs
# uid 100101이 쓸 수 있도록 권한 부여
sudo chown 100101:100101 ~/data/nginx/logs
# 또는 모든 사용자에게 쓰기 권한 (보안 주의)
chmod 777 ~/data/nginx/logs
docker run -d -v ~/data/nginx/logs:/var/log/nginx nginx:alpine
Pro-tip: 볼륨 마운트 대신 Named Volume을 사용하면 uid 매핑 문제를 Docker가 자동으로 처리합니다. 가능하면 Named Volume을 우선 사용하세요.
# Named Volume은 uid 매핑 문제 없음
docker run -d -v nginx-logs:/var/log/nginx nginx:alpine
# Named Volume 위치: ~/.local/share/docker/volumes/nginx-logs/
2. Docker Compose 실전 운용
2.1 Compose V2 확인
Rootless 모드에서도 Docker Compose V2 (docker compose 명령)를 완전히 지원합니다.
# Compose 버전 확인
docker compose version
# Docker Compose version v2.x.x
# DOCKER_HOST가 올바르게 설정되어 있어야 함
echo $DOCKER_HOST
# unix:///run/user/1000/docker.sock
2.2 실전 Compose 예시 — Nginx + Node.js 앱
# ~/apps/webapp/compose.yml
services:
web:
image: nginx:alpine
ports:
- "8080:80" # Rootless에서 1024 미만은 기본 불가 → 8080 사용
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/html:/usr/share/nginx/html:ro
networks:
- app-network
depends_on:
app:
condition: service_healthy
restart: unless-stopped
app:
image: node:20-alpine
working_dir: /app
volumes:
- ./app:/app:ro
command: node server.js
environment:
- NODE_ENV=production
- PORT=3000
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- db-data:/var/lib/postgresql/data # Named Volume 사용 권장
networks:
- app-network
restart: unless-stopped
volumes:
db-data:
# Rootless에서 Named Volume 위치: ~/.local/share/docker/volumes/
networks:
app-network:
driver: bridge
# 서비스 시작
docker compose -f ~/apps/webapp/compose.yml up -d
# 서비스 상태 확인
docker compose -f ~/apps/webapp/compose.yml ps
# 로그 확인
docker compose -f ~/apps/webapp/compose.yml logs -f web
# 서비스 중지 및 볼륨 유지
docker compose -f ~/apps/webapp/compose.yml down
# 서비스 중지 및 볼륨 삭제
docker compose -f ~/apps/webapp/compose.yml down -v
2.3 Compose 환경별 설정 분리
# compose.override.yml (개발 환경)
services:
app:
environment:
- NODE_ENV=development
volumes:
- ./app:/app # 개발 시 코드 변경 즉시 반영
ports:
- "9229:9229" # Node.js 디버그 포트
# 기본 + override 합성 (자동 적용)
docker compose up -d
# 특정 파일 명시
docker compose -f compose.yml -f compose.prod.yml up -d
3. 네트워크 제약사항과 우회 방법
3.1 포트 1024 미만 바인딩 — 3가지 방법
Rootless 모드에서 80, 443 등 1024 미만 포트를 직접 바인딩하려면 추가 설정이 필요합니다.
방법 1: sysctl로 비특권 포트 범위 하향 (서버 운영 환경 권장)
# 즉시 적용 (재부팅 후 초기화)
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
# 영구 적용
echo "net.ipv4.ip_unprivileged_port_start=80" | \
sudo tee /etc/sysctl.d/99-rootless-docker.conf
sudo sysctl -p /etc/sysctl.d/99-rootless-docker.conf
# 적용 후 포트 80 바인딩 테스트
docker run --rm -p 80:80 nginx:alpine
# 정상 동작
방법 2: rootlesskit에 CAP_NET_BIND_SERVICE 부여
# rootlesskit 바이너리에 capability 추가
sudo setcap cap_net_bind_service=ep $(which rootlesskit)
# 설정 확인
getcap $(which rootlesskit)
# /usr/bin/rootlesskit cap_net_bind_service=ep
# 데몬 재시작
systemctl --user restart docker
방법 3: iptables DNAT 규칙 추가 (방화벽이 있는 환경)
# 외부에서 80번 포트 → 내부 8080으로 리디렉션
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
# 로컬에서도 적용
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080
# 영구 저장 (Ubuntu)
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save
세 가지 방법의 특징 비교:
| 방법 | 적용 범위 | 재부팅 영구성 | 권한 변경 수준 |
|---|---|---|---|
| sysctl | 시스템 전체 | 영구 설정 가능 | 낮음 (커널 파라미터) |
| setcap | rootlesskit 바이너리 | 영구 | 중간 (capability) |
| iptables | 특정 포트만 | 영구 설정 가능 | 없음 (트래픽 리디렉션) |
3.2 slirp4netns vs pasta 네트워크 성능 비교
현재 사용 중인 네트워크 백엔드를 확인하고, 필요 시 더 빠른 pasta로 전환합니다.
# 현재 네트워크 백엔드 확인
docker info | grep -i "network"
# 또는
cat ~/.config/systemd/user/docker.service.d/*.conf 2>/dev/null | grep NET
# pasta로 전환 (Ubuntu 23.04+에서 passt 패키지 설치 후)
sudo apt-get install -y passt
mkdir -p ~/.config/systemd/user/docker.service.d/
cat > ~/.config/systemd/user/docker.service.d/network.conf << 'EOF'
[Service]
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520
EOF
systemctl --user daemon-reload
systemctl --user restart docker
3.3 host 네트워크 모드의 제약
Rootless 모드에서 --network=host는 실제로 호스트 네트워크 네임스페이스에 접근하지 못합니다. 대신 rootlesskit의 네트워크 네임스페이스 내에서 동작합니다.
# host 네트워크는 실제 호스트가 아닌 rootlesskit 네임스페이스
docker run --rm --network=host alpine ip addr
# 출력은 실제 호스트 인터페이스와 다름
# host 네트워크가 꼭 필요한 경우 → Root 모드 Docker 사용 권장
4. 이미지 빌드 (BuildKit)
4.1 BuildKit 활성화 및 빌드
Rootless 모드에서도 BuildKit을 완전히 지원합니다.
# BuildKit 활성화 확인
echo $DOCKER_BUILDKIT
# 1 (또는 daemon.json에서 features.buildkit: true 설정)
# 일반 빌드
docker build -t myapp:latest .
# BuildKit 고급 기능 활용
docker buildx create --use --name rootless-builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myapp:latest \
--push .
4.2 멀티스테이지 빌드 예시
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# 비특권 사용자로 실행 (보안 모범 사례)
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
# 빌드 및 실행
docker build -t myapp:v1.0 .
docker run -d -p 3000:3000 myapp:v1.0
# 이미지 레이어 분석
docker history myapp:v1.0
docker image inspect myapp:v1.0 | grep -E "Size|RootFS"
5. 트러블슈팅 완전 가이드
오류 1: “Cannot connect to the Docker daemon”
Error response from daemon: Cannot connect to the Docker daemon at
unix:///var/run/docker.sock. Is the docker daemon running?
원인 및 해결:
# 1. DOCKER_HOST 환경변수 미설정
echo $DOCKER_HOST
# (비어있는 경우)
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# 2. 데몬이 실행되지 않은 경우
systemctl --user status docker
systemctl --user start docker
# 3. 소켓 파일 존재 확인
ls -la /run/user/$(id -u)/docker.sock
# 파일 없으면 데몬 재시작
systemctl --user restart docker
# 4. XDG_RUNTIME_DIR 미설정 (SSH 접속 환경)
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/docker.sock
오류 2: “Got permission denied while trying to connect to Docker”
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock
원인: sudo docker 또는 root 사용자로 Rootless 소켓 접근 시도
# 잘못된 사용 (sudo는 root의 docker.sock을 찾음)
sudo docker ps # ← 금지
# 올바른 사용 (현재 사용자 권한으로)
docker ps # ← DOCKER_HOST가 올바르게 설정된 상태에서
# 다른 사용자의 컨테이너에 접근하고 싶은 경우
# → 해당 사용자로 su 후 docker 명령 실행
su - alice
docker ps
오류 3: overlay2 마운트 실패
Error response from daemon: error creating overlay mount: operation not permitted
원인 및 해결:
# Ubuntu 20.04에서 overlay2 네이티브 미지원
# fuse-overlayfs로 전환
sudo apt-get install -y fuse-overlayfs
cat > ~/.config/docker/daemon.json << 'EOF'
{
"storage-driver": "fuse-overlayfs"
}
EOF
systemctl --user restart docker
# 커널 overlay 지원 여부 확인
cat /proc/filesystems | grep overlay
# overlay가 없으면 fuse-overlayfs 필수
# 또는 Ubuntu 22.04로 업그레이드 권장
오류 4: cgroup 리소스 제한 미동작
WARNING: Your kernel does not support memory limit capabilities or the cgroup is not mounted.
원인 및 해결:
# cgroup v2 활성화 여부 확인
stat -fc %T /sys/fs/cgroup/
# tmpfs → cgroup v1 (upgrade 필요)
# cgroup2fs → cgroup v2 (정상)
# cgroup v2 활성화 (재부팅 필요)
sudo sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"/' \
/etc/default/grub
sudo update-grub
sudo reboot
# 재부팅 후 cgroup delegation 설정
sudo mkdir -p /etc/systemd/system/user@.service.d/
sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF'
[Service]
Delegate=cpu cpuset io memory pids
EOF
sudo systemctl daemon-reload
오류 5: 네트워크 성능 저하 / 타임아웃
증상: 컨테이너 내부에서 외부 통신이 느리거나 간헐적 타임아웃 발생
# 현재 네트워크 백엔드 확인
ps aux | grep rootlesskit | grep -oP '\-\-net=\S+'
# --net=slirp4netns ← 느린 userspace 네트워킹
# pasta로 교체 (Ubuntu 23.04+)
sudo apt-get install -y passt
mkdir -p ~/.config/systemd/user/docker.service.d/
cat > ~/.config/systemd/user/docker.service.d/pasta-net.conf << 'EOF'
[Service]
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta
Environment=DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520
EOF
systemctl --user daemon-reload
systemctl --user restart docker
# 네트워크 성능 테스트
docker run --rm networkstatic/iperf3 -s &
docker run --rm networkstatic/iperf3 -c $(docker inspect -f '' $(docker ps -q -l))
오류 6: “no space left on device” (디스크 공간 부족)
# 현재 Docker 디스크 사용량 확인
docker system df -v
# 불필요한 리소스 정리
docker system prune -a --volumes
# 주의: 사용 중이지 않은 모든 이미지/컨테이너/볼륨 삭제
# 특정 항목만 정리
docker image prune -a # 미사용 이미지
docker container prune # 중지된 컨테이너
docker volume prune # 미사용 볼륨
docker builder prune -a # BuildKit 캐시
# 데이터 경로를 용량이 큰 디스크로 이동 (Part 3 참조)
6. Rootless Docker 완전 제거 방법
Rootless 모드를 제거하고 Root 모드로 완전히 전환하거나 Docker 자체를 제거할 때의 절차입니다.
# 1. 실행 중인 모든 컨테이너 중지
docker stop $(docker ps -q) 2>/dev/null
# 2. Rootless 설치 제거
dockerd-rootless-setuptool.sh uninstall
# [INFO] Removing systemd service docker.service
# [INFO] Removed docker.service successfully.
# 3. 데이터 디렉토리 삭제 (이미지/컨테이너/볼륨 영구 삭제)
# ⚠️ 주의: 이 작업은 되돌릴 수 없습니다
rm -rf ~/.local/share/docker
# 4. 설정 파일 정리
rm -rf ~/.config/docker
rm -rf ~/.config/systemd/user/docker.service
rm -rf ~/.config/systemd/user/docker.socket
rm -rf ~/.config/systemd/user/default.target.wants/docker.service
# 5. .bashrc에서 환경 변수 제거 (수동 편집)
# DOCKER_HOST, XDG_RUNTIME_DIR 관련 라인 삭제
# 6. systemd 재로드
systemctl --user daemon-reload
# 7. docker-ce-rootless-extras 패키지 제거 (선택)
sudo apt-get remove -y docker-ce-rootless-extras
7. 시리즈 마무리 — 언제 Rootless를 써야 하는가?
4부에 걸쳐 Rootless Docker의 개념, 설치, 설정, 실전 활용까지 살펴봤습니다. 마지막으로 어떤 환경에서 Rootless 모드를 선택해야 하는지 기준을 정리합니다.
Rootless 모드 도입 판단 기준
| 환경 | 권장 모드 | 이유 |
|---|---|---|
| 개인/팀 개발 서버 | Rootless 강력 권장 | 보안 이점, 단일 사용자 |
| 멀티 사용자 공유 서버 | Rootless 권장 | 사용자 간 완전 격리 |
| 금융/공공 규제 환경 | Rootless 필수 검토 | 최소 권한 원칙 준수 |
| CI/CD 빌드 에이전트 | Rootless 권장 | 빌드 작업의 권한 최소화 |
| 포트 80/443 직접 바인딩 필수 + sysctl 변경 불가 | Root 모드 | 네트워크 정책 제약 |
| Docker-in-Docker 필수 | Root 모드 검토 | DinD 제한적 지원 |
| Kubernetes 런타임 | containerd/CRI-O 권장 | Docker보다 경량 런타임 적합 |
| 레거시 Ubuntu 18.04 | Root 모드 | 커널 지원 부족 |
Rootless 모드 도입의 핵심 가치
보안 강화: 컨테이너 탈출, 소켓 탈취, CVE 악용 등의 공격이 발생해도 피해 범위가 일반 사용자 권한으로 제한됩니다.
감사(Audit) 편의성: 모든 컨테이너 작업이 명확한 사용자 uid로 추적되어, 멀티 사용자 환경에서 보안 감사가 용이합니다.
Zero Trust 운용: 최소 권한 원칙(Principle of Least Privilege)을 컨테이너 인프라 레벨에서 구현할 수 있습니다.
결론
Docker Rootless 모드는 “컨테이너 보안의 기본값을 올바르게 설정하는 것”입니다.
이번 4부작 시리즈를 통해 다음을 달성할 수 있습니다.
- Part 1: root 모드의 위험성과 Rootless 원리 이해
- Part 2: Ubuntu 환경에서 Rootless Docker 설치 완료
- Part 3: systemd, daemon.json, 스토리지 설정으로 안정적 운용 환경 구성
- Part 4: Compose 운용, 네트워크 우회, 트러블슈팅으로 실무 적용
현재 root 모드로 운용 중인 Docker 환경이 있다면, 오늘부터 Rootless 모드로의 전환을 검토해 보시기 바랍니다. 설치와 설정에 드는 시간은 불과 몇십 분이지만, 얻게 되는 보안 이점은 그 이상의 가치가 있습니다.
참고자료
- Docker 공식 문서 - Rootless mode
- rootlesskit 공식 문서
- pasta 네트워크 백엔드
- NIST SP 800-190 - Application Container Security Guide
Docker Rootless Mode 시리즈
- [Part 1] Rootless 모드란? 보안 개념과 필요성
- [Part 2] Ubuntu에서 Rootless Docker 설치하기
- [Part 3] Rootless Docker 설정 및 운용 가이드
- [Part 4] Rootless Docker 실전 활용 및 트러블슈팅 ← 현재 글
댓글남기기