Docker 中的 Docker 无法安装卷

我正在运行一个 Jenkins 集群,在 Master 和 Slave 中,它们都作为 Docker 集装箱运行。

Host 是运行在 MacOS 上的最新 boot2docker VM。

为了允许 Jenkins 能够使用 Docker 执行部署,我将 Docker.sock 和 Docker 客户机从主机挂载到 Jenkins 容器,如下所示:-

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):/usr/bin/docker -v $HOST_JENKINS_DATA_DIRECTORY/jenkins_data:/var/jenkins_home -v $HOST_SSH_KEYS_DIRECTORY/.ssh/:/var/jenkins_home/.ssh/ -p 8080:8080 jenkins

我面临的问题,而安装一个卷的多克集装箱内运行的詹金斯容器。例如,如果需要在 Jenkins 容器内运行另一个 Container,则执行以下操作:-

sudo docker run -v $JENKINS_CONTAINER/deploy.json:/root/deploy.json $CONTAINER_REPO/$CONTAINER_IMAGE

上面的代码运行容器,但是文件“ loy.json”不是作为文件挂载的,而是作为“ Directory”挂载的。即使我以卷的形式挂载目录,也无法查看结果容器中的文件。

这是一个问题,因为文件权限由于多克在多克的情况下?

34336 次浏览

A Docker container in a Docker container uses the parent HOST's Docker daemon and hence, any volumes that are mounted in the "docker-in-docker" case is still referenced from the HOST, and not from the Container.

Therefore, the actual path mounted from the Jenkins container "does not exist" in the HOST. Due to this, a new directory is created in the "docker-in-docker" container that is empty. Same thing applies when a directory is mounted to a new Docker container inside a Container.

Very basic and obvious thing which I missed, but realized as soon I typed the question.

Another way to go about this is to use either named volumes or data volume containers. This way, the container inside doesn't have to know anything about the host and both Jenkins container and the build container reference the data volume the same way.

I have tried doing something similar to what you are doing, except with an agent rather that using the Jenkins master. The problem was the same in that I couldn't mount the Jenkins workspace in the inner container. What worked for me was using the data volume container approach and the workspace files were visible to both the agent container and the inner container. What I liked about the approach is the both containers reference the data volume in the same way. Mounting directories with an inner container would be tricky as the inner container now needs to know something about the host that its parent container is running on.

I have detailed blog post about my approach here:

http://damnhandy.com/2016/03/06/creating-containerized-build-environments-with-the-jenkins-pipeline-plugin-and-docker-well-almost/

As well as code here:

https://github.com/damnhandy/jenkins-pipeline-docker

In my specific case, not everything is working the way I'd like it to in terms of the Jenkins Pipeline plugin. But it does address the issue of the inner container being able to access the Jenkins workspace directory.

Regarding your use case related to Jenkins, you can simply fake the path by creating a symlink on the host:

ln -s $HOST_JENKINS_DATA_DIRECTORY/jenkins_data /var/jenkins_home

A way to work around this issue is to mount a directory (inside your docker container in which you mounted your docker socket) using the exact same path for its destination. Then, when you run a container from within that container, you are able to mount anything within that mount's path into the new container using docker -v.

Take this example:

# Spin up your container from which you will use docker
docker run -v /some/dir:/some/dir -v /var/run/docker.sock:/var/run.docker.sock docker:latest


# Now spin up a container from within this container
docker run -v /some/dir:/usr/src/app $CONTAINER_IMAGE

The folder /some/dir is now mounted across your host, the intermediate container as well as your destination container. Since the mount's path exists on both the host as the "nearly docker-in-docker" container, you can use docker -v as expected.

It's kind of similar to the suggestion of creating a symlink on the host but I found this (at least in my case), a cleaner solution. Just don't forget to cleanup the dir on the host afterwards! ;)

This also works via docker-compose and/or named volumes so you don't need to create a data only container, but you still need to have the empty directory on the host.

Host setup

Make host side directories and set permissions to allow Docker containers to access sudo mkdir -p /var/jenkins_home/{workspace,builds,jobs} && sudo chown -R 1000 /var/jenkins_home && sudo chmod -R a+rwx /var/jenkins_home

docker-compose.yml

version: '3.1'
services:
jenkins:
build: .
image: jenkins
ports:
- 8080:8080
- 50000:50000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- workspace:/var/jenkins_home/workspace/
# Can also do builds/jobs/etc here and below
jenkins-lts:
build:
context: .
args:
versiontag: lts
image: jenkins:lts
ports:
- 8081:8080
- 50001:50000
volumes:
workspace:
driver: local
driver_opts:
type: none
o: bind
device: /var/jenkins_home/workspace/

