숏컷 코드
FROM node:22-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=app:app . .
USER app
CMD ["node", "server.js"]문법
USER는 이후 빌드 명령과 런타임 명령의 기본 사용자를 바꾼다
USER <user>[:<group>] 또는 USER <UID>[:<GID>]는 Dockerfile의 이후 RUN, CMD, ENTRYPOINT 실행 사용자를 바꿉니다. 따라서 파일을 설치하거나 권한을 조정해야 하는 단계는 root로 처리하고, 앱 실행 직전에 USER를 바꾸는 패턴이 일반적입니다.
RUN apk add --no-cache tini
RUN adduser -D app
USER app
CMD ["node", "server.js"]USER를 너무 일찍 선언하면 이후 패키지 설치나 파일 복사가 권한 오류를 낼 수 있습니다. 반대로 끝까지 root로 두면 앱 취약점이 컨테이너 내부 root 권한으로 실행됩니다.
사용자는 만들고, 파일 소유자는 맞춰야 한다
사용자를 선언하는 것만으로 /app 파일 소유자가 자동 변경되지는 않습니다. COPY --chown을 사용하거나, 복사 후 chown을 실행해 앱 사용자가 읽고 쓸 수 있는 파일만 갖게 해야 합니다.
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app . /app
WORKDIR /app
USER app빌드 결과물은 읽기 권한이면 충분하고, 로그나 업로드 파일처럼 쓰기가 필요한 경로만 별도 디렉터리로 분리하는 편이 안전합니다.
root가 필요한 작업과 앱 실행을 한 stage에 섞지 않는다
multi-stage build를 쓰면 빌드 도구는 builder stage에 두고, runtime stage에서는 필요한 파일만 복사한 뒤 non-root 사용자로 실행할 수 있습니다. 특히 컴파일러, 패키지 매니저, secret 접근 도구가 런타임 이미지에 남지 않게 할 수 있습니다.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=build --chown=app:app /app/dist ./dist
USER app
CMD ["node", "dist/server.js"]체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 패키지 설치와 사용자 생성 | root 단계에서 처리 |
| 앱 프로세스 실행 | USER app 이후 CMD |
| 파일 복사 시 소유권 지정 | COPY --chown=user:group |
| 쓰기 가능한 경로 제한 | 로그·업로드 디렉터리만 별도 권한 |
| 빌드 도구 제거 | multi-stage build 사용 |
| Windows 컨테이너 | 사용자를 먼저 생성한 뒤 USER 지정 |
주의할 점
USER app만 추가하면 보안이 자동으로 끝나는 것이 아닙니다. 앱 사용자가 쓸 필요가 없는 경로까지 소유하게 만들면 권한을 줄인 효과가 약해집니다. 실행 사용자, 파일 소유권, 쓰기 가능한 디렉터리를 함께 설계해야 합니다.
사용자에 기본 그룹이 없거나 그룹 지정이 부정확하면 예상과 다른 그룹 권한으로 실행될 수 있습니다. 이름 기반 사용자와 UID/GID 기반 사용자를 섞을 때는 베이스 이미지에 실제 계정이 존재하는지도 확인해야 합니다.
참고 링크
2 sources