Rust 언어별 가이드
Rust 언어별 가이드
이 가이드는 Docker로 컨테이너화된 Rust 어플리케이션을 만드는 방법을 알려줘요.
출처: 문서
본문
Rust 언어별 가이드는 Docker를 사용해 컨테이너화된 Rust 어플리케이션을 만드는 방법을 가르쳐줘요. 이 가이드에서 다음을 배우게 돼요:
- Rust 어플리케이션 컨테이너화하기
- 이미지 빌드하고 새로 빌드한 이미지를 컨테이너로 실행하기
- 볼륨과 네트워킹 설정하기
- Compose로 컨테이너 오케스트레이션하기
- 개발에 컨테이너 사용하기
Rust 모듈을 모두 마치면, 이 가이드의 예제와 설명을 바탕으로 자신만의 Rust 어플리케이션을 컨테이너화할 수 있게 돼요.
첫 번째 Rust 이미지를 빌드하는 것부터 시작해요.
Rust 이미지 빌드하기
준비 사항 (Prerequisites)
- 최신 버전의 Docker Desktop을 설치했어야 해요.
- git 클라이언트가 필요해요. 이 섹션의 예제는 명령줄 기반의 git 클라이언트를 사용하지만, 어떤 클라이언트를 써도 무방해요.
개요 (Overview)
이 가이드는 첫 번째 Rust 이미지를 빌드하는 과정을 안내해요. 이미지에는 어플리케이션을 실행하는 데 필요한 모든 것 — 코드나 바이너리, 런타임, 의존성, 그 외 필요한 모든 파일시스템 객체가 포함돼요.
샘플 어플리케이션 가져오기
이 가이드에서 사용할 샘플 어플리케이션을 클론해요. 터미널을 열고 작업할 디렉토리로 이동한 다음, 다음 명령을 실행해 저장소를 클론해요:
$ git clone https://github.com/docker/docker-rust-hello && cd docker-rust-hello
베이스 이미지 선택하기
[!TIP]
Gordon, Docker의 AI 어시스턴트가 프로젝트에 맞는 Docker 자산을 생성해줄 수 있어요. Gordon에게 어플리케이션에 맞춘 Dockerfile, Compose 파일,
.dockerignore를 만들어 달라고 요청해보세요.
Dockerfile을 편집하기 전에 베이스 이미지를 선택해야 해요. Rust Docker Official Image 또는 Docker Hardened Image (DHI)를 사용할 수 있어요.
Docker Hardened Images (DHI)는 Docker가 관리하는 최소화되고 안전하며 프로덕션 준비가 된 베이스 이미지예요. 취약점을 줄이고 컴플라이언스를 단순화하는 데 도움이 돼요. 자세한 내용은 Docker Hardened Images를 참고해요.
Docker Hardened Images 사용하기
Docker Hardened Images (DHI)는 공개적으로 제공되며 베이스 이미지로 직접 사용할 수 있어요. DHI를 pull하려면 Docker로 한 번 인증해요:
docker login dhi.io
dhi.io 레지스트리의 DHI를 사용해요, 예를 들면:
FROM dhi.io/rust:${RUST_VERSION}-alpine3.22-dev AS build
다음 Dockerfile은 Rust DHI를 빌드 베이스 이미지로 사용해요:
# Make sure RUST_VERSION matches the Rust version
ARG RUST_VERSION=1.92
ARG APP_NAME=docker-rust-hello
################################################################################
# Create a stage for building the application.
################################################################################
FROM dhi.io/rust:${RUST_VERSION}-alpine3.22-dev AS build
ARG APP_NAME
WORKDIR /app
# Install host build dependencies.
RUN apk add --no-cache clang lld musl-dev git
# Build the application.
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/var/cache/cargo \
CARGO_HOME=/var/cache/cargo cargo build --locked --release && \
cp ./target/release/$APP_NAME /bin/server
################################################################################
# Create a new stage for running the application that contains the minimal
# We use dhi.io/static for the final stage because it's a minimal Docker Hardened Image runtime (basically "just
# enough OS to run the binary"), which helps keep the image small and with a lower attack surface compared to a
# full Alpine/Debian runtime.
################################################################################
FROM dhi.io/static:20250419 AS final
# Copy the executable from the "build" stage.
COPY --from=build /bin/server /bin/
# Configure rocket to listen on all interfaces.
ENV ROCKET_ADDRESS=0.0.0.0
# Expose the port that the application listens on.
EXPOSE 8000
# What the container should run when it is started.
CMD ["/bin/server"]
Docker Official Images 사용하기
# Pin the Rust toolchain version used in the build stage.
ARG RUST_VERSION=1.92
# Name of the compiled binary produced by Cargo (must match Cargo.toml package name).
ARG APP_NAME=docker-rust-hello
################################################################################
# Build stage (DOI Rust image)
# This stage compiles the application.
################################################################################
FROM docker.io/library/rust:${RUST_VERSION}-alpine AS build
# Re-declare args inside the stage if you want to use them here.
ARG APP_NAME
# All build steps happen inside /app.
WORKDIR /app
# Install build dependencies needed to compile Rust crates on Alpine
RUN apk add --no-cache clang lld musl-dev git
# Build the application
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/var/cache/cargo \
CARGO_HOME=/var/cache/cargo cargo build --locked --release && \
cp ./target/release/$APP_NAME /bin/server
################################################################################
# Runtime stage (DOI Alpine image)
# This stage runs the already-compiled binary with minimal dependencies.
################################################################################
FROM docker.io/library/alpine:3.18 AS final
# Create a non-privileged user (recommended best practice)
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
# Drop privileges for runtime.
USER appuser
# Copy only the compiled binary from the build stage.
COPY --from=build /bin/server /bin/
# Rocket: listen on all interfaces inside the container.
ENV ROCKET_ADDRESS=0.0.0.0
# Document the port your app listens on.
EXPOSE 8000
# Start the application.
CMD ["/bin/server"]
캐시 마운트는 단일 CARGO_HOME을 사용하므로 동시 빌드가 Cargo의 캐시 락을 통해 접근을 조정해요. CARGO_HOME을 설정하면 Cargo가 전역 구성과 자격 증명을 찾는 위치도 바뀌어요. 프로젝트 레벨 .cargo/config.toml 파일은 영향을 받지 않아요.
이미지 빌드에는 Dockerfile만 있으면 돼요. 좋아하는 IDE나 텍스트 편집기에서 Dockerfile을 열고 무엇이 들어 있는지 확인해보세요. Dockerfile에 대해 더 알아보려면 Dockerfile reference를 참고해요.
.dockerignore 파일
.dockerignore 파일은 이미지를 최대한 작게 유지하기 위해 이미지에 복사하고 싶지 않은 패턴과 경로를 지정해요. 좋아하는 IDE나 텍스트 편집기에서 .dockerignore 파일을 열어 내용을 확인해보세요.
이미지 빌드하기
이제 Dockerfile을 만들었으니 이미지를 빌드할 수 있어요. 이렇게 하려면 docker build 명령을 사용해요. docker build 명령은 Dockerfile과 컨텍스트에서 Docker 이미지를 빌드해요. 빌드의 컨텍스트는 지정된 PATH 또는 URL에 있는 파일들의 집합이에요. Docker 빌드 과정은 이 컨텍스트에 있는 어떤 파일이든 접근할 수 있어요.
빌드 명령은 선택적으로 --tag 플래그를 받아요. 태그는 이미지의 이름과 name:tag 형식의 선택적 태그를 설정해요. 태그를 전달하지 않으면 Docker는 기본 태그로 "latest"를 사용해요.
Docker 이미지를 빌드해요.
$ docker build --tag docker-rust-image-dhi .
다음과 같은 출력을 볼 수 있어요.
[+] Building 1.4s (13/13) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.67kB 0.0s
=> [internal] load metadata for dhi.io/static:20250419 1.1s
=> [internal] load metadata for dhi.io/rust:1.92-alpine3.22-dev 1.2s
=> [auth] static:pull token for dhi.io 0.0s
=> [auth] rust:pull token for dhi.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 646B 0.0s
=> [build 1/3] FROM dhi.io/rust:1.92-alpine3.22-dev@sha256:49eb72825a9e15fe48f2c4875a63c7e7f52a5b430bb52b8254b91d132aa5bf38 0.0s
=> => resolve dhi.io/rust:1.92-alpine3.22-dev@sha256:49eb72825a9e15fe48f2c4875a63c7e7f52a5b430bb52b8254b91d132aa5bf38 0.0s
=> [final 1/2] FROM dhi.io/static:20250419@sha256:74fc43fa240887b8159970e434244039aab0c6efaaa9cf044004cdc22aa2a34d 0.0s
=> => resolve dhi.io/static:20250419@sha256:74fc43fa240887b8159970e434244039aab0c6efaaa9cf044004cdc22aa2a34d 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 117B 0.0s
=> CACHED [build 2/3] WORKDIR /build 0.0s
=> CACHED [build 3/3] RUN --mount=type=bind,source=src,target=src --mount=type=bind,source=Cargo.toml,target=Cargo.toml --mount=type=bind,source=Cargo.lock,target=Cargo 0.0s
=> CACHED [final 2/2] COPY --from=build /build/target/release/docker-rust-hello /server 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:cc937bbdd712ef6e5445501f77e02ef8455ef64c567598786d46b7b21a4d4fa8 0.0s
=> => exporting config sha256:077507b483af4b5e1a928e527e4bb3a4aaf0557e1eea81cd39465f564c187669 0.0s
=> => exporting attestation manifest sha256:11b60e7608170493da1fdd88c120e2d2957f2a72a22edbc9cfbdd0dd37d21f89 0.0s
=> => exporting manifest list sha256:99a1b925a8d6ebf80e376b8a1e50cd806ec42d194479a3375e1cd9d2911b4db9 0.0s
=> => naming to docker.io/library/docker-rust-image-dhi:latest 0.0s
=> => unpacking to docker.io/library/docker-rust-image-dhi:latest 0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/yczk0ijw8kc5g20e8nbc8r6lj
로컬 이미지 확인하기
로컬 머신에 있는 이미지 목록을 보려면 두 가지 옵션이 있어요. 하나는 Docker CLI를 사용하는 것이고 다른 하나는 Docker Desktop을 사용하는 거예요. 이미 터미널에서 작업 중이므로 CLI로 이미지를 나열하는 것을 살펴보죠.
이미지를 나열하려면 docker images 명령을 실행해요.
$ docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
docker-rust-image-dhi:latest 99a1b925a8d6 11.6MB 2.45MB U
적어도 하나의 이미지가 나열된 것을 볼 수 있어요. 방금 빌드한 docker-rust-image-dhi:latest를 포함해서요.
이미지 태그 지정하기
앞서 언급했듯이 이미지 이름은 슬래시로 구분된 이름 구성 요소로 이루어져요. 이름 구성 요소는 소문자, 숫자, 구분자를 포함할 수 있어요. 구분자는 마침표, 밑줄 하나 또는 두 개, 하나 이상의 대시를 포함할 수 있어요. 이름 구성 요소는 구분자로 시작하거나 끝날 수 없어요.
이미지는 매니페스트와 레이어 목록으로 이루어져요. 지금은 매니페스트와 레이어에 대해 너무 걱정하지 말고, "태그"가 이 아티팩트들의 조합을 가리킨다는 것만 알면 돼요. 이미지에는 여러 태그를 가질 수 있어요. 빌드한 이미지에 두 번째 태그를 만들고 그 레이어를 살펴봐요.
빌드한 이미지에 새 태그를 만들려면 다음 명령을 실행해요.
$ docker tag docker-rust-image-dhi:latest docker-rust-image-dhi:v1.0.0
docker tag 명령은 이미지에 새 태그를 만들어요. 새 이미지를 만들지는 않아요. 태그는 같은 이미지를 가리키며 이미지를 참조하는 또 다른 방법일 뿐이에요.
이제 docker images 명령을 실행해 로컬 이미지 목록을 확인해요.
$ docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
docker-rust-image-dhi:latest 99a1b925a8d6 11.6MB 2.45MB U
docker-rust-image-dhi:v1.0.0 99a1b925a8d6 11.6MB 2.45MB U
docker-rust-image-dhi로 시작하는 이미지가 두 개인 것을 볼 수 있어요. IMAGE ID 열을 보면 두 이미지의 값이 같다는 것을 확인할 수 있으니 같은 이미지라는 걸 알 수 있어요.
방금 만든 태그를 제거해요. 이렇게 하려면 rmi 명령을 사용해요. rmi 명령은 remove image의 약자예요.
$ docker rmi docker-rust-image-dhi:v1.0.0
Untagged: docker-rust-image-dhi:v1.0.0
Docker의 응답이 Docker가 이미지를 제거하지 않고 "untagged"만 했을 뿐이라는 것을 알려준다는 점에 주목하세요. docker images 명령을 실행해 이를 확인할 수 있어요.
$ docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
docker-rust-image-dhi:latest 99a1b925a8d6 11.6MB 2.45MB U
Docker는 :v1.0.0으로 태그된 이미지를 제거했지만, docker-rust-image-dhi:latest 태그는 머신에 남아 있어요.
Rust 이미지를 컨테이너로 실행하기
준비 사항 (Prerequisite)
Rust 이미지 빌드하기를 완료하고 이미지를 빌드했어야 해요.
개요 (Overview)
컨테이너는 일반 운영체제 프로세스이지만, Docker가 이 프로세스를 격리해서 자체 파일시스템, 자체 네트워킹, 호스트와 분리된 자체 격리 프로세스 트리를 갖게 해요.
이미지를 컨테이너 안에서 실행하려면 docker run 명령을 사용해요. docker run 명령은 이미지의 이름이라는 하나의 파라미터를 요구해요.
이미지 실행하기
Rust 이미지 빌드하기에서 빌드한 이미지를 실행하려면 docker run을 사용해요.
$ docker run docker-rust-image-dhi
이 명령을 실행한 후 명령 프롬프트로 돌아오지 않았다는 것을 알아차릴 거예요. 어플리케이션은 들어오는 요청을 기다리며 루프에서 실행되는 서버이고, 컨테이너를 멈출 때까지 OS에 제어권을 돌려주지 않기 때문이에요.
새 터미널을 열고 curl 명령으로 서버에 요청을 보내요.
$ curl http://localhost:8000
다음과 같은 출력을 볼 수 있어요.
curl: (7) Failed to connect to localhost port 8000 after 2236 ms: Couldn't connect to server
보시다시피 curl 명령이 실패했어요. 8000 포트의 localhost에 연결하지 못했다는 뜻이에요. 이것은 컨테이너가 네트워킹을 포함한 격리 상태로 실행되기 때문에 정상이에요. 컨테이너를 중지하고 로컬 네트워크에 8000 포트를 게시해 다시 시작해요.
컨테이너를 중지하려면 ctrl-c를 눌러요. 그러면 터미널 프롬프트로 돌아올 거예요.
컨테이너에 포트를 게시하려면 docker run 명령에서 --publish 플래그(-p로 줄임)를 사용해요. --publish 명령의 형식은 [호스트 포트]:[컨테이너 포트]이에요. 즉, 컨테이너 안의 8000 포트를 컨테이너 밖의 3001 포트에 노출하려면 --publish 플래그에 3001:8000을 전달해요.
컨테이너에서 어플리케이션을 실행할 때 포트를 지정하지 않았고 기본값은 8000이에요. 이전의 8000 포트로 가는 요청이 동작하게 하려면 호스트의 3001 포트를 컨테이너의 8000 포트에 매핑할 수 있어요:
$ docker run --publish 3001:8000 docker-rust-image-dhi
이제 curl 명령을 다시 실행해요. 새 터미널을 여는 것을 기억하세요.
$ curl http://localhost:3001
다음과 같은 출력을 볼 수 있어요.
Hello, Docker!
성공! 컨테이너 안 8000 포트에서 실행 중인 어플리케이션에 연결할 수 있었어요. 컨테이너가 실행 중인 터미널로 돌아가 중지해요.
ctrl-c를 눌러 컨테이너를 중지해요.
detached 모드로 실행하기
지금까지 좋지만, 샘플 어플리케이션은 웹 서버이고 컨테이너에 연결되어 있을 필요가 없어요. Docker는 컨테이너를 detached 모드 또는 백그라운드로 실행할 수 있어요. 이렇게 하려면 --detach 또는 짧게 -d를 사용해요. Docker는 이전과 같은 방식으로 컨테이너를 시작하지만 이번에는 컨테이너에서 "탈착(detach)"해서 터미널 프롬프트로 돌려보내요.
$ docker run -d -p 3001:8000 docker-rust-image-dhi
3e4830e7f01304811d97dd3469d47a0c7a916a8b6c28ce0ef19c6f689a521144
Docker는 백그라운드에서 컨테이너를 시작하고 터미널에 Container ID를 출력했어요.
컨테이너가 제대로 실행 중인지 다시 확인해요. curl 명령을 다시 실행해요.
$ curl http://localhost:3001
다음과 같은 출력을 볼 수 있어요.
Hello, Docker!
컨테이너 나열하기
컨테이너를 백그라운드로 실행했으니, 컨테이너가 실행 중인지 또는 머신에서 어떤 다른 컨테이너가 실행 중인지 어떻게 알 수 있을까요? 머신에서 실행 중인 컨테이너 목록을 보려면 docker ps를 실행해요. 이것은 Linux에서 ps 명령으로 프로세스 목록을 보는 것과 유사해요.
다음과 같은 출력을 볼 수 있어요.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3e4830e7f013 docker-rust-image-dhi "/server" 23 seconds ago Up 22 seconds 0.0.0.0:3001->8000/tcp, [::]:3001->8000/tcp youthful_lamport
docker ps 명령은 실행 중인 컨테이너에 대한 많은 정보를 제공해요. 컨테이너 ID, 컨테이너 안에서 실행되는 이미지, 컨테이너를 시작하는 데 사용된 명령, 생성 시점, 상태, 노출된 포트, 컨테이너 이름을 볼 수 있어요.
컨테이너 이름이 어디서 왔는지 궁금할 거예요. 컨테이너를 시작할 때 이름을 제공하지 않았으므로 Docker가 무작위 이름을 생성했어요. 잠시 후에 이걸 고치겠지만, 먼저 컨테이너를 중지해야 해요. 컨테이너를 중지하려면 docker stop 명령을 실행하면 돼요 — 컨테이너를 중지하는 명령이에요. 컨테이너의 이름을 전달해야 하거나 컨테이너 ID를 사용할 수 있어요.
$ docker stop youthful_lamport
youthful_lamport
이제 docker ps 명령을 다시 실행해 실행 중인 컨테이너 목록을 확인해요.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
컨테이너 중지, 시작, 이름 붙이기
Docker 컨테이너를 시작, 중지, 재시작할 수 있어요. 컨테이너를 중지하면 제거되지 않고 상태가 stopped로 바뀌며 컨테이너 안의 프로세스가 중지돼요. 이전 모듈에서 docker ps 명령을 실행했을 때 기본 출력은 실행 중인 컨테이너만 보여줬어요. --all 또는 짧게 -a를 전달하면 시작 또는 중지 상태와 관계없이 머신의 모든 컨테이너를 볼 수 있어요.
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3e4830e7f013 docker-rust-image-dhi "/server" About a minute ago Exited (0) 28 seconds ago youthful_lamport
60009b7eaf40 docker-rust-image-dhi "/server" 2 minutes ago Exited (0) About a minute ago sharp_noyce
152e1d7d9eea docker-rust-image-dhi "/server ." 4 minutes ago Exited (0) 2 minutes ago magical_bhabha
이제 여러 컨테이너가 나열된 것을 볼 수 있을 거예요. 이것들은 시작하고 중지했지만 제거하지 않은 컨테이너들이에요.
방금 중지한 컨테이너를 재시작해요. 방금 중지한 컨테이너의 이름을 찾아 다음 재시작 명령의 컨테이너 이름을 바꿔주세요.
$ docker restart youthful_lamport
이제 docker ps --all 명령을 사용해 모든 컨테이너를 다시 나열해요.
$ docker ps --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3e4830e7f013 docker-rust-image-dhi "/server" 3 minutes ago Up 7 seconds 0.0.0.0:3001->8000/tcp, [::]:3001->8000/tcp youthful_lamport
60009b7eaf40 docker-rust-image-dhi "/server" 4 minutes ago Exited (0) 3 minutes ago sharp_noyce
152e1d7d9eea docker-rust-image-dhi "/server ." 5 minutes ago Exited (0) 4 minutes ago magical_bhabha
방금 재시작한 컨테이너가 detached 모드로 시작됐다는 점에 주목하세요. 컨테이너의 상태가 "Up X seconds"인 것도 관찰해보세요. 컨테이너를 재시작하면 원래 시작됐던 것과 같은 플래그나 명령으로 시작돼요.
이제 모든 컨테이너를 중지·제거하고 무작위 이름 문제를 해결해보죠. 방금 시작한 컨테이너를 중지해요. 실행 중인 컨테이너의 이름을 찾아 다음 명령의 이름을 시스템의 컨테이너 이름으로 바꿔주세요.
$ docker stop youthful_lamport
youthful_lamport
이제 모든 컨테이너를 중지했으니 제거해요. 컨테이너를 제거하면 더 이상 실행 중이지도 않고 stopped 상태도 아니지만, 컨테이너 안의 프로세스는 중지됐고 컨테이너의 메타데이터는 제거됐어요.
컨테이너를 제거하려면 컨테이너 이름과 함께 docker rm 명령을 실행해요. 단일 명령으로 여러 컨테이너 이름을 전달할 수 있어요. 역시 다음 명령의 컨테이너 이름을 시스템의 컨테이너 이름으로 바꿔주세요.
$ docker rm youthful_lamport friendly_montalcini tender_bose
youthful_lamport
sharp_noyce
magical_bhabha
docker ps --all 명령을 다시 실행해 Docker가 모든 컨테이너를 제거했는지 확인해요.
이제 무작위 이름 문제를 해결할 시간이에요. 표준 관행은 컨테이너에 이름을 붙이는 것이에요. 컨테이너 안에서 무엇이 실행 중인지, 어느 어플리케이션이나 서비스와 연관되어 있는지 더 쉽게 식별할 수 있기 때문이에요.
컨테이너에 이름을 붙이려면 docker run 명령에 --name 플래그를 전달해요.
$ docker run -d -p 3001:8000 --name docker-rust-container docker-rust-image-dhi
1aa5d46418a68705c81782a58456a4ccdb56a309cb5e6bd399478d01eaa5cdda
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
219b2e3c7c38 docker-rust-image-dhi "/server" 6 seconds ago Up 5 seconds 0.0.0.0:3001->8000/tcp, [::]:3001->8000/tcp docker-rust-container
이제 이름으로 컨테이너를 식별할 수 있어요.
Rust 어플리케이션 개발하기
준비 사항 (Prerequisites)
- 최신 버전의 Docker Desktop을 설치했어야 해요.
- Docker 개념을 배우기 위해 Docker Desktop Learning Center의 워크스루를 완료했어야 해요.
- git 클라이언트가 필요해요. 이 섹션의 예제는 명령줄 기반의 git 클라이언트를 사용하지만, 어떤 클라이언트를 써도 무방해요.
개요 (Overview)
이 섹션에서는 Docker에서 볼륨과 네트워킹을 사용하는 방법을 배워요. 그리고 Docker로 이미지를 빌드하고, Docker Compose로 모든 것을 훨씬 쉽게 만들 거예요.
먼저 컨테이너에서 데이터베이스를 실행하는 방법과, 볼륨과 네트워킹을 사용해 데이터를 유지하고 어플리케이션과 데이터베이스가 통신하게 하는 방법을 살펴봐요. 그다음 모든 것을 하나의 명령으로 로컬 개발 환경을 설정·실행할 수 있는 Compose 파일로 묶을 거예요.
컨테이너에서 데이터베이스 실행하기
PostgreSQL을 다운로드·설치·구성한 다음 서비스로 실행하는 대신, PostgreSQL용 Docker Official Image를 사용해 컨테이너에서 실행할 수 있어요.
컨테이너에서 PostgreSQL을 실행하기 전에, Docker가 관리해서 영구 데이터와 구성을 저장할 볼륨을 만들어요. 바인드 마운트 대신 Docker가 제공하는 명명된 볼륨 기능을 사용해요.
볼륨을 만들려면 다음 명령을 실행해요.
$ docker volume create db-data
이제 어플리케이션과 데이터베이스가 서로 통신하는 데 사용할 네트워크를 만들어요. 이 네트워크는 사용자 정의 브리지 네트워크라고 하며, 연결 문자열을 만들 때 사용할 수 있는 멋진 DNS 조회 서비스를 제공해요.
$ docker network create postgresnet
이제 PostgreSQL을 컨테이너에서 실행하고 앞서 만든 볼륨과 네트워크에 연결할 수 있어요. Docker는 Hub에서 이미지를 pull해서 로컬에서 실행해요. 다음 명령에서 --mount 옵션은 볼륨으로 컨테이너를 시작하기 위한 거예요. 자세한 내용은 Docker volumes를 참고해요.
$ docker run --rm -d --mount \
"type=volume,src=db-data,target=/var/lib/postgresql" \
-p 5432:5432 \
--network postgresnet \
--name db \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=example \
postgres:18
이제 PostgreSQL 데이터베이스가 실행 중이고 연결할 수 있는지 확인해요. 컨테이너 안에서 실행 중인 PostgreSQL 데이터베이스에 연결해요.
$ docker exec -it db psql -U postgres
다음과 같은 출력을 볼 수 있어요.
psql (15.3 (Debian 15.3-1.pgdg110+1))
Type "help" for help.
postgres=#
이전 명령에서 psql 명령을 db 컨테이너에 전달해 PostgreSQL 데이터베이스에 로그인했어요. PostgreSQL 인터랙티브 터미널을 종료하려면 ctrl-d를 눌러요.
샘플 어플리케이션 가져와 실행하기
샘플 어플리케이션에는 Awesome Compose의 react-rust-postgres 어플리케이션의 백엔드 변형을 사용할 거예요.
- 다음 명령을 사용해 샘플 어플리케이션 저장소를 클론해요.
$ git clone https://github.com/docker/docker-rust-postgres
- 클론한 저장소 디렉토리에서
Dockerfile을 만들어요. 이 어플리케이션에는 데이터베이스를 초기화하는migrations디렉토리(src외에)가 포함되어 있으므로, Dockerfile은 빌드 스테이지에 그 디렉토리용 바인드 마운트를 포함해요.
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/reference/dockerfile/
################################################################################
# Create a stage for building the application.
ARG RUST_VERSION=1.70.0
ARG APP_NAME=react-rust-postgres
FROM rust:${RUST_VERSION}-slim-bookworm AS build
ARG APP_NAME
WORKDIR /app
# Build the application.
# Leverage a cache mount to /var/cache/cargo for downloaded dependencies
# and a cache mount to /app/target/ for compiled dependencies which will
# speed up subsequent builds.
# Leverage a bind mount to the src directory to avoid having to copy the
# source code into the container. Once built, copy the executable to an
# output directory before the cache mounted /app/target is unmounted.
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/var/cache/cargo \
--mount=type=bind,source=migrations,target=migrations \
<<EOF
set -e
CARGO_HOME=/var/cache/cargo cargo build --locked --release
cp ./target/release/$APP_NAME /bin/server
EOF
################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base
# image from the build stage where the necessary files are copied from the build
# stage.
#
# The example below uses the Debian Bookworm image as the foundation for running the app.
# By specifying the "bookworm-slim" tag, it will also use whatever happens to be the
# most recent version of that tag when you build your Dockerfile. If
# reproducibility is important, consider using a digest
# (e.g., debian@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171).
FROM debian:bookworm-slim AS final
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser
# Copy the executable from the "build" stage.
COPY --from=build /bin/server /bin/
# Expose the port that the application listens on.
EXPOSE 8000
# What the container should run when it is started.
CMD ["/bin/server"]
- 클론한 저장소 디렉토리에서
docker build를 실행해 이미지를 빌드해요.
$ docker build -t rust-backend-image .
- 다음 옵션들로
docker run을 실행해 이미지를 데이터베이스와 같은 네트워크의 컨테이너로 실행해요.
$ docker run \
--rm -d \
--network postgresnet \
--name docker-develop-rust-container \
-p 3001:8000 \
-e PG_DBNAME=example \
-e PG_HOST=db \
-e PG_USER=postgres \
-e PG_PASSWORD=mysecretpassword \
-e ADDRESS=0.0.0.0:8000 \
-e RUST_LOG=debug \
rust-backend-image
- 어플리케이션이 데이터베이스에 연결되는지 확인하려면 curl로 호출해요.
$ curl http://localhost:3001/users
다음과 같은 응답을 받게 돼요.
[{ "id": 1, "login": "root" }]
Compose로 로컬 개발하기
클론한 저장소 디렉토리에서 compose.yaml 파일을 만들어요. Compose를 사용하면 docker run 명령에 전달할 모든 파라미터를 입력할 필요 없이 파일에 선언할 수 있어요.
compose.yaml 파일에서 다음 항목을 업데이트해야 해요:
- 모든 데이터베이스 지시사항을 주석 해제해요.
- server 서비스 아래에 환경 변수를 추가해요.
다음은 업데이트된 compose.yaml 파일이에요.
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker compose reference guide at
# https://docs.docker.com/reference/compose-file/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
target: final
ports:
- 8000:8000
environment:
- PG_DBNAME=example
- PG_HOST=db
- PG_USER=postgres
- PG_PASSWORD=mysecretpassword
- ADDRESS=0.0.0.0:8000
- RUST_LOG=debug
# The commented out section below is an example of how to define a PostgreSQL
# database that your application can use. `depends_on` tells Docker Compose to
# start the database before your application. The `db-data` volume persists the
# database data between container restarts. The `db-password` secret is used
# to set the database password. You must create `db/password.txt` and add
# a password of your choosing to it before running `docker compose up`.
depends_on:
db:
condition: service_healthy
db:
image: postgres:18
restart: always
user: postgres
secrets:
- db-password
volumes:
- db-data:/var/lib/postgresql
environment:
- POSTGRES_DB=example
- POSTGRES_PASSWORD_FILE=/run/secrets/db-password
expose:
- 5432
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:
secrets:
db-password:
file: db/password.txt
파일이 이 두 서비스에 대해 네트워크를 지정하지 않는다는 점에 주목하세요. Compose를 사용하면 네트워크를 자동으로 만들어서 서비스들을 연결해요. 자세한 내용은 Networking in Compose를 참고해요.
Compose로 어플리케이션을 실행하기 전에, 이 Compose 파일이 데이터베이스의 비밀번호를 담을 password.txt 파일을 지정한다는 점에 주의해요. 이 파일은 소스 저장소에 포함돼 있지 않으므로 직접 만들어야 해요.
클론한 저장소 디렉토리에서 db라는 새 디렉토리를 만들고, 그 안에 데이터베이스 비밀번호를 담은 password.txt 파일을 만들어요. 좋아하는 IDE나 텍스트 편집기를 사용해 password.txt 파일에 다음 내용을 추가해요.
mysecretpassword
이전 섹션들에서 실행 중인 다른 컨테이너가 있다면 중지해요.
이제 다음 docker compose up 명령을 실행해 어플리케이션을 시작해요.
$ docker compose up --build
이 명령은 --build 플래그를 전달하므로 Docker가 이미지를 컴파일한 다음 컨테이너를 시작해요.
이제 API 엔드포인트를 테스트해요. 새 터미널을 열고 curl 명령으로 서버에 요청을 보내요:
$ curl http://localhost:8000/users
다음과 같은 응답을 받게 돼요:
[{ "id": 1, "login": "root" }]
요약 (Summary)
이 섹션에서는 Compose 파일을 구성해 단일 명령으로 Rust 어플리케이션과 데이터베이스를 실행하는 방법을 살펴봤어요.
관련 정보: