컨테이너에 일반 사용자를 지정했더니 녹화가 시작되지 않았습니다. 실행 프로세스의 UID는 내려갔지만 새 녹화 볼륨의 디렉터리는 root 소유로 남아 있었습니다. “root로 실행하지 않는다”는 한 줄과, 실제 파일에 쓸 수 있다는 조건이 함께 맞아야 했습니다.
PokeClip에서는 미디어 프로세스가 쓰고 사이드카가 읽어 색인·업로드합니다. 두 프로세스를 각각 UID 10002와 10001로 실행하면서도 이 공유 경로는 유지해야 했습니다. 더구나 최종 이미지는 scratch라 시작 스크립트로 chown할 셸도 없습니다.
저는 기동 때 권한을 고치는 대신 이미지에 녹화 디렉터리를 만들고 소유권까지 확정했습니다. 이 방식은 새 볼륨의 초기화에 맞고, 기존 볼륨은 따로 이행해야 합니다. 구현·계약 검사는 AI와 협업했으며 변경은 PR #62에 반영했습니다.
빈 볼륨의 첫 복사가 빠져 있었습니다
Docker는 빈 named volume을 이미지의 디렉터리에 마운트할 때 기존 내용을 기본적으로 복사합니다. 이미 데이터가 있는 볼륨은 이미지 내용으로 자동 갱신되지 않습니다. 이 수명 차이는 Docker 볼륨 문서에도 설명돼 있습니다.
문제가 된 구성에서는 최종 이미지에 해당 디렉터리가 없어 기대한 초기 복사가 이루어지지 않았습니다. 실행 사용자 설정만 고쳐도 새 볼륨의 소유권이 맞춰지지 않았던 이유입니다. macOS bind mount에서는 소유권 재매핑 때문에 같은 구성이 동작해 보일 수 있어, 로컬 성공을 Linux의 권한 경계까지 확인한 근거로 삼지 않았습니다.
시작 스크립트 대신 이미지에 조건을 넣었습니다
prep 스테이지에서 빈 디렉터리를 만든 뒤 최종 이미지에 COPY --chown으로 옮겼습니다. Dockerfile의 소유권 옵션과 모드 옵션을 사용하면 최종 scratch 이미지에 셸이나 권한 수정 도구를 넣지 않아도 됩니다. 복사된 경로의 초기 소유권이 새 볼륨 생성 경로에도 적용되도록 구성했습니다.
| 대상 | 정한 값·경계 | 목적 |
|---|---|---|
| 미디어 프로세스 | UID/GID 10002 | 녹화 디렉터리에 쓰기 |
| 사이드카 | UID/GID 10001 | 녹화 파일 읽기 |
| 디렉터리 / 파일 | 0755 / 0644 | 다른 UID의 읽기·탐색 허용 |
| 실행 훅 바이너리 | root 소유, 일반 사용자 쓰기 금지 | 실행 계정의 바이너리 교체 방지 |
| 프로세스 옵션 | no-new-privileges | 새 권한 획득 제한 |
소유권을 고친 뒤에는 실행 훅의 보호 조건도 다시 봤습니다. root 소유라서 안전하다고 생각하기 쉽지만, --chmod=0777이면 일반 사용자가 내용을 덮어쓸 수 있었습니다. 따라서 소유자가 root인지와 실행 계정에 쓰기 권한이 없는지를 함께 검사하도록 보강했습니다. 녹화 디렉터리에는 쓰기 권한을 주되, 그 안에서 실행하는 바이너리까지 바꿀 수 있게 해서는 안 됐습니다.
다른 선택을 버린 이유
root init 서비스를 두면 필요한 디렉터리를 쉽게 고칠 수 있지만, 매 기동마다 넓은 권한으로 볼륨을 만지는 경로가 남습니다. gosu를 쓰는 방식도 처음에는 root로 시작합니다. Compose의 pre_start를 쓰면 팀의 도구 버전 조건이 바뀝니다.
| 대안 | 추가로 맡아야 하는 책임 |
|---|---|
| root init | 기동마다 높은 권한으로 변경할 대상 제한 |
| gosu로 사용자 전환 | root 시작과 전환 단계 유지 |
| Compose pre_start | 팀 실행 환경의 지원 버전 관리 |
| 빌드 시 디렉터리 소유권 확정 | 신규 볼륨 초기화와 기존 볼륨 이행 분리 |
이 대안들을 비교한 뒤 런타임 root 작업을 남기지 않는 쪽을 택했습니다. 신규 실행의 조건은 이미지 빌드로 고정할 수 있습니다. 이미 쓰던 볼륨의 소유권까지 바뀌는 것은 아니므로 배포에서는 신규 생성과 기존 데이터 이행을 나눠야 했습니다.
검증은 사용자 이름에서 녹화·훅까지 이어졌습니다
설정만 검사하고 끝내지 않고 USER, COPY 소유권, 볼륨, 바이너리 쓰기 모드, NoNewPrivs를 실제 녹화·훅 실행까지 이어 확인했습니다. 검증과 이행 범위에 그 경로를 정리했습니다. 녹화 길이가 기대보다 길어지는 현상도 있었지만 root 대조군에서도 나타났습니다. 권한 변경에서 생긴 회귀인지 구분하려면 사용자 설정을 되돌린 대조가 필요했습니다.
기존 볼륨은 대상을 지정해 소유권을 이행해야 합니다. DB 볼륨까지 지우는 down -v를 일반적인 이행 방법으로 사용하지 않습니다. 컨테이너가 새로 만들어져도 데이터 볼륨의 수명은 이어진다는 사실을 배포 절차에서 반영해야 합니다.
최종적으로 신규 볼륨에서는 UID 10002가 녹화하고 UID 10001이 읽을 수 있게 만들면서, 실행 훅은 두 계정이 덮어쓸 수 없도록 분리했습니다. 기존 볼륨은 별도 이행 대상으로 남겼습니다. USER 한 줄을 고치는 작업이 이미지의 초기 상태, 데이터의 수명, 두 프로세스의 공유 권한을 맞추는 작업으로 넓어진 이유입니다.
이 글은 PokeClip의 개발·검증 기록을 2026년 10월 15일 기준으로 정리한 CASE 18입니다. 전체 기술 기록에서 연결되는 결정을 함께 읽을 수 있습니다.