Skip to content
게으른 엔지니어의 기술 블로그
Go back

docker run --user는 안 되고 HERMES_UID는 됐던 이유

어제 Docker에서 Hermes 돌리다가 uid 하나 때문에 하루를 갈아넣은 이야기의 결말은 docker-compose.ymlHERMES_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 권한으로 몇 가지 부트스트랩 작업을 먼저 해야 한다:

--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나 비슷한 환경변수가 있는지 먼저 찾아보는 게 맞는 순서다.

정리


Share this post:

Previous Post
리눅스 ACL이 뭐길래 — POSIX ACL과 mask 이해하기
Next Post
Hermes cron이 꺼지면 스킵되는 이유 — in-process 스케줄러의 함정