Rootless Docker와 /data/docker — root 없이, 그리고 / 를 지키며 (Ubuntu 24.04)

2026.09.09

·

새 서버에 Docker를 올릴 때, 우리는 늘 두 가지를 함께 결정합니다. rootless로 설치할 것, 그리고 데이터를 /data/docker에 둘 것. 편하게 apt install docker.io 한 줄로 끝낼 수도 있지만, 운영 서버에서는 그렇게 하지 않습니다. 이 두 결정에는 분명한 이유가 있고, 그 이유를 이해하면 어느 배포판·어느 환경에서도 같은 원칙을 적용할 수 있습니다.

이 글은 결국 세 가지 질문에 답합니다.

  • 왜 root가 아니라 rootless로 설치하는가
  • 왜 기본 경로가 아니라 /data/docker에 데이터를 쌓는가
  • /data라는 개념은 대체 어디서 왔는가

대상 · Ubuntu 24.04 LTS · rootless Docker (Docker CE) · 소요 10분

Sponsored


1. 왜 rootless Docker인가

기본 방식으로 Docker를 설치하면 데몬(dockerd)이 root 권한으로 돕니다. 편하긴 하지만 보안 관점에서는 큰 부담입니다. Docker 데몬에 접근할 수 있는 사용자는 사실상 그 서버의 root와 같습니다. 컨테이너 하나가 탈출(container escape)하면, 그 즉시 호스트의 root를 쥐게 됩니다. “컨테이너로 격리했으니 안전하다”는 믿음이 root 데몬 위에서는 절반만 참인 셈입니다.

Rootless Docker는 이 데몬을 일반 사용자 권한으로 돌립니다. 사용자 네임스페이스(user namespace)를 이용해, 컨테이너 안의 root(uid 0)를 호스트에서는 권한 없는 일반 uid로 매핑합니다. 그래서 컨테이너가 탈출하더라도 손에 쥐는 것은 그 일반 사용자의 권한뿐, 호스트 root가 아닙니다.

  • 공격 표면 축소 — root 데몬이 없으니, 데몬을 노린 권한 상승 경로가 사라집니다.
  • 격리 강화 — 컨테이너 탈출의 피해가 일반 사용자 범위에 갇힙니다.
  • 사용자 단위 운영 — Docker가 그 사용자의 홈·systemd(user) 아래에서 돌아, 여러 사용자가 서로 간섭 없이 각자의 Docker를 가질 수 있습니다.

솔직히 — rootless가 만능은 아닙니다. 1024 미만의 특권 포트를 컨테이너가 직접 열 수 없고(리버스 프록시로 우회), 네트워크는 slirp4netns를 거쳐 순수 성능이 약간 손해입니다. 하지만 우리가 서버에 올리는 대부분의 워크로드(웹·API·DB·캐시)에는 이 제약이 문제가 되지 않고, 얻는 보안 이득이 훨씬 큽니다. 도구는 상황에 맞게 고르는 것이지, 하나가 항상 옳은 것은 아닙니다.


2. 왜 기본 경로가 아니라 /data/docker인가

Docker의 데이터 — 이미지, 컨테이너 레이어, 볼륨, 빌드 캐시, 컨테이너 로그 — 는 예측 불가능하게 커집니다. 이미지를 몇 개 받고, 빌드를 몇 번 돌리고, 볼륨에 데이터가 쌓이고, 로그가 흐르다 보면 수십 기가바이트가 순식간에 찹니다.

문제는 Docker의 기본 데이터 경로가 /var/lib/docker, 즉 루트(/) 파티션 안이라는 점입니다. 아무 설정 없이 쓰면 이 폭증하는 데이터가 전부 /에 쌓입니다. 그리고 루트 파티션이 100%에 도달하는 순간, 서버는 로그도 못 남기고 명령어도 못 실행하는 최악의 상태로 얼어붙습니다.

