00 碎碎念

为什么使用 Docker 去搭建 Jenkins 呢?

主要原因是个人频繁切换环境,有时候在本地虚拟机玩,有时候会在云服务器上折腾,渐感被自己折磨过多了,因此希望能够创建一个配置文件,以便一键启动并快速方便地使用。另外,自己经常在其他地方装环境,公司的服务也比较老,一但要迁移,用 Docker 就很方便。所以,使用 Docker 是很适合的。

网上那么多一样的文章,为什么要写这篇呢?

虽然说这篇文章是作为笔记的,但是因为自己在使用网上的方法安装的时候,遇到几个问题,尝试了很多方法才成功,所以想要记录一下,以备不时之需。

01 Docker Compose 安装

以下是测试过的,能正常部署和使用 Jenkins 的 Docker Compose 配置,部分参数请按需调整。

version: '3.9'
services:
  jenkins:
    # 镜像选择了 jdk17 最新版,可自行选择
    image: jenkins/jenkins:jdk17
    container_name: jenkins
    # 重启策略:除非手动停止,否则出错会无限重启
    restart: unless-stopped
    # 指定用户 uid:gid(`用户id`:`宿主机的docker组id`,跟文件权限有关)
    user: 1000:995
    ports:
      # 8080 为 Jenkins 的 Web 端口
      - 9001:8080
      # 50000 为代理节点与主服务器的通信端口?
      - 50001:50000
    volumes:
      # 同步宿主机的时间
      - /etc/timezone:/etc/timezone
      - /etc/localtime:/etc/localtime
      # Jenkins 数据目录映射出来,方面操作和备份
      - .jenkins_home:/var/jenkins_home
      # 把宿主机的 docker 和 docker-compose 给 Jenkins 使用,这样可以直接在 Jenkins 内部打镜像,并直接操作容器
      - /usr/bin/docker:/usr/bin/docker
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/local/bin/docker-compose:/usr/local/bin/docker-compose

使用 Docker Compose 直接启动即可:

# -d:后台启动
$ docker-compose up -d

# 1. 查看日志,临时登录密码会放到文件中,也会直接打在日志上
#    临时密码位置:/var/jenkins_home/secrets/initialAdminPassword
# 2. 或使用 docker 命令查看: docker logs jenkins
$ docker-compose logs -f

Jenkins initial setup is required.An admin user has been created and a password generated.
Please use the following password to proceed to installation:

# 临时密码
57ea14a04f9464d895655348d6ad6a86

然后,访问 9001(这里已经映射为 9001 端口了):http://ip:9001,输入临时密码登录。

接着,选择安装插件,创建用户,等待完成即可。(这些步骤都可以跳过,后续再进行即可)

02 问题及解决办法

2.1 Docker 权限问题(Permission denied)

用网上的方法,折腾了好久……

把 Docker 相关文件映射到容器内部后,会遇到权限问题(如果直接指定 root 用户部署,不会有权限问题!),网上有许多把 jenkins 用户添加到 docker 组的方式,但这依然是基于宿主机的环境,未必能直接解决。

我比较爱折腾,环境稍微有差异,直接把 jenkins 用户添加到 docker 组依然有权限问题,接下来看看具体的情况和处理方法。

事情是这样子的~

首先,来看看 docker.sock 在 Jenkins 容器内部的情况:

jenkins@b0e6d3e8ce07:~$ ls -l /var/run/ | grep docker
srw-rw----. 1 root  998 0 Jun  3 01:59 docker.sock

docker.sock 是我们从宿主机映射到容器内的 sock 文件,它是 Docker 守护进程(Docker daemon)与 Docker 客户端之间进行通信的 UNIX 套接字文件(UNIX socket file)。

由于 docker 相关文件是属于宿主机的,原则上不直接修改它,而是通过修改容器来兼容。但是,这里的 sock 属于 root 用户,如果使用普通用户(默认 jenkins:1000)部署,并没有权限去读取。

998 是 docker 组的组 id(gid),网上找到比较好的方法是,把 jenkins 用户直接加入该组,就可以正常使用宿主机 docker 了。

而我的问题就出在这里。

先用网上的方法试试?

接着,来看下容器内部的情况:

jenkins@b0e6d3e8ce07:~$ id jenkins
uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

jenkins@b0e6d3e8ce07:~$ id
uid=1000(jenkins) gid=995 groups=995

# 下面是配置 `user: root` 部署的,容器内部使用 root 用户,可以正常使用 docker
root@8900354a95f1:/# id
uid=0(root) gid=0(root) groups=0(root)

第一个命令,id jenkins 表明 jenkins 用户的用户 id 和组 id 都为 1000;
第二个命令,id 表明当前登录会话的用户 id 为 1000,而组 id 和 groups 为 995(为什么不一样?因为 docker-compose 配置文件设置的,groups 就是重点)。

也就是说,当前登录会话的用户,既不是 root 用户,也不属于 998 组,所有没有权限读取 docker.sock

所以,到这儿得先把 jenkins 加入 998 组,否则不可能有权限的。这个 docker 组应该不存在,我们需要先创建该组,然后再把 jenkins 加入:

# 先退出容器,使用 root 登录(-u 指定登录用户)
$ sudo docker exec -it -u root jenkins bash

# 然后,创建 docker 组,指定与 docker.sock 一样的组 id
# groupadd -g <gid> <group_name>
$ groupadd -g 998 docker

