2026年7月25日
2026年7月25日
GitLab CI/CDでDockerイメージをビルド・デプロイする方法(Container Registry連携)
GitLab CI/CD で Docker イメージを扱う流れは、「ジョブ内で docker build する → GitLab Container Registry に push する → サーバー側で pull して入れ替える」の 3 段階です。この記事では、Docker-in-Docker(DinD)を使ったビルド、コミットごとのタグ付け、キャッシュの効かせ方、デプロイ先での docker compose による更新までを、セキュリティ上の注意と合わせて解説します。.gitlab-ci.yml の基本(ステージ・rules・変数)は別記事で扱っているため、本記事はイメージのビルドと配布に絞っています。
全体像と前提
前提として、次の環境を想定します。
- GitLab.com またはセルフマネージドの GitLab で、プロジェクトの Container Registry が有効になっている
- ジョブを実行する Runner が DinD を使える(Docker executor で特権モードが許可されている)
- デプロイ先は Docker と Docker Compose が入った Linux サーバー
GitLab.com が提供する Linux の Runner は DinD を使える構成になっていますが、自前の Runner では config.toml で privileged = true を設定しないと DinD は動きません。後述するように特権モードには相応のリスクがあるので、自前の Runner で有効にするかは慎重に判断してください。
Dockerfile を用意する
例として、PHP と Apache の公式イメージに小さな PHP アプリを載せる Dockerfile を使います。
FROM php:8.3-apache
RUN docker-php-ext-install mysqli pdo_mysql
COPY src/ /var/www/html/
RUN chown -R www-data:www-data /var/www/html
WordPress サイト全体をイメージにする場合は注意点があります。公式の wordpress イメージは /var/www/html をボリュームとして扱う設計で、初回起動時にコアファイルを展開します。そのため、テーマやプラグインをイメージに含めても、既存のボリュームが残っていると新しいイメージの内容が反映されないことがあります。WordPress で使う場合は、どのディレクトリをボリュームにし、どれをイメージで更新するかを先に設計し、ステージングで更新手順を確認してから本番に適用してください。
イメージを小さく保つには、ビルド用のツールを最終イメージに残さないマルチステージビルドが有効です。.dockerignore で .git や node_modules を除外しておくと、ビルドコンテキストの転送も速くなります。
DinD でビルドして Container Registry に push する
GitLab は、ジョブ内で使えるレジストリ用の定義済み変数を用意しています。
CI_REGISTRY: レジストリのホスト名CI_REGISTRY_IMAGE: このプロジェクトのイメージのパスCI_REGISTRY_USER/CI_REGISTRY_PASSWORD: ジョブの間だけ有効な認証情報CI_COMMIT_SHORT_SHA: コミットの短いハッシュ
これらを使うと、パスワードを自分で登録せずにログインできます。
stages:
- build
- deploy
build-image:
stage: build
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
script:
- docker pull "$CI_REGISTRY_IMAGE:latest" || true
- >
docker build
--cache-from "$CI_REGISTRY_IMAGE:latest"
--build-arg BUILDKIT_INLINE_CACHE=1
-t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
-t "$CI_REGISTRY_IMAGE:latest"
.
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
- docker push "$CI_REGISTRY_IMAGE:latest"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
設定の意図を順に説明します。
image と services の Docker のバージョンはそろえておきます。片方だけ古いと、API バージョンの不一致で接続に失敗することがあります。タグの番号は例なので、採用時点のサポート中のバージョンを Docker Hub で確認してください(2026年10月現在)。
DOCKER_TLS_CERTDIR: "/certs" は、ジョブのコンテナと DinD サービスの間を TLS で接続するための設定です。これを空にして TLS を無効化する古い例もありますが、特別な理由が無ければ TLS ありの構成を使います。
docker login は --password-stdin で標準入力からパスワードを渡します。コマンドライン引数に直接書くと、プロセス一覧やログに残るおそれがあるためです。
タグの付け方とキャッシュ
latest だけで運用すると、「本番でいま動いているのはどのコミットか」が分からず、問題が起きたときに一つ前に戻すのも難しくなります。上の例のように、コミットハッシュのタグを毎回付ける運用にしておくと、デプロイとロールバックを特定のバージョンで行えます。リリースをタグで管理しているなら、タグのパイプラインで CI_COMMIT_TAG をイメージのタグに使う方法もあります。ブランチ名を使う場合は、イメージタグに使えない文字を除いた CI_COMMIT_REF_SLUG を使います。
DinD のジョブは毎回まっさらな Docker デーモンで始まるため、そのままではレイヤーキャッシュが効きません。例では、前回の latest を pull して --cache-from に指定し、BUILDKIT_INLINE_CACHE=1 でキャッシュ情報をイメージに埋め込んでいます。依存関係のインストールなど変化の少ない処理を Dockerfile の前のほうに書くと、キャッシュが当たりやすくなります。より細かく制御したい場合は docker buildx のレジストリキャッシュ(--cache-to / --cache-from type=registry)も選択肢ですが、ビルダーの設定が別途必要になります。
DinD 以外の選択肢
DinD は特権モードのコンテナで Docker デーモンを動かすため、Runner のホストに対する影響範囲が大きくなります。Runner をホストの Docker ソケット(/var/run/docker.sock)にマウントする方法もありますが、これはジョブにホストの Docker を丸ごと操作する権限を渡すことと同じで、共有 Runner では特に避けるべき構成です。
特権モードを使わずにビルドしたい場合は、Buildah や rootless モードの BuildKit が選択肢になります。以前よく使われた Kaniko は、元のリポジトリが 2025 年にアーカイブされたと案内されているため、新しく採用する場合はメンテナンス状況を確認してから選んでください。どの方法でも、GitLab のドキュメントに Runner 側の要件がまとまっています。
デプロイ先で pull して入れ替える
サーバー側では、レジストリから pull するための認証が必要です。プロジェクトの「Settings」→「Repository」付近にある Deploy tokens で、read_registry スコープだけを持つトークンを作り、サーバーで一度ログインしておきます(メニューの位置はバージョンで変わります)。
# サーバー側で一度だけ実行(トークンは標準入力から渡す)
docker login registry.gitlab.com -u your-deploy-token-username --password-stdin
docker-compose.yml ではイメージのタグを環境変数で受け取れるようにしておきます。
services:
app:
image: registry.gitlab.com/your-group/your-project:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
デプロイジョブは SSH でサーバーに入り、タグを指定して pull と再作成を行います。SSH_PRIVATE_KEY は File 型、SSH_KNOWN_HOSTS には確認済みのホスト鍵を CI/CD 変数として登録しておきます。
deploy:
stage: deploy
image: alpine:3.20
needs: [build-image]
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
environment:
name: production
resource_group: production
before_script:
- apk add --no-cache openssh-client
- chmod 600 "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- >
ssh -i "$SSH_PRIVATE_KEY" "$DEPLOY_USER@$DEPLOY_HOST"
"cd /srv/app && IMAGE_TAG=$CI_COMMIT_SHORT_SHA docker compose pull && IMAGE_TAG=$CI_COMMIT_SHORT_SHA docker compose up -d"
ロールバックしたいときは、前のコミットハッシュを IMAGE_TAG に指定して同じコマンドを実行すれば、前のイメージに戻せます。データベースのスキーマ変更を伴うリリースはイメージを戻すだけでは元に戻らないため、事前にバックアップを取り、ステージングで手順を確認してください。
デプロイ用ユーザーを docker グループに入れると、実質的にサーバーの root 権限に近い操作ができるようになります。専用ユーザーにする、SSH 鍵に実行できるコマンドの制限をかけるなど、鍵が漏れたときの影響を抑える工夫を検討してください。
運用で気をつけること
- レジストリの容量: コミットごとにタグを作るとイメージが増え続けます。Container Registry のクリーンアップポリシーで、古いタグを自動削除する設定を検討してください。
- 脆弱性スキャン: GitLab には Container Scanning のテンプレートがあり、ビルドしたイメージの既知の脆弱性を検出できます。利用できる機能はプランによって異なります。
- ベースイメージの更新: アプリに変更が無くてもベースイメージには修正が入ります。定期的にパイプラインを動かして再ビルドする(スケジュール実行)と、古いイメージが残り続けるのを防げます。
まとめ
docker:<版>とdocker:<版>-dindをそろえ、DOCKER_TLS_CERTDIRで TLS ありの DinD を使います。- ログインは定義済み変数と
--password-stdinで行い、パスワードを YAML やログに残しません。 - イメージにはコミットハッシュのタグを付け、デプロイとロールバックをバージョン指定で行います。
- 特権モードや Docker ソケットのマウントはリスクを理解したうえで選び、必要に応じて Buildah などを検討します。
- サーバー側は
read_registryだけの Deploy token で pull し、docker composeでタグを指定して入れ替えます。