参考

https://blog.bedrock.day/a4d5bc7ab63d6c1ae90e

前提

Docker Desktop for Mac を利用しています。 Linux 環境の利用や他のコンテナランタイムを利用している場合は他の結果になると思います。

Jenkins Controller を docker-compose で立てる

https://www.jenkins.io/doc/book/installing/docker/ https://hub.docker.com/r/jenkins/jenkins

https://github.com/ganyariya/jenkins-by-docker-compose/commit/8f63f0c93ce7d1ba0bc30e0e2948c7c21703c31e

jenkins controller 用の Dockerfile を用意します。 もしなにもカスタマイズしないのであれば、そのまま jenkins/jenkins イメージを使うだけで良いです。 ただし、今回はカスタムプラグインを前もってインストールしておきたいため、下記のように Dockerfile をビルドします。

# ref: https://www.jenkins.io/doc/book/installing/docker/#on-macos-and-linux
FROM jenkins/jenkins:lts-jdk17
 
USER root
RUN apt-get update && apt-get install -y lsb-release ca-certificates curl && \
    install -m 0755 -d /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
    chmod a+r /etc/apt/keyrings/docker.asc && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
    https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
    | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    apt-get update && apt-get install -y docker-ce-cli && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
 
USER jenkins
COPY jenkins_plugins.txt /usr/share/jenkins/ref/jenkins_plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/jenkins_plugins.txt

compose から jenkins を起動します。 context をなにも設定しないと、 compose.yml が存在するディレクトリがコンテキストディレクトリになります。 今回は ./jenkins-controller をコンテキストディレクトリとしています。 こうすることで Dockerfile 内の COPY jenkins_plugins.txt 命令において、 ./jenkins-controller から見た相対ディレクトリ = ./jenkins-controller/jenkins_plugins.txt を正しく取ってくれます。

services:
  jenkins-controller:
    build:
      # https://docs.docker.com/reference/compose-file/build/#context
      context: ./jenkins-controller
      dockerfile: Dockerfile # [context = ./jenkins-controller]/Dockerfile
    image: jenkins-controller-image
    container_name: jenkins-controller-container
    ports:
      # 8081 = your pc port, 8080 = jenkins-controller port in container
      - 8081:8080
      - 50000:50000
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
    # run container as `jenkins` user
    user: jenkins

Docker コマンドを実行できるようにする

https://docs.cloudbees.com/docs/cloudbees-ci/latest/pipelines/docker-workflow https://www.jenkins.io/doc/book/pipeline/docker/

docker pipeline plugin を利用することで

  • agent として docker を利用する
  • docker image の build push が行える

ことができます。 上記プラグインを動かすには docker がインストールされている必要があります。

しかし、現時点の jenkins-controller はただの jenkins/jenkins image であり、 docker はインストールされていません。

よって、 jenkins-controller に docker を扱えるようにする必要があります。

dood vs dind

https://qiita.com/t_katsumura/items/d5be6afadd6ec6545a9d

dooddind が存在します。

  • dood
    • ホストマシンの docker daemon を利用する
  • dind
    • ホストマシンに立てたコンテナ A においてさらに Docker をインストールし、 A の Docker Daemon を利用することで

という違いです。

dind で docker pipeline を動かす

https://github.com/ganyariya/jenkins-by-docker-compose/pull/1

docker 公式から提供されている dind image を利用します。 dind image にはすでに docker がインストールされており、かつ DOCKER_TLS_CERTDIR で指定したパスへ証明書を生成を生成するという特徴があります。

あとは jenkins-controller 側で DOCKER_HOST と DOCKER_CERT_PATH を指定します。 こうするとこで、 jenkins-controller 内の jenkins が docker pipeline を実行したとき

  • docker daemon への接続として TCP ソケット通信を利用する
  • mTLS 通信を行う

ことができ、 docker を agent として利用できます。

