Quick Syntax
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"]root가 필요한 설치와 권한 설정을 끝낸 뒤 USER app으로 앱을 시작합니다. COPY --chown은 Linux container용 Dockerfile 옵션이므로 Windows container에서는 같은 방식으로 권한을 맞출 수 없다는 점도 함께 확인합니다.
실행 사용자와 파일 소유권
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"]최소 권한 경계
사용자에 기본 그룹이 없거나 그룹 지정이 부정확하면 예상과 다른 그룹 권한으로 실행될 수 있습니다. 이름 기반 사용자와 UID/GID 기반 사용자를 섞을 때는 베이스 이미지에 실제 계정이 존재하는지도 확인해야 합니다.
USER app만 추가하면 보안이 자동으로 끝나는 것이 아닙니다. 앱 사용자가 쓸 필요가 없는 경로까지 소유하게 만들면 권한을 줄인 효과가 약해집니다. 실행 사용자, 파일 소유권, 쓰기 가능한 디렉터리를 함께 설계해야 합니다.
참고 링크
2 sources