Welcome to my tutorials on how to leverage Docker to deploy your application! This is meant as much to help me learn, so it doubles as a “what worked for me” series.
In the previous lesson, we had a brief primer on Docker and building images, created a two-line Dockerfile, and built and ran an image from that.
This lesson, we are going to add our own files into the image we want to create. After all, I don’t know anyone that wants to write their entire application as a sh script in the CMD line of a Dockerfile. So we will build two images. The first image will move the sh command into a file and run that. The second will expand on that and run a couple of different shell scripts.
Before starting, we need to review how files get stored in an image and discuss layers. An image is a virtual file system, composed of layers. An image can have multiple layers, and an image that is based on another image uses all the layers of that base image. Docker has two features that make this really neat: image layers overlay into the virtual filesystem, and each layer only contains changes. The first point means that the virtual filesystem is built by taking the files in the first layer, then adding the files from the second layer, and so on. This is like extracting a zip file, then extracting a second zip on top, then extracting a third on that. The files from the last layer wins in case of conflict, so if two layers had /bin/ls, the ls file from the last layer is used. The second point means that the layers (and by extension the image) is small- only changes are tracked. In our last lesson, there were no new files-so that layer was really tiny. For this lesson, the layer only needs to store our one file and the code to run the script- the busybox image has all the layers we need for this to work.
For the first image, start by creating an empty folder for your image “project”. In there, create a file called helloworld.sh and add the following:
helloworld.sh
#!/bin/sh
echo Hello World!
NOTE: If you are on Windows, the .sh files must be saved with Unix-style newlines. I was able to use Notepad++ (https://notepad-plus-plus.org/) and clicked on Edit->EOL Conversion->Unix to get this to work. You can also download VS Code (https://code.visualstudio.com/) and write your file in that program; in the bottom right of the window, clicking on CRLF will let you click on LF to switch to Unix newlines. If you don’t do this, you get the error “exec /helloworld.sh: no such file or directory” when you run the image in a container. The Notepad that comes with Windows will tell you if your file has Unix line endings, but it still does not convert it.
Also create a Dockerfile file in the folder:
FROM busybox
COPY --chmod=555 ./helloworld.sh /helloworld.sh
CMD ["/helloworld.sh"]
This introduces the COPY command. COPY works very similar to the cp (or copy) shell command. The COPY command expects one or more source paths and one destination path. The source path is relative to the path given to docker build. The destination must be absolute, and uses the paths of the image’s virtual file system. Note that the files you want to copy must be present in the current or a child directory from where you run docker build, as this directory gets temporarily copied into Docker for image creation.
Another feature about this COPY command is the --chmod=555 that follows the COPY command itself. It is a Unix permissions value. This specific value (555) allows for the file to be read and executed (but not edited) by any user in the container. On Linux, Mac, and WSL, the file permissions your source files have are copied into the image. On native Windows, the permissions are always 755, which allows only the owning user to make changes while allowing all users to read and execute. If the permissions are not set correctly, your container will be unable to execute or read the files you copy over. Of course, this is problematic, and it has given me a few headaches, too. As a general rule of thumb, include the --chmod=555 option anytime you need a file to be executable in the image.
Now change your directory to where your Dockerfile is at and let’s build and run this image:
docker build -t copyfile1 .
docker run --rm copyfile1
Now that we have run the file that we added to the image, let’s make things more complicated. Create a new folder and add the following three files.
whoareyou.sh
#!/bin/sh
echo What is your name?
read name
/greet.sh $name
greet.sh
#!/bin/sh
clear
echo Hello, $1!
echo At $(date) I said hello to $1.
ls /
Dockerfile
FROM busybox:glibc
COPY --chmod=555 ./*.sh /
CMD ["/whoareyou.sh"]
Run the following in the new Dockerfile directory to bake your image and run it:
docker build -t copyfile2 .
docker run -it --rm copyfile2
Now you should be prompted for your name, and when you hit enter, the screen clears, your image greets you, tells you when it greeted you, and lists the root of the image’s virtual filesystem. Not bad for a few lines of text in three files, right?
If you noticed, we included the -it option with docker run. Without it, docker does not allocate a TTY terminal or pass your input into the container. This would effectively start your script, but not let you type into it. That would stink.
To summarize, we reviewed how to add files into an image and execute them. We also covered how the image is made of layers and why this saves space. Last, we explained why a shell script written on Windows might fail in an image if the newlines are not in Unix format.
For the next lesson, we will explore how to install software packages into our image.