services:
  jenkins-controller:
    build:
      # https://docs.docker.com/reference/compose-file/build/#context
      context: ./jenkins-controller
      dockerfile: Dockerfile # [context = ./jenkins-controller]/Dockerfile
    image: jenkins-controller-image
    container_name: jenkins-controller-container
    ports:
      # 8081 = your pc port, 8080 = jenkins-controller port in container
      - 8081:8080
      # 50000 = jenkins agent node connects to the controller on 50000 port
      - 50000:50000
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client:ro
    # run container as `jenkins` user
    user: jenkins
    environment:
      # https://docs.docker.jp/compose/reference/envvars.html#docker-host
      # when jenkins controller run docker command, connect to jenkins-docker -runner by `docker` network alias
      # use tls connection
      - DOCKER_HOST=tcp://docker:2376 # mTLS connection with port 2376 of `docker` service
      - DOCKER_CERT_PATH=/certs/client
      - DOCKER_TLS_VERIFY=1
    depends_on: 
      - docker
 
    networks:
      - jenkins-network
 
  # https://zenn.dev/taku_sid/articles/20250409_docker_patterns
  # https://qiita.com/t_katsumura/items/d5be6afadd6ec6545a9d
  # docker in docker
  docker:
    image: docker:dind
    container_name: jenkins-docker-container
    privileged: true # for run docker command in docker container
    expose:
      - "2376" # for TLS connection
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client
    environment:
      # https://hub.docker.com/_/docker#tls
      # dind container creates TLS certificates in DOCKEDR_TLS_CERTDIR
      - DOCKER_TLS_CERTDIR=/certs
    networks:
      - jenkins-network
 
networks:
  jenkins-network:
    driver: bridge
 
volumes:
  jenkins-docker-certs:

実際に job を動かしたところ下記のようになりました。 Imgur

pipeline {
    agent {
        docker { image 'node:24.11.0-alpine3.22' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --eval "console.log(process.platform,process.env.CI)"'
            }
        }
    }
}

dood で docker pipeline を動かす

/var/run/docker.sock:/var/run/docker.sock を volume へ追加することで、 jenkins-controller-container がホストマシンへの Docker Daemon へ unix-domain-socket 経由で通信できるようにします。

services:
  jenkins-controller:
    build:
      # https://docs.docker.com/reference/compose-file/build/#context
      context: ./jenkins-controller
      dockerfile: Dockerfile # [context = ./jenkins-controller]/Dockerfile
    image: jenkins-controller-image
    container_name: jenkins-controller-container
    ports:
      # 8081 = your pc port, 8080 = jenkins-controller port in container
      - 8081:8080
      # 50000 = jenkins agent node connects to the controller on 50000 port
      - 50000:50000
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      # When the jenkins controller attempts to execute a docker pipeline, use the docker daemon running on the host PC.
      - /var/run/docker.sock:/var/run/docker.sock
    user: jenkins
 
    networks:
      - jenkins-network
 
networks:
  jenkins-network:
    driver: bridge
 

しかし、 job を実行したところ Docker Daemon へアクセスできないというエラーがでました。 どうやら jenkins-controller-container の jenkins user が docker.socket ならびに docker コマンドが実行できないようです。

+ docker pull node:24.11.0-alpine3.22
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/images/create?fromImage=docker.io%2Flibrary%2Fnode&tag=24.11.0-alpine3.22": dial unix /var/run/docker.sock: connect: permission denied

そこで、下記記事と同じ対応を取りました。 jenkins user を root グループへ追加しています。 このようにすることで docker.socket へアクセスできるようになり、パイプラインが正常に実行できるようになりました。 https://qiita.com/shiojojo/items/e5ac5a7ca5cfd860c1e7

# ref: https://www.jenkins.io/doc/book/installing/docker/#on-macos-and-linux
FROM jenkins/jenkins:lts-jdk17
 
USER root
RUN apt-get update && apt-get install -y lsb-release ca-certificates curl && \
    install -m 0755 -d /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
    chmod a+r /etc/apt/keyrings/docker.asc && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
    https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
    | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    apt-get update && apt-get install -y docker-ce-cli && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
 
# add jenkins user to root group
# because jenkins want to access /var/run/docker.sock
RUN gpasswd -a jenkins root
 
USER jenkins
COPY jenkins_plugins.txt /usr/share/jenkins/ref/jenkins_plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/jenkins_plugins.txt
 
 

