ASP.NET Core 5.2 · 5부. 배포와 실전

Docker로 배포하기

컨테이너에 담아 올립니다.

예상 학습 시간 18분 난이도 실전
개념 설명

돌아갈 자리까지 함께 담습니다

5.1에서 환경을 나눴습니다. 그런데 운영 서버에 .NET 런타임이 깔려 있어야 앱이 돕니다. 버전이 다르거나 없으면 뜨지 않습니다.

컨테이너는 앱과 런타임, 그리고 필요한 것들을 한 덩어리로 묶습니다. 그 덩어리를 이미지라고 부르고, 그것을 띄운 것이 컨테이너입니다. 어느 기계에서 띄우든 안쪽은 같습니다.

  • "제 기계에서는 되는데" 가 줄어듭니다. 런타임 버전과 설치된 것이 이미지에 박혀 있습니다.
  • 올리는 것이 파일 묶음이 아니라 이미지 하나입니다. 되돌릴 때도 앞 이미지를 띄우면 됩니다.
  • 같은 이미지를 여러 개 띄우기 쉽습니다.
최소 예제

Dockerfile 없이 만들기

.NET SDK 에 이미지를 만드는 기능이 들어 있습니다. Dockerfile 을 적지 않아도 되고, Docker 가 깔려 있지 않아도 됩니다. 파일로 뽑아 두었다가 나중에 올릴 수 있습니다.

터미널
dotnet publish -c Release --os linux --arch x64 \
  -t:PublishContainer \
  -p:ContainerArchiveOutputPath=./api.tar.gz
출력
기본 이미지 'mcr.microsoft.com/dotnet/aspnet:10.0' 위에 'latest' 태그가 있는 'api' 이미지를 빌드하는 중입니다. 'api:latest' 이미지를 './api.tar.gz'의 로컬 보관에 푸시했습니다. $ ls -la api.tar.gz 91.5 MB

이 단원의 명령은 브라우저가 아니라 여러분의 터미널에서 돌립니다.

만들어진 파일을 열어 보면 층이 여럿입니다. 마지막 하나가 우리 앱이고 나머지는 기본 이미지입니다.

0926a8eb…/layer.tar
fec4a809…/layer.tar
0473cde9…/layer.tar
      …
28e60b7b….json          // 이미지 설정

Docker 가 있다면 곧바로 올릴 수도 있습니다. 레지스트리 주소를 적으면 그쪽으로 갑니다.

dotnet publish -c Release --os linux --arch x64 -t:PublishContainer \
  -p:ContainerRegistry=myregistry.azurecr.io \
  -p:ContainerRepository=todo-api \
  -p:ContainerImageTag=1.0.3
상세 사용법

이미지의 거의 전부가 기본 이미지입니다

우리 앱을 그냥 게시하면 얼마나 될까요. 재어 보았습니다.

잰 것
dotnet publish -c Release --os linux --arch x64 -o ./out
ls ./out
출력
Api Api.deps.json Api.dll Api.pdb Api.runtimeconfig.json appsettings.json appsettings.Development.json … 파일 8개, 모두 합쳐 121KB

앱은 121KB 이고 이미지는 91.5MB 입니다. 나머지는 리눅스와 .NET 런타임입니다. 여기서 두 가지가 따라옵니다.

  • 레이어 캐시가 중요합니다. 기본 이미지는 바뀌지 않으므로 한 번 받아 두면 다시 받지 않습니다. 배포할 때마다 오가는 것은 앱 층뿐입니다.
  • 기본 이미지를 무엇으로 두느냐가 크기를 정합니다. 우리 코드를 줄여도 표가 나지 않습니다.

더 작게 만들려면

런타임을 앱 안에 넣고(자체 포함) 사용하지 않는 것을 걷어내면(트리밍), .NET 이 들어 있지 않은 더 작은 기본 이미지를 사용할 수 있습니다.

터미널
dotnet publish -c Release --os linux --arch x64 -t:PublishContainer \
  --self-contained true -p:PublishTrimmed=true \
  -p:ContainerBaseImage=mcr.microsoft.com/dotnet/runtime-deps:10.0 \
  -p:ContainerArchiveOutputPath=./slim.tar.gz
잰 크기
aspnet:10.0 위에 (기본) 91.5 MB runtime-deps:10.0 + 트리밍 56.4 MB

대신 잃는 것이 있습니다. 트리밍은 실제로 부르는 것만 남기므로, 반사로 형식을 찾는 코드가 실행할 때 깨질 수 있습니다. 오류가 빌드가 아니라 실행에서 나므로 돌려 보고 확인해야 합니다.

서버가 몇 대뿐이라면 91.5MB 로 두는 편이 안전합니다. 이미지를 자주 내려받는 곳이라면 줄일 값이 있습니다.

상세 사용법

Dockerfile 로 적을 때

빌드 과정을 직접 정해야 한다면 Dockerfile 을 적습니다. 두 단계로 나누는 것이 관례입니다.

Dockerfile
# ── 1단계: 빌드 (SDK 가 있는 이미지) ──
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# 프로젝트 파일만 먼저 넣고 복원합니다.
# 소스만 바뀌면 이 층은 캐시가 그대로 쓰입니다.
COPY Api/Api.csproj Api/
RUN dotnet restore Api/Api.csproj

