어제 Docker에서 Hermes 돌리다가 uid 하나 때문에 하루를 갈아넣은 이야기의
결말은 docker-compose.yml에 HERMES_UID=1000/HERMES_GID=1000 환경변수 두 줄을 넣어서
해결됐다. 근데 그 전에 먼저 시도했던 user: "1000:1000"는 왜 실패했을까? 그리고 환경변수
쪽은 정확히 무슨 일을 하길래 성공했을까? 컨테이너 안까지 들어가서 실제 부트스트랩
스크립트를 읽어보고 근거를 갖고 정리해본다.
UID/GID가 뭔지부터
UID/GID 개념이 헷갈린다면 펼쳐보기 — 아는 사람은 건너뛰어도 됨
리눅스에서 "사용자"는 사실 사람이 읽는 이름(decipher, hermes
같은)이 아니라 숫자로 식별된다. 이 숫자가 UID(User ID)다.
그룹도 마찬가지로 GID(Group ID)라는 숫자로 식별된다.
decipher라는 이름은 그냥 사람이 보기 편하라고 /etc/passwd
파일에 적어둔 라벨일 뿐이고, 커널이 실제로 파일 소유권을 검사할 때 보는 건 그 뒤에 있는
숫자(UID)다. id decipher를 쳐보면 이렇게 나온다.
$ id decipher
uid=1000(decipher) gid=1000(decipher) groups=1000(decipher),...
즉 decipher = uid 1000이라는 뜻이고, 파일 소유권 비교는 전부 이 숫자로
이뤄진다. 이름이 같아도 숫자가 다르면 완전히 다른 사용자로 취급된다.
왜 컨테이너 uid가 호스트에 그대로 보이나
Docker는 기본적으로 컨테이너와 호스트가 같은 UID 숫자 공간을 공유한다(별도 설정 없이는
“UID 네임스페이스 격리”가 안 된다는 뜻). 그래서 컨테이너 안에서 uid 10000인 프로세스가
파일을 만들면, 호스트에서 봤을 때도 그 파일은 uid 10000 소유로 보인다. 다만 호스트 쪽엔
/etc/passwd에 10000번을 쓰는 사람 이름이 없을 뿐이라, ls -la를 치면 이름 대신 숫자
10000이 그대로 찍힌다.
이게 바로 처음에 ~/hermes-data를 host의 decipher(uid 1000) 계정으로 못 열었던 이유다 —
같은 숫자 공간을 공유하지만, 숫자 자체가 서로 안 맞았던 것.
1차 시도: docker run --user 1000:1000
가장 직관적인 시도는 컨테이너를 아예 uid 1000으로 실행하는 거였다.
services:
hermes:
user: "1000:1000"
재시작하니 컨테이너가 곧바로 죽었다. 로그를 열어보면 이렇다.
[stage2] ERROR: container started with --user 1000 (an arbitrary, non-hermes UID).
This is not supported under the s6-overlay image. The container bootstrap
(UID remap, data-volume ownership, config seeding) needs to start as root...
왜 안 됐나 — 실제 스크립트로 확인
컨테이너 안에 들어가서 이 에러를 실제로 뱉는 스크립트(/opt/hermes/docker/stage2-hook.sh)를
직접 읽어봤다. 맨 위 주석에 이유가 정확히 적혀 있다.
# --- Reject the unsupported `docker run --user <uid>:<gid>` start ---
# ...
# Under s6-overlay this no longer works: the bootstrap (UID remap, data-volume
# ownership, config seeding) requires root, and it is skipped when the container
# starts non-root.
핵심은 이거다. 이 이미지는 s6-overlay라는 프로세스 관리자로 여러 서비스(게이트웨이, 대시보드 등)를 감독하는데, 컨테이너가 뜨자마자 root 권한으로 몇 가지 부트스트랩 작업을 먼저 해야 한다:
- uid/gid를 원하는 값으로 바꾸기 (
usermod/groupmod는 root만 할 수 있다) - 데이터 볼륨 소유권을 그 uid로 맞추기 (
chown도 root만) - 설정 파일 시딩, 권한 정리 등
--user 1000:1000으로 컨테이너를 띄우면 PID 1부터 이미 uid 1000으로 시작되기 때문에,
이 root 전용 부트스트랩 단계 자체를 실행할 권한이 없다. 그래서 시작하자마자 죽는다.
실제로 스크립트는 시작하자마자 이 상황을 명시적으로 감지해서 막아버린다.
cur_uid="$(id -u)"
if [ "$cur_uid" != 0 ] && [ "$cur_uid" != "$(id -u hermes)" ]; then
cat >&2 <<EOF
[stage2] ERROR: container started with --user $cur_uid ...
EOF
exit 1
fi
“지금 내가 root(0)도 아니고, 이미지가 빌드될 때 정해둔 hermes 계정의 uid(10000)도 아니면”
→ 바로 에러 찍고 종료. 우리가 --user 1000을 줬을 때 정확히 이 조건에 걸린 것이다.
2차 시도: HERMES_UID/HERMES_GID — 왜 이건 됐나
environment:
- HERMES_UID=1000
- HERMES_GID=1000
이번엔 user: 필드를 아예 안 쓴다. 즉 컨테이너는 평소처럼 root로 시작한다(Docker
기본값). root로 시작했으니 위에서 막혔던 부트스트랩 단계를 정상적으로 실행할 수 있다. 같은
스크립트의 다음 부분이 바로 그 단계다.
if [ -n "${HERMES_UID:-}" ] && ... && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
echo "[stage2] Changing hermes UID to $HERMES_UID"
usermod -u "$HERMES_UID" hermes
fi
if [ -n "${HERMES_GID:-}" ] && ... ; then
echo "[stage2] Changing hermes GID to $HERMES_GID"
groupmod -o -g "$HERMES_GID" hermes 2>/dev/null || true
fi
정확히 우리가 로그에서 봤던 그 줄이다.
[stage2] Changing hermes UID to 1000
[stage2] Changing hermes GID to 1000
root 권한으로 usermod -u 1000 hermes를 실행해서, 이미지 빌드 시점에 10000번이었던
hermes 계정의 uid를 1000으로 바꿔치기한 것이다. 그다음 데이터 볼륨도 그 새 uid에
맞춰 소유권을 정리한다.
chown hermes:hermes "$HERMES_HOME" 2>/dev/null || ...
그러고 나서야 실제 서비스(게이트웨이, 대시보드)를 s6-setuidgid hermes <명령어>로 그 uid로
권한을 낮춰서 실행한다. root로 시작 → 필요한 root 작업(uid 변경, chown) 처리 → 그다음에
비로소 권한을 낮춰서 서비스 실행, 이 순서가 지켜져야만 s6-overlay가 정상 동작하는
구조였다.
일반화: PUID/PGID 컨벤션
스크립트 주석을 더 읽어보니 PUID/PGID라는 이름도 같이 지원한다.
# NAS users (UGOS, Synology, unRAID) expect the LinuxServer.io PUID/PGID
# convention and bind-mount /opt/data from a host directory owned by
# their own UID
HERMES_UID="${HERMES_UID:-${PUID:-}}"
HERMES_GID="${HERMES_GID:-${PGID:-}}"
이건 LinuxServer.io라는 곳에서 만든 이미지들이 널리 쓰는 컨벤션이다. “컨테이너를 host
uid에 맞춰 돌리고 싶으면 --user가 아니라 PUID/PGID 환경변수를 써라”는 패턴이 이
Hermes 이미지에만 있는 게 아니라, 꽤 많은 Docker 이미지가 같은 이유(root로 시작해야 하는
초기화 단계가 있는 이미지)로 이미 채택하고 있다는 뜻이다. 그래서 앞으로 비슷한 상황을
만나면, --user부터 시도하기 전에 이미지 문서에 PUID/PGID나 비슷한 환경변수가 있는지
먼저 찾아보는 게 맞는 순서다.
정리
- 컨테이너와 호스트는 기본적으로 같은 UID 숫자 공간을 공유한다 — 그래서 uid가 안 맞으면 호스트에서 파일이 안 보이는 문제가 생긴다.
docker run --user는 컨테이너를 처음부터 지정한 uid로 띄운다. 이미지가 root 권한으로 해야 하는 부트스트랩(uid 변경, chown, 설정 시딩)이 있다면 이 방식은 그 단계 자체를 실행 못 하게 막아버린다.- 반면
HERMES_UID/HERMES_GID(또는PUID/PGID) 같은 환경변수 방식은 컨테이너를 root로 정상 시작시킨 다음, 그 안에서usermod/chown으로 필요한 걸 다 처리하고 나서 비로소 권한을 낮춰 서비스를 실행한다. - 이건 이 이미지만의 특별한 설계가 아니라, LinuxServer.io PUID/PGID 컨벤션을 따르는 꽤 흔한 패턴이다. 비슷한 문제를 다른 이미지에서 만나면 이 이름부터 찾아보면 된다.