# 最后,把 jenkins 加入该组
# usermod -a -G <group_name> <username>
$ usermod -aG docker jenkins

到这一步,我还是没有权限,重启容器也没有用,这是为什么呢?

一路走到黑?

折腾了很久都没效果,本来已经要放弃了,就在宿主机试验了一遍:docker 组已经存在,把普通用户加入 docker,重新登录,普通用户居然有权限了!

[springx@** ~]$ id
uid=1000(springx) gid=1000(springx) groups=1000(springx),995(docker) context=un...

可以看出,在宿主机中,当前登录的用户的会话是包含 docker 组的(groups = 1000,995,宿主机的 docker 组 id 正好为 995)。

相反的是,在容器内部,即使修改了用户组,登录会话的组还是 docker-compose 配置的 995(groups),所以怎么试都没有权限。

找到问题的根源:有没有权限,与登录会话有关,需要拥有 docker 组的权限才可以。

既然知道了问题的根源,那如何让登录会话拥有 docker 组权限呢?

临时测试方法(newgrp

想要修改登录会话所属组,就要修改登录身份。也就是说,用户以哪个组的身份登录,就拥有对应组的权限。

我们可以通过 newgrp 临时切换到 docker 组,来测试 jenkins 用户是否有权限使用 docker:

$ newgrp docker
$ id
uid=1000(jenkins) gid=995 groups=998
$ docker version
# ... OK!

从上面的切换方式可以看出,jenkins 已经有了权限,但是重新登录后,又会回到原来的组,而且仅仅是当前切换后的会话才有权限,在项目中使用 docker 还是没有权限的。

永久解决

既然修改登录的组,就能获取权限,那该如何彻底解决呢?

前面提到,docker-compose 配置了用户 id 和组 id,这将会使用户以该配置的运行,所以,我们只需要把组 id 指定为和宿主机一样的 docker 组 id 即可

从最前面给出的 docker-compose 配置文件,可以看出给 user 的配置是 1000:995,1000 是宿主机的用户,Jenkins 是该用户部署的,默认用户是 jenkins(1000),配置一样的 id,可以避免映射处理的文件的读写权限问题;995 是宿主机 docker 的组 id,这样容器内部的用户就会以 1000:955 的身份登录。

# 宿主机的 docker 组 id
$ cat /etc/group | grep docker
docker:x:995:springx

小结

对于容器内的 Jenkins,想要使用宿主机的 docker,只需要在 docker-compose.yaml 中配置 user,指定宿主机 docker 组 id 即可,并不需要在容器内创建 docker 组(实际 Jenkins 容器内并不存在 995 的组,但这并不影响)

version: '3.9'
services:
  jenkins:
    image: jenkins/jenkins:jdk17
    container_name: jenkins
    # 指定用户 uid:gid(`用户id`:`docker组id`,跟文件权限有关)
    # 只要 995 这个值与宿主机的 docker 组 id(使用 `cat /etc/group | grep docker` 查看)一致即可!
    user: 1000:995

2.2 映射目录权限问题

映射到宿主机的目录和文件,同样会遇到权限问题,这个与上面 docker 权限问题相比简单至极。

要么,开放映射目录(.jenkins_home)的所有权限。(这样似乎不太好?反正我感觉不好,但我没证据~)

$ chmod 777 .jenkins_home

要么,切换映射目录的归属。例如,我使用普通用户 springx(1000:1000)部署 Jenkins,目录所有者就是 1000,刚好 Jenkins 容器内部默认用户 jenkins 也是 1000:1000,所以,只要用 springx 用户先创建 .jenkins_home,然后配置 docker-compose 指定 user: 1000:gid 即可。

[springx@fun ~]$ mkdir .jenkins_home
# 如果使用其他用户创建,需要修改权限
[springx@fun ~]$ chwon 1000:1000 .jenkins_home

如果没有提前创建 .jenkins_home,它的默认所有者是 root,此时如果使用普通用户部署(就如上诉使用 1000 用户部署),容器内部的用户是没有权限的:

$ docker-compose up
[+] Running 2/1
 ✔ Network jenkins2_default  Created          
 ✔ Container jenkins2           Created          
Attaching to jenkins2
jenkins2  | touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
jenkins2  | Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?
jenkins2 exited with code 0
  • 这里 /var/jenkins_home 映射到了 .jenkins_home
  • 如果配置了重启策略 restart: unless-stopped,只需要修改 chown springx:springx .jenkins_home,然后等待重启即可。
  • 如果执行 docker-compose 的命令是我的普通用户 springx,那么 .jenkins_home 是会有权限的,只不过我没有把 springx 用户加入 docker 组,而是用 sudo 执行的

小结

如果使用 root 用户部署,映射目录不会有权限问题;如果使用普通用户部署,需要先创建映射目录,并配置相应的权限,否则自动创建的映射目录,属于执行执行创建命令的用户所有,如果用户 id 不一致,会出现无法写入的问题(我用的 sudo,则创建的文件的所有者都是 root)。

总结

使用 docker-compose 部署 Jenkins,主要涉及几个权限问题,其中主要的是 docker 权限和映射目录的权限:

docker 权限可以使用 root 部署,或者修改 docker.sock 权限(不建议);也可以通过配置,把容器内部的用户,添加到和宿主机 docker 组一样的 id 组即可(user: 1000:995)。

映射目录权限,可以先手动创建,然后分配给相应的用户,并且 docker-compose 配置对应的用户 id 即可;也可以创建后,再修改权限,等待容器自动重启即可。