This article is a continuation of the introduction to the combination of Vagrant and Docker that I started here: First steps with provisioning of Docker containers using Vagrant as provider. These first steps include how you can leverage Vagrant to create and manage a simple Docker container as well as a Virtual Box Linux VM that provides the Docker engine & infrastructure. Execution of simple Docker files was demonstrated including how to make files from the Windows or Vagrant host available in the Docker build context. We have seen the commands to attach to the running container and look what is going on. We have also seen how we can execute command against the Docker container.
In this article, I will discuss more complex challenges including more intricate Docker files that create more interesting Docker containers. I will also discuss more advanced options and operations including the installation of Java, mapping folders into the Docker container (from both the Windows host and the dockerhostvm), using port forwarding to access a service (Apache HTTP Server) running in the Vagrant container from the Windows host, linking containers and using a Data Container to prevent having to create temporary files in the Docker layer structure during the container build process.
In the combination of Vagrant and Docker as discussed in this article, we have the Docker Container running inside the Linux Docker host that itself is running inside a Virtual Machine on top of the Windows host. Folder mappings between these three components make files mutually available and shareable. Vagrant can be used to configure the desired folder mappings – in addition to the folder mappings that are available by default.
The folder containing the Vagrantfile is automatically available inside the dockerhostvm, mapped to /vagrant. Likewise, this folder is automatically shared with the Docker container as /vagrant. During the build process of the Docker container, all files and folders in the directory that contains the Dockerfile are available in the build context (to copy and otherwise process files from over the course of the container construction).
Using the standard synced_folder configuration definitions, we can define mappings between folders on the Vagrant host – the Windows machine – and both the dockerhostvm (in the DockerHostVagrantfile) and the Docker container my-little-container (in the Vagrantfile). The next illustration shows how the c:\temp directory on the Vagrant host is mapped to the /host_temp directory inside my-little-container. This is done using this instruction:
config.vm.synced_folder "c:/temp", "/host_temp"
in the Vagrantfile.
Vagrant implements this mapping in a two step process: first it creates a mapping between the host and the dockerhostvm (with a system-generated name such as /var/lib/docker/docker_1440312061_83042). Next it creates a Docker volume instruction to map this system defined, more or less hidden folder on the dockerhostvm into the Docker container.
In the DockerHostVagrantFile, we can use the following instruction to create a mapping from the Vagrant (Windows) host to the dockerhostvm:
config.vm.synced_folder "c:/data", "/host_data"
This maps the folder c:\data on the Windows host to the folder /host_data inside the dockerhostvm.
Inside the dockerhostvm, we can access a directory called /host_data that links through to c:\data. Note that this is standard Vagrant behavior, nothing to do with Docker. However, this /host_data folder on dockerhostvm can be mapped into the Docker container using a Docker volume configuration. Add the following entry in the Vagrantfile:
d.volumes = ["/host_data/:/dock_host_data"]
to ensure that when the container is started, the /host_data folder in dockerhostvm is mapped into the Docker container as /dock_host_data.
Let’s create a folder /share_x inside dockerhostvm. This folder is not mapped to the Vagrant host, it just lived inside the Virtual Machine. This folder too can be mapped into my-little-container by extending the d.volumes setting in the Vagrantfile:
d.volumes = ["/share_x/:/dock_host", "/host_data/:/dock_host_data"]
This instructs Vagrant to make sure that when starting my-little-container the volume instruction to the Docker engine includes the mapping of /share_x to /dock_host.
Docker containers do not persist state. Well, that is not quite correct. But sometimes – due to confusion between images and containers – this may be the impression you get. If any files inside the running container are changed, then these changes are not lost when the container is stopped. However, when another container is started from the same starting point, the changes are not part of the second container. When the first container is restarted, then the changes wrought in the earlier session(s) of that container are still around. Only when the container is be committed (resulting in a new image) will the current state be truly preserved and made available to other containers’ sessions. If a container runs an application or service that relies on persistent state – in the form of data, configuration files or anything else that should be available beyond the current session in sessions of other containers – then a way must be found to not have that state inside the container. Options include: using an external file system – mapped into the container as a mounted volume – to write the data, using a linked container (which only shifts the issue, not resolve it), use external services (such as a remote database, Cloud based storage,…) to capture the to be persisted state. We will talk about linking containers and the use of a dedicated data container a little bit later.
Let’s first add Apache Http Server into our container – and expose it using port forwarding.
Network Connections and Port Forwarding
What good is a container if we cannot access it. We have seen how from both the Vagrant host and the dockerhostvm we can get into our container to perform activities. However, we have not seen how we can access a running service inside a container. Suppose the container runs a database – then we would like to access it, over JDBC for example. Or let’s assume the container runs a web server – then we would like our browser to be able to access it over HTTP. The container is assigned an IP address, can communicate to the outside world over TCP/IP (and therefore HTTP) – leveraging the networking facilities of Virtual Box that Vagrant has setup for us. We can ping into the container from the dockerhostvm (and supposedly from the Vagrant host as well – however, I did not yet get that to work). Additionally, network ports in the container can be exposed (published) and forwarded to the dockerhostvm. This basically means that accessing http://localhost:6453 on dockerhostvm is processed in the container if a port forwarding was configured from some port in the container to port 6453.
We will see next how the Apache HTTP Server is installed into our container and how we can access this server – listening on port 80 – from the dockerhostvm at port 8080 (because of a port forwarding) and from the Vagrant host, as shown in the next figure.
In very short: the IP address of the dockerhostvm is assigned by Vagrant based on the network configuration in the DockerHostVagrantfile. The port fowarding between my-little-container and dockerhostvm is defined in the Docker configuration in the Vagrantfile. The IP address of the Docker container is assigned when the container is started (and can be different every time the container starts afresh). The files served by Apache are located in directory /var/www/html on the container (the default for Apache). These files were copied from the Vagrant host when the container was built; instructions for this are in the Dockerfile.
The Dockerfile is extended with the following lines to support installation of Apache and copying of the contents of the web-site directory from the Vagrant host into the container:
RUN apt-get update RUN apt-get install -q -y apache2 EXPOSE 80 COPY /web-site/ /var/www/html/
optionally a CMD instruction could be added to start Apache whenever the container is started (CMD exec /usr/sbin/apache2ctl -D FOREGROUND). Instead, this instruction is configured in the Vagrantfile.
The configuration in the Vagrantfile is extended with two configurations:
d.cmd = ["/usr/sbin/apache2ctl","-D", "FOREGROUND"] d.ports = ["8080:80"]
The first one ensures that after starting up the container, Apache is started. The second configures the port forwarding between dockerhostvm (port 8080) to my-little-container (port 80).
In DockerHostVagrantfile is the configuration for the private network (through Virtual Box) between Vagrant host and dockerhostvm – making the latter available to the former at IP address 10.10.10.29:
config.vm.network :private_network, ip: "10.10.10.29"
Note: in theory, we should also be able to define port forwarding between Vagrant host and dockerhostvm (using a configuration such as config.vm.network :forwarded_port, host: 8080, guest: 8080). This should allow us to access Apache from the Vagrant host using localhost:8080; however, I have not yet been able to get this to work.
The next figure shows the various configurations made and their interdependencies.
Managing Multiple Containers with Vagrant and Linking Containers
Vagrant can do more than build and manage a single container: it can do the same for multiple containers. And these containers can be linked. This means we can have Vagrant prepare the following situation for us, where we can access the Apache server in my-little-container from my-tiny-container – using the alias my-friend. As a result, we can issue the command curl myfriend/hello-world.html in my-tiny-container and receive the contents from the hello-world.html from the Apache server.
A few modifications are required for this to work.In Vagrantfile, we define a second virtual machine to be provided/provisioned. This vm – called my-tiny-container – is also provided by the Docker provider using the same dockerhostvm as the Docker host. A child directory called tiny-docker is created under the directory that contains the Vagrantfile. In this directory is a new Dockerfile – that specifies the image to use for my-tiny-container and instructs Docker to install the curl utility. In the Vagrantfile, a link is defined from my-tiny-container to my-little-container, specifying the alias my-friend to be used inside my-tiny-container when referring to my-little-container.
To see this in action, we first bring up the new container:
When the container build is complete and successful:
We can gain access to the container – either from dockerhostvm or from the Vagrant host as shown here:
When inside dockerhostvm we can use “curl my-friend/hello-world.html” to access the document hello-world.html served up by Apache server in the companion container my-little-container (that we know under the name my-friend).
Note that using ping myfriend we can also get in touch with the linked container.
The configuration details in the files mentioned above are:
config.vm.define "my-tiny-container" do |m| m.vm.provider :docker do |d| d.build_dir = "tiny-docker" d.name = 'my-tiny-container' d.vagrant_machine = "dockerhostvm" d.vagrant_vagrantfile = "./DockerHostVagrantfile" d.remains_running = true d.link("my-little-container:my-friend") d.cmd = ["ping", "-c 51", "127.0.0.1"] end end
the new Dockerfile (in the tiny-docker directory):
FROM ubuntu:14.04 RUN apt-get install -q -y curl
Note: now that the Vagrantfile has to deal with more than one vm, all commands have to be qualified with the name of the vm to which they should apply – my-little-container or my-tiny-container. For example vagrant up my-tiny-container and vagrant docker-run my-tiny-container -t — bash.
Sharing a volume across containers
One container can leverage the volumes defined for another container – effectively sharing the same underlying file system area. In this example, we specify for my-tiny-container that it shares the volumes defined for my-little-container. That means specifically that the volumes /dock-host-data en /dock-host that are configured for my-little-container are also accessible inside my-tiny-container.
When the container is brought up, the following command is executed by Vagrant against Docker in the dockerhostvm:
Command: “docker” “run” “–name” “my-tiny-container” “-d” “–link” “my-little-container:my-friend” “-v” “/var/lib/docker
/docker_1440328908_21248:/host_temp” “-v” “/var/lib/docker/docker_1440328908_88325:/vagrant” “–volumes-from=my-little-container”
“aa91c3674269” “ping” “-c 51” “127.0.0.1”
note the bold part that is what does the trick. This instruction is based on the following addition in the Vagrantfile:
d.create_args = ["--volumes-from=my-little-container"]
in the configuration for the my-tiny-container vm.
Inside the running my-container we can access the volumes from my-little-container – /dock_host and /dock_host_data – just as if they were created directly for my-tiny-container.
Using a Data Container
As discussed above, Docker containers do not persist and share their state (unless explicitly committed as images). They do persist state – but only for their own sake. Other containers will not be able to reuse that state. Vagrant does reuse the same container when you go through a vagrant up – vagrant halt – vagrant up cycle. That means that changes applied to the container in the first session are around in the second, because it uses a different container. When you perform a vagrant docker-run against a container, you will get a second, distinct container (although based on the same image) that does not have the changes from the other container; also note that this container is immediately removed after the execution is complete.
Note that changes created in a container are only really lost when that container is removed completely. To be exact, read this explanation by Adrian Mouat (in this article): “Docker images are stored as series of read-only layers. When we start a container, Docker takes the read-only image and adds a read-write layer on top. If the running container modifies an existing file, the file is copied out of the underlying read-only layer and into the top-most read-write layer where the changes are applied. The version in the read-write layer hides the underlying file, but does not destroy it — it still exists in the underlying image. When a Docker container is deleted, relaunching the image will start a fresh container without any of the changes made in the previously running container — those changes are lost.”
Files that are manipulated from within the container ideally live outside the container – for example on host folders that are mapped into the container. A special pattern is the use of a dedicated Docker data (only) container. This is a container that is linked to other containers with a sole purpose of making its volumes available to the other container(s). As explained by Raman Gupta: “Even though containers do not persist data across invocations, volumes declared by containers do. Even when not explicitly mapped to any host directory. And since docker volumes, and the data they contain, survive as long as any container references them, even indirectly, as long as the data container exists (even if not running), the data is logically and effectively “stored” within docker.”
Let me not discuss all details that are very adequately explained by the authors whose articles I have listed below under resources. Let’s just look at an example of using a Data Container.
The next figure shows a third container, defined in the Vagrantfile and created and managed by Vagrant just like the previous two. This container is called my-data-container. It is built from a very simple Dockerfile that consists of two lines: one to indicate the image – the same image as the other two containers – and a VOLUME instruction. As Adrian Mouat puts is: “volumes are directories (or files) that are outside of the default Union File System and exist as normal directories and files on the host filesystem” Because of the VOLUME instruction, a folder is created in the Docker host – dockerhostvm – under /var/lib/docker/volumes if you must know , but you really should not make use of that information. This folder is mounted in the container as /data. Any change in /data in the container is reflected on the host file system and vv.
And now the the real action. Bring up the my-data-container:
Note: the container is started and immediately exited (as there is no CMD defined)
Next, bring up my-tiny-container:
There is no special indication in the logging of the volumes-from instruction.
When we enter into my-tiny-container with vagrant docker-run my-tiny-container -t — bash we can navigate to the directory /data and create a new file using vi readme.txt.
When we then exit the container and start another one, using the same command, we will find that the file readme.txt still lives in /data – which is of course the volume in the my-data-container that is in fact a directory on the file system of dockerhostvm..
Any instance of my-tiny-container will be able to leverage this same volume, over and over again.
From the dockerhostvm, we can inspect details for my-data-container:
and learn about the /data volume:
Note that we can use the volumes-from on other containers as well – and thereby also allow them access to the /data volume in (or rather set up by) the my-data-container.
One topic needs to be discussed in a further article: a volume in the my-data-container mapped to a host folder (on the Vagrant host). Is it also available in my-tiny-container that uses the volumes-from on my-data-container? And is the contents from that host-folder also available during the build stage of the container? I believe it is which would mean that installation files, temporary files (result of unzipping installers for example) etc. do not have to be copied into the layers of the container that is being built but instead remain outside of it. That also means that we do not run into the 10 GB size limitation that I ran into before when building containers.
Installing Java into the Container
Something almost trivial really – get Java set up in the container. There are many ways of going about this. I will pick a very simple one.
With the following additions to the /tiny-docker/Dockerfile I take care of adding the default JDK to the Ubuntu image (which requires first to run apt-get update to fix many dependencies). Subsequently, I copy a simple Java Class (HelloWorld) from /tiny-docker/files/services) to the container, compile that class using javac and run the class:
RUN apt-get update RUN apt-get install -q -y default-jdk COPY /files/services/HelloWorld.java /u01/services/ RUN javac /u01/services/HelloWorld.java RUN java -cp /u01 services.HelloWorld
See the result of bringing up the container with vagrant up:
The output from class HelloWorld is visible under Step 6, proof that the JDK was installed and could be used to compile and run a Java Class.
This article showed how you can leverage somewhat more complex Docker files that create more interesting Docker containers – such as those that are Java or Apache enabled. I have shown some advanced options and operations regarding the provisioning of Docker containers from Vagrant, including the installation of Java, mapping folders (from both the Windows host and the dockerhostvm), using port forwarding to access Apache running in the Vagrant container from the Windows host, linking containers and using a Data Container.
GitHub Repository with Sources for this article: https://github.com/lucasjellema/vagrant-docker-explorations
Tips and Guidelines for writing Docker Files: http://kimh.github.io/blog/en/docker/gotchas-in-writing-dockerfile-en/
Building Containerized Apps With Vagrant – on Willem’s Fizzy Logic
Docker Networking Made Simple or 3 Ways to Connect LXC Containers by Lukas Pustina
My own earlier articles on Docker: My First Steps with Docker – starting from Windows as host and Docker – Take Two – Starting From Windows with Linux VM as Docker Host
On enabling Puppet on Ubuntu: https://docs.puppetlabs.com/guides/install_puppet/install_debian_ubuntu.html
On installing Apache HTTP Server: https://www.maketecheasier.com/install-and-configure-apache-in-ubuntu/
On installing Java on Ubuntu: https://www.digitalocean.com/community/tutorials/how-to-install-java-on-ubuntu-with-apt-get
On Java 8 and its installation using Docker files from Oracle: https://github.com/dockerfile/java/blob/master/oracle-java8/Dockerfile
On storage, Volumes and persistence
Why Docker Data Containers are Good – by Raman Gupta
Docker storage 101: How storage works in Docker – by Chris Evans
Persisting Data with Docker Containers – by Ethan Buchman
Understanding Volumes in Docker – by Adrian Mouat