gpasswd -a jenkins docker で docker group に配置する方法もあるのですが

  • mac host pc の /var/run/docker.sock の gid 所有権限
  • container の /var/run/docker.sock の gid 所有権限 が異なり、エラーがでてしまいました。 そのため、 root グループに突っ込むという苦し紛れの対応をとっています。

https://github.com/ganyariya/jenkins-by-docker-compose/pull/2

dind パターンを採用する

https://github.com/ganyariya/jenkins-by-docker-compose/pull/3

Docker Desktop (mac) の場合、 jenkins user を root group へ入れないと dood を実現することが難しそうです。 また、ホストの Docker 領域を汚してしまうというのも辛いところです。 そのため、 dind パターンを採用します。

リファクタする

SSH 接続 agent を作成するまえに、下記のように dind をリファクタしました。 dind-runner で名前解決すれば、 service 名は docker 以外でもよいと考えたためです。 しかし error during connect: Get "[https://dind-runner:2376/v1.51/containers/node:24.11.0-alpine3.22/json](https://dind-runner:2376/v1.51/containers/node:24.11.0-alpine3.22/json)": tls: failed to verify certificate: x509: certificate is valid for 085732f6d3db, docker, localhost, not dind-runner といエラーが発生します。

    environment:
      # https://docs.docker.jp/compose/reference/envvars.html#docker-host
      # when jenkins controller executes docker command, it connects to dind-runner service's 2376 port with mTLS.
      - DOCKER_HOST=tcp://dind-runner:2376
      - DOCKER_CERT_PATH=/certs/client
      - DOCKER_TLS_VERIFY=1
    depends_on: 
      - dind-runner
 
    networks:
      - jenkins-network
 
  # https://qiita.com/t_katsumura/items/d5be6afadd6ec6545a9d
  # docker in docker
  dind-runner:
    image: docker:dind
    container_name: jenkins-docker-container
    privileged: true
    expose: 
      - "2376" # for mTLS connection
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client
    environment:
      # https://hub.docker.com/_/docker#tls
      # dind image creates TLS certificates in DOCKEDR_TLS_CERTDIR
      - DOCKER_TLS_CERTDIR=/certs
    networks:
      - jenkins-network

error during connect: Get "[https://dind-runner:2376/v1.51/containers/node:24.11.0-alpine3.22/json](https://dind-runner:2376/v1.51/containers/node:24.11.0-alpine3.22/json)": tls: failed to verify certificate: x509: certificate is valid for 085732f6d3db, docker, localhost, not dind-runner

どうやら dint image が作成する mTLS 用証明書は docker, localhost というホスト名しか許可していないようです。

よって、 network alias を利用して docker という名前でもアクセスできるようにします。

dind パターンの TLS 通信の仕組み

どうやら以下のような仕組みのようです 。

  1. dind runner で生成した /certs/client ディレクトリを volume を利用して、 jenkins-controller 側へ渡す
  2. jenkins-controller が docker へアクセスしたいとき、 tcp://docker:2376 へアクセスしようとする
  3. TLS 接続を開始する
    1. jenkins-controller は /certs/client の証明書を利用し、 dind-runner は /certs/server の証明書を利用する
    2. dind-runner の /certs/ca が上位認証局として /certs/server の正しさを証明する

大まかな理解ですが、 /certs/client だけ jenkins-controller へ渡し、 dind-runner 側が server として機能することには納得しました。

ssh 接続による agent を利用する

https://hub.docker.com/r/jenkins/ssh-agent

ssh-agent image を利用します。 #yaml-anchor を利用して ssh-agent とするための定義を用意しています。 環境変数として JENKINS_AGENT_SSH_PUBKEY を利用します。 この環境変数は ssh-agent image が利用するものであり、 JENKINS_AGENT_SSH_PUBKEY の内容を自動的に /home/jenkins/.sshautorized_keys へ転記します。

