최소화되고 효율적인 Rust 웹 애플리케이션 Docker 이미지 구축하기
Lukas Schneider
DevOps Engineer · Leapcell

소개
클라우드 네이티브 개발의 빠르게 진화하는 환경에서 애플리케이션을 효율적으로 패키징하고 배포하는 능력은 매우 중요합니다. Docker는 컨테이너화의 사실상의 표준으로 부상하여 휴대성과 일관된 실행 환경을 제공합니다. 성능과 안전성으로 알려진 Rust 웹 애플리케이션의 경우, 최소화되고 효율적인 Docker 이미지를 만드는 것은 단순한 최적화가 아니라 고유한 장점을 극대화하기 위한 근본적인 단계입니다. 더 작은 이미지 크기는 더 빠른 다운로드, 저장 비용 감소, 더 빠른 시작 시간, 그리고 더 작은 공격 표면으로 이어지며, 이 모든 것은 현대 마이크로서비스 아키텍처와 서버리스 배포에 매우 중요합니다. 이 글은 Rust 웹 애플리케이션을 위한 간소화된 Docker 이미지를 구축하는 과정을 안내하며, 가능한 가장 가볍고 성능이 뛰어나도록 보장합니다.
효율적인 컨테이너화를 위한 핵심 개념
실질적인 내용으로 들어가기 전에, 효율적인 Rust Docker 이미지를 구축하기 위한 전략의 기초가 되는 몇 가지 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
- 다단계 빌드 (Multi-stage Builds): 이 Docker 기능은 Dockerfile에서 여러 개의
FROM
명령문을 사용할 수 있게 합니다. 각FROM
지시어는 새로운 빌드 단계를 시작합니다. 그런 다음 한 단계에서 다른 단계로 아티팩트를 선택적으로 복사하여 빌드 시간 종속성과 런타임 요구 사항을 효과적으로 분리할 수 있습니다. 이는 최종 이미지 크기를 작게 유지하는 데 중요합니다. - Musl libc (정적 링크):
musl
은 정적 링크를 위해 설계된 가볍고 빠르며 안전한 C 표준 라이브러리입니다.musl
로 Rust 애플리케이션을 컴파일함으로써, 시스템 동적 라이브러리(예:glibc
)에 의존하지 않는 완전히 정적으로 링크된 바이너리를 생성할 수 있습니다. 이는 최종 이미지를 매우 작고 이식 가능하게 만듭니다. 왜냐하면 바이너리 자체만 필요하기 때문입니다. FROM scratch
: 이것은 Docker에서 가능한 가장 작은 기본 이미지입니다. 운영 체제조차 포함되어 있지 않은 아무것도 포함하지 않습니다. 정적으로 링크된 바이너리와 함께 사용될 때, 가능한 가장 최소한의 최종 이미지를 생성합니다.- 빌드 캐싱 (Build Caching): Docker 레이어와 빌드 캐싱은 기본입니다. Docker가 레이어를 캐시하는 방식(
Dockerfile
명령문을 기반으로)을 이해하면 자주 변경되는 명령문을 나중에 배치하여 빌드 프로세스를 최적화할 수 있습니다. - 릴리스 vs. 디버그 빌드: Rust는 다양한 컴파일 프로파일을 제공합니다.
cargo build --release
는 성능과 크기를 위해 바이너리를 최적화하며, 디버그 기호를 제거하고 다양한 최적화를 적용합니다. 이는 프로덕션 배포에 필수적입니다.
최소화된 Rust 웹 애플리케이션 Docker 이미지 구축
Rust를 위한 최소화된 Docker 이미지를 구축하는 본질은 다단계 빌드와 정적 링크를 활용하는 것입니다. 간단한 actix-web
애플리케이션의 실제 예시를 살펴보겠습니다.
간단한 actix-web
애플리케이션부터 시작해 봅시다.
// src/main.rs use actix_web::{get, App, HttpResponse, HttpServer, Responder}; #[get("/")] async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello from Rust on Docker!") } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hello)) .bind(("0.0.0.0", 8080))? .run() .await }
그리고 Cargo.toml
:
# Cargo.toml [package] name = "my-rust-app" version = "0.1.0" edition = "2021" [dependencies] actix-web = "4" tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
이제 최적화된 Dockerfile
을 만들어 봅시다.
# Stage 1: Builder # 안정적인 빌드 환경을 위해 Debian slim과 함께 특정 Rust 버전을 사용합니다. FROM rust:1.76-slim-bookworm AS builder # 컨테이너 내에서 작업 디렉토리를 설정합니다. WORKDIR /app # 정적 컴파일을 위해 musl-tools를 설치합니다. RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/* # 정적 링크를 위해 musl 타겟을 추가합니다. RUN rustup target add x86_64-unknown-linux-musl # Docker 캐시를 활용하기 위해 먼저 Cargo.toml과 Cargo.lock만 복사합니다. # 이 레이어는 소스 코드보다 덜 자주 변경됩니다. COPY Cargo.toml Cargo.lock ./ # 종속성만 빌드합니다. 이 레이어는 캐시 가능성이 높습니다. # Cargo.toml 및 Cargo.lock이 변경되지 않았다면 이 단계는 건너뜁니다. RUN cargo fetch --locked --target x86_64-unknown-linux-musl # 모든 소스 코드를 복사합니다. COPY src ./src # musl 타겟과 함께 릴리스 바이너리를 빌드합니다. # --release는 최적화 및 더 작은 크기를 위해 # --locked는 재현 가능한 빌드를 보장하기 위해 # --target은 musl을 사용한 정적 링크를 위해 RUN CARGO_INCREMENTAL=0 \ RUSTFLAGS="-C strip=debuginfo -C target-feature=+aes,+sse2,+ssse3" \ cargo build --release --locked --target x86_64-unknown-linux-musl # Stage 2: Runner # 가장 작은 최종 이미지를 얻기 위해 scratch부터 시작합니다. FROM scratch # 빌더 스테이지에서 컴파일된 바이너리만 복사합니다. COPY /app/target/x86_64-unknown-linux-musl/release/my-rust-app . # 애플리케이션이 수신 대기하는 포트를 노출합니다. EXPOSE 8080 # 애플리케이션을 실행하는 명령을 정의합니다. CMD ["./my-rust-app"]
이 Dockerfile을 자세히 살펴보겠습니다.
-
빌더 스테이지 (
FROM rust:1.76-slim-bookworm AS builder
):debian-slim
기반의rust
이미지로 시작합니다. 이는 불필요한 불필요한 요소를 제거한 완전한 Rust 툴체인과 필요한 빌드 종속성을 제공합니다.WORKDIR /app
은 후속 명령에 대한 작업 디렉토리를 설정합니다.RUN apt-get update && apt-get install -y musl-tools
:musl
컴파일에 필요한 정적 링커 및 헤더를 제공하는musl-tools
패키지를 설치합니다.RUN rustup target add x86_64-unknown-linux-musl
:x86_64-unknown-linux-musl
타겟을 Rust 툴체인에 추가하여 정적 링크를 활성화합니다.COPY Cargo.toml Cargo.lock ./
: 이것은 중요한 캐싱 최적화입니다. 먼저 매니페스트 파일만 복사하고 종속성을 빌드(cargo fetch
)하면 Docker는 이 레이어를 캐시할 수 있습니다. 소스 코드만 변경되면 이 무거운 종속성 컴파일 단계는 건너<binary data, 1 bytes><binary data, 1 bytes>니다.RUN cargo fetch --locked --target x86_64-unknown-linux-musl
: 모든 프로젝트 종속성을 가져옵니다.COPY src ./src
: 실제 소스 코드를 복사합니다. 소스 코드가 변경되면 이 레이어는 후속 단계에 대한 캐시를 무효화합니다.RUN CARGO_INCREMENTAL=0 RUSTFLAGS='-C strip=debuginfo -C target-feature=+aes,+sse2,+ssse3' cargo build --release --locked --target x86_64-unknown-linux-musl
: 애플리케이션을 컴파일합니다.CARGO_INCREMENTAL=0
: Docker에서의 릴리스 빌드에는 유용하지 않고 때로는 이미지 크기를 늘릴 수 있는 점진적 컴파일을 비활성화합니다.RUSTFLAGS='-C strip=debuginfo -C target-feature=+aes,+sse2,+ssse3'
: 최종 바이너리에서 디버그 정보를 제거하고 잠재적인 성능 향상을 위해 특정 CPU 기능을 활성화합니다.--release
: 성능 최적화 및 더 작은 바이너리 크기를 보장합니다.--locked
:Cargo.lock
을 사용하여 재현 가능한 빌드를 보장합니다.--target x86_64-unknown-linux-musl
:musl
을 사용한 정적 링크를 위한 타겟을 지정합니다.
-
러너 스테이지 (
FROM scratch
):FROM scratch
: 최소 이미지의 마법이 일어나는 곳입니다. 빈 기본값으로 시작합니다.COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-rust-app .
:builder
스테이지에서 최종 정적으로 링크된 바이너리만scratch
이미지로 복사합니다. 바이너리가 자체 포함(정적으로 링크됨)되어 있기 때문에scratch
이미지에 다른 파일이나 종속성이 필요하지 않습니다.EXPOSE 8080
: 컨테이너가 포트 8080을 수신 대기함을 Docker에 알리지만, 실제로는 포트를 게시하지는 않습니다.- `CMD [