When you docker-compose up --build jenkins (you may want to incorporate this into a ready to run example like https://github.com/thbkrkr/jks where the .groovy scripts pre-configure Jenkins to be useful on startup) and then you will be able to have your jobs clone into the $JENKINS_HOME/workspace directory and shouldn't get errors about missing files/etc because the host and container paths will match, and then running further containers from within the Docker-in-Docker should work as well.

Dockerfile (for Jenkins with Docker in Docker)

ARG versiontag=latest
FROM jenkins/jenkins:${versiontag}


ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"


COPY jenkins_config/config.xml /usr/share/jenkins/ref/config.xml.override
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt


USER root
RUN curl -L http://get.docker.io | bash && \
usermod -aG docker jenkins
# Since the above takes a while make any other root changes below this line
# eg `RUN apt update && apt install -y curl`
# drop back to the regular jenkins user - good practice
USER jenkins
EXPOSE 8080

If you are like me and don't want to mess with Jenkins Setup or too lazy to go through all this trouble, here is a simple workaround I did to get this working for me.

Step 1 - Add following variables to the environment section of pipeline

environment {
ABSOLUTE_WORKSPACE = "/home/ubuntu/volumes/jenkins-data/workspace"
JOB_WORKSPACE = "\${PWD##*/}"
}

Step 2 - Run you container with following command Jenkins pipeline as follows.

    steps {
sh "docker run -v ${ABSOLUTE_WORKSPACE}/${JOB_WORKSPACE}/my/dir/to/mount:/targetPath imageName:tag"
}

Take note of the double quotes in the above statement, Jenkins will not convert the env variables if the quotes are not formatted properly or single quotes are added instead.


What does each variable signify?

  • ABSOLUTE_WORKSPACE is the path of our Jenkins volume which we had mounted while starting Jenkins Docker Container. In my case, the docker run command was as follows.

    sudo docker run \ -p 80:8080 \ -v /home/ubuntu/volumes/jenkins-data:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -d -t jenkinsci/blueocean

Thus the varible ABSOLUTE_WORKSPACE=/home/ubuntu/volumes/jenkins-data + /workspace

  • JOB_WORKSPACE command gives us the current workspace directory where your code's lives. This is also the root dir of your code base. Just followed this answer for reference.

How does this work ?

It is very straight forward, as mentioned in @ZephyrPLUSPLUS ( credits where due ) answer, the source path for our docker container which is being run in Jenkins pipeline is not the path in current container, rather the path taken is host's path. All we are doing here is constructing the path where our Jenkins pipeline is being run. And mounting it to our container. Voila!!

Here's a little illustration to help clarify ... enter image description here

I have same problem in Gitlab CI, I solved this by using docker cp to do something like mount

script:
- docker run --name ${CONTAINER_NAME} ${API_TEST_IMAGE_NAME}
after_script:
- docker cp ${CONTAINER_NAME}:/code/newman ./
- docker rm ${CONTAINER_NAME}

You can solve this passing in an environment variable. Example:

.
├── docker-compose.yml
└── my-volume-dir
└── test.txt

In docker-compose.yml

version: "3.3"
services:
test:
image: "ubuntu:20.04"
volumes:
- ${REPO_ROOT-.}/my-volume-dir:/my-volume
entrypoint: ls /my-volume

To test run

docker run -e REPO_ROOT=${PWD} \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ${PWD}:/my-repo \
-w /my-repo \
docker/compose \
docker-compose up test

You should see in the output:

test_1  | test.txt

Lots of good info in these posts but I find none of them are very clear about which container they are referring to. So let's label the 3 environments:

  • host: H
  • docker container running on H: D
  • docker container running in D: D2

We all know how to mount a folder from H into D: start D with

docker run ... -v <path-on-H>:<path-on-D> -v /var/run/docker.sock:/var/run/docker.sock ...

The challenge is: you want path-on-H to be available in D2 as path-on-D2.

But we all got bitten when trying to mount the same path-on-H into D2, because we started D2 with

docker run ... -v <path-on-D>:<path-on-D2> ...

When you share the docker socket on H with D, then running docker commands in D is essentially running them on H. Indeed if you start D2 like this, all works (quite unexpectedly at first, but makes sense when you think about it):

docker run ... -v <path-on-H>:<path-on-D2> ...

The next tricky bit is that for many of us, path-on-H will change depending on who runs it. There are many ways to pass data into D so it knows what to use for path-on-H, but probably the easiest is an environment variable. To make the purpose of such var clearer, I start its name with DIND_. Then from H start D like this:

docker run ... -v <path-on-H>:<path-on-D> --env DIND_USER_HOME=$HOME \
--env DIND_SOMETHING=blabla -v /var/run/docker.sock:/var/run/docker.sock ...

and from D start D2 like this:

docker run ... -v $DIND_USER_HOME:<path-on-D2> ...

Based from the description mentioned by @ZephyrPLUSPLUS here is how I managed to solve this:

vagrant@vagrant:~$ hostname
vagrant
vagrant@vagrant:~$ ls -l /home/vagrant/dir-new/
total 4
-rw-rw-r-- 1 vagrant vagrant 10 Jun 19 11:24 file-new
vagrant@vagrant:~$ cat /home/vagrant/dir-new/file-new
something
vagrant@vagrant:~$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock  docker /bin/sh
/ # hostname
3947b1f93e61
/ # ls -l /home/vagrant/dir-new/
ls: /home/vagrant/dir-new/: No such file or directory
/ # docker run -it --rm -v /home/vagrant/dir-new:/magic ubuntu /bin/bash
root@3644bfdac636:/# ls -l /magic
total 4
-rw-rw-r-- 1 1000 1000 10 Jun 19 11:24 file-new
root@3644bfdac636:/# cat /magic/file-new
something
root@3644bfdac636:/# exit
/ # hostname
3947b1f93e61
/ # vagrant@vagrant:~$ hostname
vagrant
vagrant@vagrant:~$

So docker is installed on a Vagrant machine. Lets call it vagrant. The directory you want to mount is in /home/vagrant/dir-new in vagrant. It starts a container, with host 3947b1f93e61. Notice that /home/vagrant/dir-new/ is not mounted for 3947b1f93e61. Next we use the exact location from vagrant, which is /home/vagrant/dir-new as the source of the mount and specify any mount target we want, in this case it is /magic. Also note that /home/vagrant/dir-new does not exist in 3947b1f93e61. This starts another container, 3644bfdac636. Now the contents from /home/vagrant/dir-new in vagrant can be accessed from 3644bfdac636.

I think because docker-in-docker is not a child, but a sibling. and the path you specify must be the parent path and not the sibling's path. So any mount would still refer to the path from vagrant, no matter how deep you do docker-in-docker.