x-jenkins-ssh-definition: &jenkins-ssh-definition
  image: jenkins/ssh-agent:latest-jdk25
  container_name: jenkins-ssh-agent-container
  expose:
    - "22"
  environment:
    - JENKINS_AGENT_SSH_PUBKEY=${JENKINS_AGENT_SSH_PUBKEY}
  networks:
    - jenkins-network
      
      
  jenkins-ssh-agent1:
    *jenkins-ssh-definition
  jenkins-ssh-agent2:
    *jenkins-ssh-definition
 

下記のように Jenkins 上でマシンを設定することで、ビルドに利用できます。

Imgur

Imgur

 
pipeline {
    agent { label 'jenkins-ssh-agent1-label' }
 
    stages {
        stage('Checkout Source') {
            steps {
                git branch: 'main', url: 'https://github.com/ganyariya/ganyariya.git'
                sh 'ls -a'
            }
        }
        
        stage('docker build and test') {
            agent {
                docker { 
                    image 'node:lts-alpine'
                }
            }
            steps {
                sh 'echo "--- Inside Docker Container ---"'
                sh 'ls -a'
                sh 'node -v'
            }
        }
    }
}

新しい学びとして --user を指定することで該当の user で exec できます。

# root でログインできる
docker compose exec -it --user root jenkins-controller /bin/bash

dind パターンだと docker pipeline がうまくいかない

Groovy 上で docker workflow pipeline を利用する場合、 agent となるコンテナに docker がインストールされている必要があります。 そのため、x-jenkins-ssh-agent-definition の Dockerfile で docker をインストールするようにしました。

しかし、 dind コンテナ上でうまくジョブを実行するには dind と jenkins-ssh-agent1 でワークスペースを volume で共有する必要があります。 volume を共有しないと jenkins が job を実行できません。

下記のようにすると、 jenkins-ssh-agent1 を利用して docker pipeline が実行できるようになりました。 しかし、 jenkins-ssh-agent2 も docker-pipeline を実行できるようにさせようとすると、 jenkins-ssh-agent-volume2:/home/jenkins/agent を dind にも設定しないといけません。 すると、 /home/jenkins/agent がコンフリクトしてしまいます。

回避するには dind1, dind2 のように docker in docker を複数コンテナ用意しないといけません。 dind パターンを利用する場合、 dind を使うコンテナは1つに抑えたほうがよさそうです。 複数コンテナから docker を利用したい場合、 dood パターンにしたほうがよいです。

x-dind-accessor-environment: &dind-accessor-environment
  # https://docs.docker.jp/compose/reference/envvars.html#docker-host
  # when jenkins controller executes docker pipeline, it connects to `docker` 2376 port with mTLS.
  DOCKER_HOST: tcp://docker:2376
  DOCKER_CERT_PATH: /certs/client
  DOCKER_TLS_VERIFY: 1 
 
x-jenkins-ssh-agent-definition: &jenkins-ssh-agent-definition
  # https://hub.docker.com/r/jenkins/ssh-agent
  build:
    context: ./jenkins-ssh-agent
  image: jenkins/ssh-agent:latest-jdk25
  expose:
    - "22"
  environment:
    <<: *dind-accessor-environment
    # jenkins/ssh-agent container automatically registers JENKINS_AGENT_SSH_PUBKEY to `/home/jenkins/.ssh/authorized_keys` on startup
    JENKINS_AGENT_SSH_PUBKEY: ${JENKINS_AGENT_SSH_PUBKEY}
  depends_on: 
    - dind-runner
  networks:
    - jenkins-network
 
