본문 바로가기
WIKI 기술 지식 베이스

Docker 이미지 빌드하기

원문 보기 위키 갱신

이 절에서는 Backstage App을 배포 가능한 Docker 이미지로 빌드하는 방법을 설명해요. 크게 세 부분으로 나뉘는데, 먼저 속도와 더 효율적이고 종종 더 단순한 캐싱 때문에 권장되는 호스트 빌드 방식, 두 번째로 전체 멀티 스테이지 Docker 빌드, 마지막으로 프론트엔드와 백엔드를 별도의 이미지로 배포하는 방법을 다뤄요.

출처: 문서

본문

요약(Summary)

이 절에서는 Backstage App을 배포 가능한 Docker 이미지로 빌드하는 방법을 설명해요. 크게 세 부분으로 나뉘는데, 먼저 속도와 더 효율적이고 종종 더 단순한 캐싱 때문에 권장되는 호스트 빌드 방식, 두 번째로 전체 멀티 스테이지 Docker 빌드, 마지막으로 프론트엔드와 백엔드를 별도의 이미지로 배포하는 방법을 다뤄요.

사전 준비

이 가이드는 Docker와 그것이 어떻게 작동하는지에 대한 기본적인 이해가 있다고 가정해요. Docker가 처음이라면 Docker 개요 가이드에서 시작할 수 있어요.

또한 다음 사전 준비도 완료해야 해요.

  • 시작하기 가이드를 따라 앱을 만들었을 것
  • 인증(auth) 프로바이더를 설정했을 것. 인증 가이드가 좋은 시작점이며, 기본 Guest 인증 프로바이더는 컨테이너 환경에서 사용하기 위한 것이 아니에요.
  • 연결할 수 있는 Postgres 데이터베이스가 준비되어 있을 것. 데이터베이스 가이드가 도움이 될 수 있어요.

경고

이 사전 준비 없이 진행하면 바람직하지 않은 결과를 초래할 가능성이 매우 높아요. 가장 흔한 것은 기본 Guest 인증 프로바이더가 컨테이너 환경에서 사용하기 위한 것이 아니기 때문에 적절한 인증 프로바이더가 설정되지 않는 것이에요.

호스트 빌드(Host Build)

이 절에서는 빌드의 대부분이 Docker 밖에서 일어나는 Backstage 저장소로부터 Docker 이미지를 빌드하는 방법을 설명해요. 빌드 단계가 더 빠르게 실행되는 경향이 있고, 단일 변경이 전체 캐시를 깨뜨리지 않는 호스트에서 의존성을 더 효율적으로 캐싱할 수 있기 때문에 거의 항상 더 빠른 방식이에요.

호스트 빌드에 필요한 단계는 yarn install로 의존성을 설치하고, yarn tsc로 타입 정의를 생성하며, yarn build:backend로 백엔드 패키지를 빌드하는 것이에요.

참고

이 명령들을 Docker 기본 이미지와 같은 Node 버전으로 실행하세요. 다른 버전을 사용하면 런타임에 네이티브 모듈이 실패하게 돼요.

CI 워크플로에서는 루트에서 다음과 같이 보일 수 있어요.

yarn install --immutable
# tsc outputs type definitions to dist-types/ in the repo root, which are then consumed by the build
yarn tsc
# Build the backend, which bundles it all up into the packages/backend/dist folder.
yarn build:backend

호스트 빌드가 완료되면 이미지를 빌드할 준비가 된 거예요. 다음 Dockerfile은 @backstage/create-app으로 새 앱을 만들 때 포함돼 있어요.

FROM node:24-trixie-slim

# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && \
    apt-get install -y --no-install-recommends libsqlite3-dev && \
    rm -rf /var/lib/apt/lists/*

# From here on we use the least-privileged `node` user to run the backend.
USER node

# This should create the app dir as `node`.
# If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`.
WORKDIR /app

# Copy files needed by Yarn
COPY --chown=node:node .yarn ./.yarn
COPY --chown=node:node .yarnrc.yml ./
COPY --chown=node:node backstage.json ./

# This switches many Node.js dependencies to production mode.
ENV NODE_ENV=production

# Copy repo skeleton first, to avoid unnecessary docker cache invalidation.
# The skeleton contains the package.json of each package in the monorepo,
# and along with yarn.lock and the root package.json, that's enough to run yarn install.
COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
    yarn workspaces focus --all --production && rm -rf "$(yarn cache clean)"

# This will include the examples, if you don't need these simply remove this line
COPY --chown=node:node examples ./examples

# Then copy the rest of the backend bundle, along with any other files we might want.
COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz

CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]

backend:bundle 명령과 skeleton.tar.gz 파일이 어떻게 작동하는지에 대한 자세한 내용은 backend:bundle 명령 문서를 참고하세요.

Dockerfile은 packages/backend/Dockerfile에 있지만, 루트 yarn.lock과 package.json, 그리고 .npmrc 같은 필요할 수 있는 다른 파일들에 접근하려면 저장소 루트를 빌드 컨텍스트로 사용해 실행해야 해요.

@backstage/create-app 명령은 빌드 컨텍스트 크기를 줄여 빌드를 빠르게 하기 위해 다음 .dockerignore를 저장소 루트에 추가해요.

.git
.yarn/cache
.yarn/install-state.gz
node_modules
packages/*/src
packages/*/node_modules
plugins
*.local.yaml

프로젝트가 빌드되었고 .dockerignore와 Dockerfile이 준비되면 최종 이미지를 빌드할 준비가 돼요. 저장소 루트에서 빌드를 실행하세요.

docker image build . -f packages/backend/Dockerfile --tag backstage

로컬에서 이미지를 시험해보려면 다음을 실행할 수 있어요.

docker run -it -p 7007:7007 backstage

그러면 터미널에서 로그가 나오기 시작하고, 브라우저에서 http://localhost:7007을 열 수 있어요.

멀티 스테이지 빌드(Multi-stage Build)

참고

이 설정에서는 .dockerignore가 다르니 자세한 내용은 계속 읽어보세요.

이 절에서는 전체 프로젝트를 Docker 안에서 빌드하는 멀티 스테이지 Docker 빌드를 설정하는 방법을 설명해요. 이는 보통 호스트 빌드보다 느리지만, 빌드 환경에 Docker in Docker가 없거나 다른 요구 사항 때문에 종종 필요할 수 있어요.

빌드는 세 단계로 나뉘는데, 첫 단계는 모든 의존성을 설치하는 초기 yarn install을 캐싱할 수 있게 초기 설치 단계에 관련된 모든 package.json 파일을 찾아요. 두 번째 단계는 빌드 자체를 실행하며 호스트 빌드에서 호스트에서 실행하는 단계와 비슷해요. 세 번째이자 마지막 단계는 모든 것을 최종 이미지로 패키징하며 호스트 빌드의 Dockerfile과 비슷해요.

다음 Dockerfile은 멀티 스테이지 빌드를 실행하며 저장소 루트에 추가해야 해요.

# Stage 1 - Create yarn install skeleton layer
FROM node:24-trixie-slim AS packages
WORKDIR /app
COPY backstage.json package.json yarn.lock ./
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY packages packages
# Comment this out if you don't have any internal plugins
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+

# Stage 2 - Install dependencies and build packages
FROM node:24-trixie-slim AS build
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && \
    apt-get install -y --no-install-recommends libsqlite3-dev && \
    rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /app
COPY --from=packages --chown=node:node /app .
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
    yarn install --immutable
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
    && tar xzf packages/backend/dist/skeleton.tar.gz -C packages/backend/dist/skeleton \
    && tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle

# Stage 3 - Build the actual backend image and install production dependencies
FROM node:24-trixie-slim
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && \
    apt-get install -y --no-install-recommends libsqlite3-dev && \
    rm -rf /var/lib/apt/lists/*

# From here on we use the least-privileged `node` user to run the backend.
USER node

# This should create the app dir as `node`.
# If it is instead created as `root` then the `tar` command below will
# fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`)
# so the app dir is correctly created as `node`.
WORKDIR /app

# Copy the install dependencies from the build stage and context
COPY --from=build --chown=node:node /app/.yarn ./.yarn
COPY --from=build --chown=node:node /app/.yarnrc.yml  ./
COPY --from=build --chown=node:node /app/backstage.json ./
COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./

# Note: The skeleton bundle only includes package.json files -- if your app has
# plugins that define a `bin` export, the bin files need to be copied as well to
# be linked in node_modules/.bin during yarn install.
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
    yarn workspaces focus --all --production && rm -rf "$(yarn cache clean)"

# Copy the built packages from the build stage
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./

# Copy any other files that we need at runtime
COPY --chown=node:node app-config*.yaml ./

# This will include the examples, if you don't need these simply remove this line
COPY --chown=node:node examples ./examples

# This switches many Node.js dependencies to production mode.
ENV NODE_ENV=production

CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]

새로 만든 Backstage 앱에는 일반적으로 plugins/ 폴더가 없으므로 그 줄을 주석 처리하고 싶을 거예요. 이 빌드는 빌드에 사용되는 backstage-cli가 제대로 설치되지 않기 때문에 메인 저장소에서도 작동하지 않아요.

저장소의 새 클론이 아닌 곳에서 빌드할 때 빌드를 빠르게 하려면 .dockerignore를 설정해야 해요. 이것은 호스트 빌드용과 다르고, 빌드를 위해 모든 패키지의 소스 코드에 접근해야 하기 때문이에요. 하지만 호스트의 기존 빌드 출력물이나 의존성은 무시할 수 있어요. 새 .dockerignore를 위해 기존 내용을 이걸로 교체하세요.

dist-types
node_modules
packages/*/dist
packages/*/node_modules
plugins/*/dist
plugins/*/node_modules
*.local.yaml

Dockerfile과 .dockerignore를 모두 프로젝트 루트에 추가한 후, 다음을 실행해 지정된 태그로 컨테이너를 빌드하세요.

docker image build -t backstage .

로컬에서 이미지를 시험해보려면 다음을 실행할 수 있어요.

docker run -it -p 7007:7007 backstage

그러면 터미널에서 로그가 나오기 시작하고, 브라우저에서 http://localhost:7007을 열 수 있어요.

별도 프론트엔드(Separate Frontend)

참고

이것은 선택 단계이며, @backstage/plugin-app-backend 플러그인의 기능을 잃게 돼요. 가장 주목할 만한 것은 프론트엔드 구성이 더 이상 백엔드에서 주입되지 않고, 프론트엔드 번들을 빌드할 때 올바른 구성을 사용해야 한다는 점이에요.

프론트엔드를 백엔드와 별도로, 별도 이미지에서 또는 예를 들어 정적 파일 서빙 프로바이더에서 서빙하는 것이 때때로 바람직해요. 이를 위한 첫 단계는 백엔드 패키지에서 app-backend 플러그인을 제거하는 것이며, 다음과 같이 합니다.

  • packages/backend/src/plugins/app.ts 삭제
  • packages/backend/src/index.ts에서 다음 줄 제거:
backend.add(import('@backstage/plugin-app-backend'));
  • packages/backend/package.json에서 @backstage/plugin-app-backend와 app 패키지 의존성(예: app) 제거. app 패키지 의존성을 제거하지 않으면 앱이 여전히 빌드되어 백엔드와 함께 번들로 묶여요.

app-backend가 백엔드에서 제거되면 프론트엔드를 서빙하기 위해 좋아하는 정적 파일 서빙 방법을 사용할 수 있어요. NGINX 이미지 설정 방법의 예시는 메인 저장소의 contrib 폴더에서 찾을 수 있어요.

프론트엔드의 별도 docker 빌드를 만들고 있다면 아마 .dockerignore를 적절히 조정해야 할 거예요. 대부분 packages/app/dist가 무시되지 않도록 하는 것으로 충분할 거예요.

트러블슈팅 팁

Docker 이미지를 빌드할 때 종종 문제가 발생할 수 있는데, 도움이 되는 두 가지 유용한 플래그가 있어요.

  • --progress=plain: 더 자세한 출력을 주고 로그를 섹션으로 접지 않아요. 오류가 있는데 마지막 명령과 어쩌면 종료 코드만 보여줄 때 매우 유용해요. 이 플래그를 사용하면 오류가 실제로 어디에 있는지 더 잘 볼 수 있어요.
  • --no-cache: 매번 모든 레이어를 다시 빌드해요. 처음부터 빌드하고 있는지 확실히 하고 싶을 때 유용해요.

이 플래그 사용 예시는 다음과 같아요.

docker image build . -f packages/backend/Dockerfile --tag backstage --progress=plain --no-cache

커뮤니티 기여 Dockerfile 대안

위에서 언급한 packages/backend에 있는 Dockerfile은 Backstage 유지보수자가 유지보수하지만, contrib/docker에는 커뮤니티 기여 Dockerfile 대안도 있어요. contrib/docker의 Dockerfile들은 Backstage 유지보수자가 유지보수하지 않으며, packages/backend의 Dockerfile이 업데이트될 때 반드시 업데이트되지는 않아요.

최소 강화 이미지(Minimal Hardened Image)

contrib/docker/minimal-hardened-image 디렉터리 안에 wolfi-base 이미지를 사용해 취약점을 줄이는 기여 Dockerfile이 있어요. 이걸 기여했을 때, 이 대안 Dockerfile은 packages/backend/Dockerfile에서 빌드한 이미지와 비교해 빌드된 Backstage docker 이미지의 취약점을 98.2% 줄였어요.

유지보수를 줄이기 위해 이미지의 다이제스트는 contrib/docker/minimal-hardened-image/Dockerfile 파일에서 제거됐어요. 다이제스트가 포함된 완전한 예시는 cgr.dev/chainguard/wolfi-base:latest@sha256:3d6dece13cdb5546cd03b20e14f9af354bc1a56ab5a7b47dca3e6c1557211fcf이며, Dockerfile의 FROM 줄을 다이제스트를 사용하도록 업데이트하는 것을 권장해요. 최신 다이제스트를 얻으려면 이미지에서 docker pull을 수행하세요. 다이제스트를 사용하면 Dependabot이나 Renovate 같은 도구가 정확히 어떤 이미지 다이제스트가 사용되는지 알 수 있고, 새 다이제스트가 나올 때 Pull Request가 트리거될 수 있어요.

이미지가 최신 상태로 유지되어 해결된 취약점 수정이 자주 반영되도록 Dependabot/Renovate나 비슷한 도구를 설정하는 것을 권장해요.

더 알아보기 (Learn more)