그래서 우리는 Docker의 data-root를 /data/docker로 지정합니다. 이렇게 하면 이미지·볼륨·로그가 아무리 커져도 /data 볼륨 안에서 소진될 뿐, /와 OS는 멀쩡합니다. 한 곳의 폭증이 시스템 전체로 번지지 못하게 미리 격리하는 것입니다.

[ Docker 데이터를 어디에 두는가 ]

  기본값 (위험)
    이미지·볼륨·로그 폭증 ─▶ /var/lib/docker ─▶ / 파티션 100%
                                                  ↓
                                        OS 마비 (로그·명령 불가)

  우리 방식 (격리)
    이미지·볼륨·로그 폭증 ─▶ /data/docker ─▶ /data 볼륨만 소진
                                                  ↓
                                        / 와 OS 는 정상

그래서 데이터 경로를 정할 때 우리가 늘 확인하는 것은 하나입니다 — 그 경로가 /와 같은 파티션에 있지는 않은가. 아무리 /data/docker라고 이름을 붙여도, 그 /data가 실은 별도 볼륨이 아니라 /와 같은 디스크라면 이 격리는 무의미하기 때문입니다.


3. /data는 어디서 온 개념인가

여기서 짚고 넘어갈 것이 있습니다. /data/docker의 /data는 Docker만을 위해 급조한 경로가 아닙니다. 우리가 모든 데이터베이스·미들웨어 서버에 일관되게 적용하는 파티션 분리 표준의 일부입니다.

그 표준은 서버의 저장 공간을 역할에 따라 네 개의 독립된 볼륨으로 나눕니다.

  /       →  OS 전용 (고정 크기, 신성불가침)
  /app    →  엔진·바이너리·설정   (거의 안 늘어남)
  /log    →  로그 + 로그로테이트   (예측 가능한 증가)
  /data   →  실제 데이터 — DB·볼륨·대용량   (I/O 집중, 크게 증가)

핵심은 커지는 속도와 방식이 다른 데이터를 서로 다른 방에 격리한다는 것입니다. 그래서 예측 불가능하게 커지는 것들 — 데이터베이스 파일, 볼륨, 그리고 Docker의 이미지·컨테이너·로그 — 은 전부 /data에 둡니다. data-root를 /data/docker로 두는 것은, 그 표준을 Docker에 그대로 적용한 것일 뿐입니다.

📌 철학은 여기서 이어집니다
왜 저장 공간을 이렇게 네 개로 나누는가, 그리고 이 /data라는 개념이 실제로 어떤 새벽을 무사히 넘겼는가는 — 디스크가 가득 차던 새벽, 우리는 아무 일도 없었습니다 (‘역할에 따라 나눈 네 개의 공간’ 파트)에서 이어집니다. 그리고 왜 /를 신성불가침으로 지키고 애플리케이션 데이터를 절대 /에 쌓지 않는지는 같은 글의 ‘루트(/)를 지키는 것이 최우선입니다’ 파트에서 여러 겹의 방어선으로 다룹니다. /data/docker는 그 철학의 실전 적용입니다.


4. 설치 — 준비 단계

이제 실제 설치입니다. rootless Docker는 일반 Docker CE 패키지에 더해 rootless 전용 패키지 몇 개가 필요합니다.

# Docker CE 저장소는 미리 추가돼 있다고 가정
sudo apt-get install -y 
  docker-ce docker-ce-cli containerd.io 
  docker-buildx-plugin docker-compose-plugin 
  docker-ce-rootless-extras uidmap dbus-user-session 
  slirp4netns fuse-overlayfs
  • docker-ce-rootless-extras — rootless 설치 도구(dockerd-rootless-setuptool.sh)
  • uidmap — 사용자 네임스페이스 매핑(newuidmap/newgidmap)
  • slirp4netns — rootless 네트워킹
  • fuse-overlayfs — root 없이 쓰는 오버레이 스토리지 드라이버

먼저 root 데몬을 꺼둡니다. rootless와 rootful이 동시에 돌면 충돌하니, 시스템 레벨 Docker 서비스는 비활성화합니다.