COPY . .
RUN dotnet publish Api/Api.csproj -c Release -o /app

# ── 2단계: 실행 (런타임만 있는 이미지) ──
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .

ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080

ENTRYPOINT ["dotnet", "Api.dll"]
두 단계로 나누는 까닭
SDK 이미지는 컴파일러와 도구가 들어 있어 훨씬 큽니다. 그것으로 빌드만 하고, 결과물만 런타임 이미지로 옮깁니다. 소스와 SDK 는 최종 이미지에 들어가지 않습니다.

COPY 를 두 번 나눈 것이 요령입니다. Docker 는 줄 단위로 캐시합니다. 소스만 고쳤을 때 restore 를 다시 하지 않으려면, 프로젝트 파일을 먼저 넣고 복원한 뒤에 나머지를 넣어야 합니다.

.dockerignore 도 함께 둡니다. 없으면 bin·obj 가 통째로 들어가 캐시를 매번 깨뜨립니다.

bin/
obj/
.git/
**/appsettings.Development.json

마지막 줄을 눈여겨보십시오. 4.4에서 본 대로 개발용 설정은 이미지에 넣지 않습니다. 5.1의 환경 변수로 정합니다.

버전 배지

포트와 사용자

.NET 8 부터

컨테이너 기본 포트가 80에서 8080으로 바뀌었습니다. 그리고 이미지가 루트가 아닌 사용자로 돌도록 바뀌었습니다. 1024보다 작은 포트는 루트가 아니면 열 수 없어 함께 바뀐 것입니다.

옛 예제를 그대로 옮겨 EXPOSE 80 만 적으면 연결되지 않습니다. 포트는 ASPNETCORE_HTTP_PORTS 로 정합니다(예전의 ASPNETCORE_URLS 도 그대로 동작합니다).

실습 문제

직접 해보기

1. 이미지 하나 만들어 보기 난이도 하

1.2에서 만든 프로젝트로 이미지를 만들어 파일로 뽑고, 크기를 재어보세요. Docker 를 깔지 않아도 됩니다.

프로젝트 파일 이름을 함께 적어야 할 수 있습니다.
dotnet publish TodoApi.csproj -c Release --os linux --arch x64 \ -t:PublishContainer \ -p:ContainerArchiveOutputPath=./todo.tar.gz # 이미지 이름과 태그를 정하려면 # -p:ContainerRepository=todo-api # -p:ContainerImageTag=1.0.0
2. 캐시를 깨뜨리는 줄 찾기 난이도 중

아래 Dockerfile 은 소스를 한 글자만 고쳐도 복원부터 다시 합니다. 왜 그런지와 어떻게 고칠지 적어보세요.

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app
캐시는 줄 단위입니다. 어느 줄이 바뀌면 그 아래가 모두 다시 돕니다.
# COPY . . 가 소스를 모두 넣으므로, 한 글자만 고쳐도 이 층이 # 달라집니다. 그 아래의 restore 도 함께 다시 돕니다. # 고치면 COPY Api/Api.csproj Api/ RUN dotnet restore Api/Api.csproj # 프로젝트 파일이 그대로면 캐시 COPY . . RUN dotnet publish Api/Api.csproj -c Release -o /app # 패키지를 더하지 않는 한 restore 는 다시 돌지 않습니다.
3. 컨테이너에는 뜨는데 열리지 않습니다 난이도 상

이미지를 띄웠고 로그에 Application started 까지 나왔습니다. 그런데 브라우저로 열리지 않습니다. 무엇을 살펴야 할지 세 가지 이상 적어보세요.

로그의 "Now listening on" 줄에 어떤 주소가 적혀 있습니까. 컨테이너 안에서의 이야기입니다.
// 1. 안쪽 포트와 바깥 포트를 이었는가. // docker run -p 8080:8080 ← 두 숫자가 모두 필요합니다. // 2. 안에서 어떤 포트로 듣고 있는가. // 로그의 "Now listening on" 을 봅니다. .NET 8 부터 8080 이라 // 옛 예제대로 80 으로 이으면 닿지 않습니다. // 3. localhost 로만 듣고 있지 않은가. // http://localhost:8080 으로 듣고 있으면 컨테이너 안에서만 // 닿습니다. http://+:8080 또는 0.0.0.0 이어야 합니다. // ASPNETCORE_HTTP_PORTS 로 정하면 알아서 그렇게 됩니다. // 4. https 로만 열려 있지 않은가. // 컨테이너 안에는 인증서가 없는 것이 보통이라, 앞단(리버스 // 프록시)에서 https 를 맡고 안쪽은 http 로 두는 것이 흔합니다.
요약
  • 컨테이너는 앱과 런타임을 한 덩어리로 묶어, 어느 기계에서 띄우든 안쪽이 같게 합니다.
  • SDK 에 이미지 만들기가 들어 있어 Dockerfile 도 Docker 도 없이 만들 수 있습니다.
  • 앱은 121KB 인데 이미지는 91.5MB 입니다. 거의 전부가 기본 이미지이므로 레이어 캐시가 중요합니다.
  • Dockerfile 은 빌드와 실행을 두 단계로 나누고, 프로젝트 파일을 먼저 넣어 복원 캐시를 지킵니다.
  • .NET 8 부터 기본 포트가 8080 이고 루트가 아닌 사용자로 돕니다.