🐳

【DevOps実践】RustでAPIサーバーを構築してみた②: Docker構築編

に公開

前置き

【DevOps実践】RustでAPIサーバーを構築してみた①: 全体紹介編で全体像を紹介してます。
なにやってるかが分からない方は読んでみてください。

Docker構築

アプリケーション開発で使用するDockerを構築していきます。
マルチステージビルドでbase, dev, builder, runtimeに分けていきます。
以下の内容は全部同じDockerfileで記述します。

Base: 全ステージ共通項目

# 使用するRustのバージョン
ARG RUST_VERSION=1.88.0
FROM rust:${RUST_VERSION}-slim-bookworm AS base
# Rustのコンパイルで使用するパッケージをインストール
RUN apt-get update && apt-get install -y \
    pkg-config libssl-dev build-essential \
    && rm -rf /var/lib/apt/lists/*

Dev: 開発環境

ユーザー作成、ワークディレクトリ、必要なパッケージは自由にやってください。
自分はDevContainerにする予定なので、ホストPC側と権限合わせたり、ユーザー作成してます。

FROM base AS dev

# ユーザー情報(必要に応じて調整)
ARG USER_NAME=rust
ARG USER_UID=1000
ARG USER_GID=1000

# 開発時に必要なパッケージをインストール
RUN apt-get update && apt-get install -y sudo curl && rm -rf /var/lib/apt/lists/*

# ユーザー作成、sudoをパスワードなしで実行できる設定
RUN groupadd --gid ${USER_GID} ${USER_NAME} \
    && useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USER_NAME} \
    && echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

RUN rustup component add rustfmt clippy

USER ${USER_NAME}
WORKDIR /home/${USER_NAME}/app

# SQLx CLIのインストール、UVインストール
RUN cargo install sqlx-cli --no-default-features --features postgres \
    && cargo install --git https://github.com/astral-sh/uv uv

Builder: 本番環境イメージビルド用

今回はSQLxを使っているため、ビルド時にDBに接続してSQL実行可能かのチェックが入ります。
そのために毎回Docker作るのが面倒なので、SQLX_OFFLINEで実行します。
SQLX_OFFLINEで実行できるよう、開発環境で以下のコマンドを実行し、型ファイルを生成します。

cargo sqlx prepare --workspace -- --bin app

というのも、ローカルでDockerImageをビルドする場合の話ですが、今回はGithubActionsで自動化してきます。

FROM base AS builder
# 開発環境みたいにユーザー作成してないので、
# ルートディレクトリ直下でディレクトリを指定
WORKDIR /app

# 依存解決キャッシュ
COPY Cargo.toml Cargo.lock ./
COPY extensions ./extensions

# 依存解決キャッシュ用のダミーファイルを配置
RUN mkdir -p src \
    && echo "pub fn dummy() {}" > src/lib.rs \
    && echo "fn main() { println!(\"dummy\"); }" -> src/main.rs \
    && cargo build --release

# アプリケーション全体をコピー
COPY . .

ENV SQLX_OFFLINE=true
RUN cargo build --release --bin app --features sqlx-release

Runtime: 本番用イメージ

このイメージがCloudRunで実行する本体です。
distrolessがよくわからない方むけにちょっと説明:
  Google が提供する Docker コンテナのベースイメージ
  不要なコンポーネントを削除した最小限のイメージであり、セキュリティ上のメリットがあります
ArtifactRegistryはリポジトリ容量が0.5GB越えなければ無料で使用できるため、
最小限にビルドできるように努力しましょう。

FROM gcr.io/distroless/cc-debian12 AS runtime

WORKDIR /app
# app続きでちょっと紛らわしいですが、解説すると
# /app/target/release/app:
#   先頭の/appはディレクトリ名
#   最後のappはアプリケーションをビルドした成果物名
# /app/app: builderステージでビルドした成果物をそのまま持ってきた
copy --from=builder /app/target/release/app /app/app

# CloudRunで実行するには、アプリケーションが8080番でリッスンしている必要がある
ENV PORT=8080
ENV HOST=0.0.0.0

# ルートユーザなしで実行する
USER nonroot:nonroot

EXPOSE 8080
CMD ["./app"]

おまけ

Posrgresql

ARG POSTGRES_VERSION=17
FROM postgres:${POSTGRES_VERSION}

# これはある人だけ
COPY ./docker/postgres/init.sql /docker-entrypoint-initdb.d/
COPY ./docker/postgres/postgresql.conf /etc/postgresql/postgresql.conf

# Entrypointは保持して、CMDで構成ファイルを指定
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]

compose.yaml

services:
  app:
    container_name: actix-web-sqlx
    networks:
      - actix-web-net
    build:
      context: .
      dockerfile: ./docker/rust/Dockerfile
      target: dev

    tty: true
    ports:
      - 8000:8080
    depends_on:
      - db
      - test-db
      - redis
    volumes:
      - .:/home/rust/app

  db:
    container_name: actix-db
    networks:
      - actix-web-net
    build:
      context: .
      dockerfile: ./docker/postgres/Dockerfile
    restart: unless-stopped
    volumes:
      - ./docker/postgres/postgresql.conf:/etc/postgresql/postgresql.conf
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  test-db:
    container_name: actix-test-db
    networks:
      - actix-web-net
    build:
      context: .
      dockerfile: ./docker/postgres/Dockerfile

    restart: unless-stopped
    volumes:
      - ./docker/postgres/postgresql.conf:/etc/postgresql/postgresql.conf
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  redis:
    container_name: actix-redis
    image: redis:latest
    networks:
      - actix-web-net

networks:
  actix-web-net:
volumes:
  actix-postgres:
  actix-postgres-test:

DevContainer

# compose.yaml
services:
  app:
    volumes:
      - ..:/workspaces:cached
    command: sleep infinity

DevContainerは開発効率優先でRustAnalyzerの機能を一部オフにしてます。

# devcontainer.json
{
	"name": "actix-web-app",
	"dockerComposeFile": [
		"../compose.yaml",
		"compose.yaml"
	],
	"service": "app",
	"features": {
		"ghcr.io/nils-geistmann/devcontainers-features/zsh:0": {},
		"ghcr.io/devcontainers/features/git:1": {}
	},
	"postCreateCommand": "bash -c 'git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-\"$HOME/.oh-my-zsh/custom\"}/plugins/zsh-autosuggestions && git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-\"$HOME/.oh-my-zsh/custom\"}/plugins/zsh-syntax-highlighting && git clone https://github.com/zsh-users/zsh-completions ${ZSH_CUSTOM:-\"$HOME/.oh-my-zsh/custom\"}/plugins/zsh-completions && sed -i \"s/plugins=(git)/plugins=(git zsh-autosuggestions zsh-completions zsh-syntax-highlighting)/\" ~/.zshrc'",

	"customizations": {
		"vscode": {
			"settings": {
				"editor.tabSize": 4,
				"terminal.integrated.defaultProfile.osx": "zsh",
				"workbench.colorTheme": "Monokai Vibrant (Rust)",
				"[rust]": {
					"editor.defaultFormatter": "rust-lang.rust-analyzer",
					"editor.formatOnSave": true
				},
				"rust-analyzer.check.command": "clippy",
				"rust-analyzer.highlighting.strings": false,
				"rust-analyzer.checkOnSave.enable": false,
				"rust-analyzer.procMacro.enable": true,
				"rust-analyzer.cargo.runBuildScripts": false,
				"rust-analyzer.diagnostics.enable": true
			},
			"extensions": [
				"DioxusLabs.monokai-vibrant-rust",
				"rust-lang.rust-analyzer",
				"tamasfe.even-better-toml",
				"streetsidesoftware.code-spell-checker",
				"fill-labs.dependi",
				"qufiwefefwoyn.inline-sql-syntax"
			]
		}
	}
}

次回

【DevOps実践】RustでAPIサーバーを構築してみた③: クラウド構築編

Discussion