sudo systemctl disable --now docker.service docker.socket

다음으로 사용자 네임스페이스에 쓸 subuid/subgid 범위를 등록합니다. 컨테이너 내부의 uid들이 호스트에서 매핑될 “빌린 uid 대역”입니다.

# 사용자에게 100000부터 65536개의 subuid/subgid 할당
echo "${USER}:100000:65536" | sudo tee -a /etc/subuid
echo "${USER}:100000:65536" | sudo tee -a /etc/subgid

마지막으로 linger를 켭니다. 기본적으로 사용자 systemd 서비스는 그 사용자가 로그인해 있을 때만 돕니다. 서버는 아무도 로그인하지 않은 채로 돌아야 하므로, linger를 켜서 로그인 세션과 무관하게 Docker가 상시 실행되게 합니다.

sudo loginctl enable-linger "${USER}"

5. 데이터 경로와 daemon.json

2·3단계에서 이야기한 /data/docker를 실제로 준비합니다. 소유자는 Docker를 돌릴 사용자로, 권한은 710(소유자만 접근)으로 잠급니다.

sudo mkdir -p /data/docker
sudo chown "${USER}:${USER}" /data/docker
sudo chmod 710 /data/docker

그다음 daemon.json으로 데이터 경로와 로그 정책을 지정합니다. 여기서 rootless의 함정이 하나 있습니다. rootful Docker는 /etc/docker/daemon.json을 읽지만, rootless는 ~/.config/docker/daemon.json을 읽습니다. 위치를 헷갈리면 설정이 먹지 않습니다.

# 파일 위치: ~/.config/docker/daemon.json  (rootless 전용 경로)
{
  "data-root": "/data/docker",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

6. rootless 데몬 기동

이제 Docker를 돌릴 사용자로 rootless 설치 도구를 실행하고, 사용자 systemd 서비스로 데몬을 올립니다. (root가 아니라 그 사용자 본인으로 실행하는 것이 핵심입니다.)

# Docker를 돌릴 사용자 셸에서 실행
export XDG_RUNTIME_DIR="/run/user/$(id -u)"

dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker.service

마지막으로 docker 명령이 rootless 소켓을 바라보도록 DOCKER_HOST를 셸 설정에 추가합니다. rootless 소켓은 사용자별 런타임 디렉토리 아래에 생깁니다.

# ~/.bashrc 에 추가
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

새 셸에서 docker info를 실행해 Docker Root Dir: /data/docker와 rootless 표시가 보이면 성공입니다.

docker info | grep -E "Root Dir|rootless"
# Docker Root Dir: /data/docker
# ... rootless

7. 로그 폭주까지 막는다

5단계 daemon.json의 log-opts를 그냥 지나치지 마세요. 이것도 /data 격리와 같은 목적의 안전장치입니다. Docker 컨테이너의 표준 출력 로그는 기본 설정에서 무한정 쌓입니다. 말 많은 컨테이너 하나가 로그로 디스크를 가득 채우는 일은 실제로 흔합니다.

  • max-size: 10m — 로그 파일 하나가 10MB에 도달하면 회전(rotate)
  • max-file: 3 — 최대 3개까지만 보관
  • → 컨테이너당 로그 상한 약 30MB로 고정

이렇게 상한을 못 박아두면, 어떤 컨테이너가 로그를 쏟아내도 그 양이 예측 가능한 선에서 멈춥니다. 커질 수 있는 것에는 미리 천장을 둔다 — /data로 데이터를 격리하는 것과 같은 발상입니다.


함정 모음 — 이럴 때 이렇게

  • 로그아웃하니 Docker가 죽는다 — enable-linger를 안 한 것입니다. sudo loginctl enable-linger <사용자> 후 재기동하세요.
  • docker 명령이 root 소켓을 찾는다 — DOCKER_HOST가 설정 안 된 것입니다. echo $DOCKER_HOST로 /run/user/UID/docker.sock을 가리키는지 확인하세요.
  • daemon.json을 고쳤는데 반영이 안 된다 — rootless는 /etc/docker가 아니라 ~/.config/docker/daemon.json입니다. 위치를 확인하고 systemctl --user restart docker.
  • 서비스가 안 뜬다 — journalctl --user -u docker.service -n 50으로 로그를 봅니다. 대개 uidmap/subuid 누락이나 XDG_RUNTIME_DIR 미설정입니다.
  • 소켓이 사라졌다 — 소켓은 /run/user/UID(tmpfs)에 있어 재부팅 시 재생성됩니다. linger가 켜져 있으면 부팅과 함께 자동으로 다시 올라옵니다.

마치며

Docker를 설치하는 방법은 많지만, 우리가 늘 같은 두 결정을 내리는 데에는 분명한 이유가 있습니다. rootless는 컨테이너의 사고가 호스트 root로 번지지 않게 하고, /data/docker는 데이터의 폭증이 /로 번지지 않게 합니다. 방향은 달라 보여도 목적은 하나입니다 — 한 곳의 문제가 전체의 문제가 되지 않도록 미리 격리하는 것.

이 두 원칙과 로그 상한, 권한 잠금(710)까지 — 우리는 이것을 표준으로 삼아, 어느 서버에 Docker를 올리든 결과가 같게 만듭니다. 검증된 방식을 매번 재현하는 것, 그것이 우리가 인프라에서 신뢰를 만드는 방법입니다. 파티션을 나누는 것도, Docker 데이터를 /data/docker에 모으는 것도, 결국 같은 원칙 위에 있습니다.


MORE POSTS

다른 글 보기

guniq 인사이트 썸네일 — 중소기업 OpenLDAP 통합 인증 구축 – 00. 왜 OpenLDAP인가: 비용과 자유, 그리고 표준의 중요성
테크 랩

중소기업 OpenLDAP 통합 인증 구축 – 00. 왜 OpenLDAP인가: 비용과 자유, 그리고 표준의 중요성

MS Active Directory 대신 OpenLDAP을 선택한 이유, 오픈소스 디렉토리 서버의 장단점, 그리고 연동 유연성을 결정짓는 LDAP 표준 준수의 중요성을 실제 운영 경험 기반으로 정리합니다.
2026.10.12
guniq 인사이트 썸네일 — Zabbix 인프라 모니터링 구축 – 15. 실전 이슈: 운영 중 마주친 오탐·누락·설정 오류 총정리 (트러블슈팅)
테크 랩

Zabbix 인프라 모니터링 구축 – 15. 실전 이슈: 운영 중 마주친 오탐·누락·설정 오류 총정리 (트러블슈팅)

Zabbix 모니터링 구축과 운영 중 실제로 마주친 문제들을 총정리합니다. Media Type 기본 비활성화, 트리거 표현식 포맷 변경, stale value 오탐, crontab 초기화, netstat 타입 오류 등 삽질의 기록과 해결책을 공유합니다.
2026.10.09
guniq 인사이트 썸네일 — Zabbix 인프라 모니터링 구축 – 14. 환경 분리: DEV·PRD를 하나의 Zabbix로 운영하기 (호스트그룹·권한)
테크 랩

Zabbix 인프라 모니터링 구축 – 14. 환경 분리: DEV·PRD를 하나의 Zabbix로 운영하기 (호스트그룹·권한)

개발과 운영 환경을 하나의 Zabbix 서버로 운영하면서 호스트 그룹, 사용자 권한, Slack 알림 채널을 분리하는 방법을 다룹니다. API 토큰, 스크립트, 로그까지 환경별 분리 전략 전체를 설명합니다.
2026.10.08

프로젝트 문의 환영합니다

기획부터 개발, 운영까지 함께 만들어 드립니다.

무료 3분 자가진단

우리 회사, 자체 클라우드가 답일까?

AWS vs 자체 인프라 · 11개 항목 3분 체크