services:
  jenkins-controller:
    build:
      # https://docs.docker.com/reference/compose-file/build/#context
      context: ./jenkins-controller
    image: jenkins-controller-image
    container_name: jenkins-controller-container
    ports:
      - 8080:8080
      - 50000:50000 # 50000 = jenkins agent connects to controller on 50000 port
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client:ro
    user: jenkins
    environment:
      <<: *dind-accessor-environment
    depends_on: 
      - dind-runner
    networks:
      - jenkins-network
 
  # docker in docker pattern
  dind-runner:
    image: docker:dind
    container_name: jenkins-docker-container
    privileged: true
    expose: 
      - "2376"
    volumes:
      - ./jenkins-controller-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client
      - jenkins-ssh-agent-volume:/home/jenkins/agent
    environment:
      # https://hub.docker.com/_/docker#tls
      # dind image creates TLS certificates in DOCKER_TLS_CERTDIR
      - DOCKER_TLS_CERTDIR=/certs
    networks:
      jenkins-network:
        aliases:
          - docker # for DOCKER_HOST=tcp://docker:2376 name resolver
 
  jenkins-ssh-agent1:
    <<: *jenkins-ssh-agent-definition
    volumes:
      # ssh-agent1 上で docker pipeline を実行するには
      # - dind の docker.socket へ通信する
      # - dind と volume を /home/jenkins/agent で共有する
      # 必要がある
      # その結果 ssh-agent1 と ssh-agent2 両方で docker pipeline を実行しようとすると
      # dind 上の volume で jenkins-ssh-agent-volume{1,2}: /home/jenkins/agent とする必要があり、不可能となる
      # よって dind を利用して複数の ssh-agent コンテナで docker pipeline は実行できない
      - jenkins-ssh-agent-volume:/home/jenkins/agent
      - jenkins-docker-certs:/certs/client:ro
  jenkins-ssh-agent2:
    <<: *jenkins-ssh-agent-definition
    volumes:
      - jenkins-docker-certs:/certs/client:ro
 
networks:
  jenkins-network:
    driver: bridge
 
volumes:
  jenkins-docker-certs:
  jenkins-ssh-agent-volume:

dood pattern に変更する

https://blog.nijohando.jp/post/docker-in-docker-docker-outside-of-docker/ https://mitomito.hatenablog.jp/entry/2022/06/06/080000

dind pattern において「volume を共有できない」という問題が発生しました。 そのため dood pattern に変更します。 下記コミットのような変更をおこないました。

https://github.com/ganyariya/jenkins-by-docker-compose/pull/7/commits/10d258a4d1c02ef07ab93cdc029f883cd47bb7db

しかし、この場合 Jenkfins Job で Docker Pipeline を利用しようとするとエラーになってしまいます。 というのも Job を実行する linux の jenkins user は /var/run/docker.sock を読み書きできる権限を持っていないためです。 root user もしくは root group しか読み書きできない設定となっています。

 docker compose exec -it jenkins-ssh-agent1 /bin/bash
root@dff89a57a690:/home/jenkins# ls -la /var/run/docker.sock
srw-rw---- 1 root root 0 Nov 15 01:59 /var/run/docker.sock
root@dff89a57a690:/home/jenkins#
Running on [Jenkins](http://localhost:8080/computer/\(built-in\)/) in /var/jenkins_home/workspace/Test_Docker
[Pipeline] {[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] isUnix[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] withEnv[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] {[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] sh[](http://localhost:8080/job/Test_Docker/2/console#)
+ docker inspect -f . node:lts-alpine
 
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "[http://%2Fvar%2Frun%2Fdocker.sock/v1.51/containers/node:lts-alpine/json](http://%2Fvar%2Frun%2Fdocker.sock/v1.51/containers/node:lts-alpine/json)": dial unix /var/run/docker.sock: connect: permission denied
[Pipeline] isUnix[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] withEnv[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] {[](http://localhost:8080/job/Test_Docker/2/console#)
[Pipeline] sh[](http://localhost:8080/job/Test_Docker/2/console#)
+ docker pull node:lts-alpine
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "[http://%2Fvar%2Frun%2Fdocker.sock/v1.51/images/create?fromImage=docker.io%2Flibrary%2Fnode&tag=lts-alpine](http://%2Fvar%2Frun%2Fdocker.sock/v1.51/images/create?fromImage=docker.io%2Flibrary%2Fnode&tag=lts-alpine)": dial unix /var/run/docker.sock: connect: permission denied

そのため、 dood pattern を実現する場合、 jenkins user を root group に追加するしかありません。 もしくは docker compose up をしたあとで /var/run/docker.sock の権限を変更し docker group 所有にする必要があります。

今回は楽をするために jenkins user を root group へ追加することにしました。

https://github.com/ganyariya/jenkins-by-docker-compose/pull/7

TCP 接続による agent を利用する

https://hub.docker.com/r/jenkins/inbound-agent