How to Conditionally Create a File in a Dockerfile Build
Building from my previous post of how to conditionally copy a file in a Docker image build from a Dockerfile, I am going over how to conditionally create within a Docker image build in a Dockerfile. For example:
- If a specified file is not in the working directory, create a file.
- Otherwise, use the specified file.
Motivation
The main reason I want to solve this problem is I want to have a simple Dockerfile for existing Ruby on Rails projects, but also to create new Ruby on Rails projects.
With the way Docker uses images, I will always create a new Docker image based on the project, but I would like to have one goto Dockerfile for any Ruby on Rails project. For example:
- I want to take an existing app and put it in a Docker container.
- I want to start a new app and have it inside a Docker container.
Requirements
If you want to follow along, the approach I took with this solution will require:
- Install Docker Community Edition
I did development on a UNIX based system (such as macOS or Ubuntu),
mainly for bash utility: echo
.
Create File
In my example, I will create a new Ruby Gemfile. Otherwise, use the existing Gemfile (copied over in the build process).
If there needs to be a new Gemfile, create the following Gemfile with contents:
ruby '2.5.1'
source 'https://rubygems.org'
gem 'rails', '~> 5.1.0'
It’s the same as the default Gemfile I used in my Docker Rails article.
Solution
The approach I took to solve the problem of creating a Gemfile if it is not in the folder, or using the existing one is to:
- Test for existence of the Gemfile inside the working directory.
- If the Gemfile is not there, write a prebuilt Gemfile.
I used Bash for my solution as the system Docker is building will have Bash available.
Testing for File in Bash
The Bash snippet to test for the Gemfile existence is:
if [ ! -e "Gemfile" ]; then
# do something
fi
The do something
section is where I will have the script write the
file contents, but how?
Creating Files with: echo
Using another Unix command, echo
, we can write the contents of a
file in the form:
echo "file contents" > file.txt
So, combining the above, the function becomes:
if [ ! -e "Gemfile" ]; then
echo "ruby '2.5.1'" > Gemfile
echo "source 'https://rubygems.org'" >> Gemfile
echo "gem 'rails', '~> 5.1.0'" >> Gemfile
fi
There is a slight difference between >
and >>
:
- The former,
>
, will overwrite the file with a new one. - While
>>
, will append to the existing file.
Small difference in syntax will have large consequences.
Making Script Executable: chmod
If we write the script file to disk out and add an execution bit to it:
$ ls
script.sh
$ cat script.sh
if [ ! -e "Gemfile" ]; then
echo "ruby '2.5.1'" > Gemfile
echo "source 'https://rubygems.org'" >> Gemfile
echo "gem 'rails', '~> 5.1.0'" >> Gemfile
fi
$ chmod +x script.sh
$ ./script.sh
$ ls
Gemfile script.sh
$ cat Gemfile
ruby '2.5.1'
source 'https://rubygems.org'
gem 'rails', '~> 5.1.0'
$
So, the script does what we want, how do we get this script inside the Dockerfile?
File COPY
One way is to write the file and have the Dockerfile copy the script over, the commands inside the Dockerfile:
...
CMD mkdir /app
WORKDIR /app
COPY script.sh .
...
This approach works, especially if the script file is large or complex.
One downside to this approach is there is another file maintain with the Dockerfile. Every project will have to include the Dockerfile and the additional file.
File RUN
Another approach is to embed the script within the Dockerfile and use
Docker’s RUN
command to create a script to create the file. (Think
how meta that is!)
I prefer this approach
I will take advantage of the echo
command from earlier, a shortened
version in a Dockerfile:
...
RUN echo "file contents" > script.sh
CMD chmod +x script.sh
CMD ./script.sh
...
In this case, I chose to have the RUN
command write the script out as:
RUN echo "!#/bin/bash\nif [ ! -e \"Gemfile\" ]; then\necho \"ruby '2.5.1'\" > Gemfile\necho \"source 'https://rubygems.org'\" >> Gemfile\necho \"gem 'rails', '~> 5.1.0'\" >> Gemfile\nfi" > script.sh
CMD chmod +x script.sh
CMD ./script.sh
Having the whole script in a single line is long and hard to read or change, but once it’s perfected, there only needs to be a single file to keep around, the Dockerfile.
Can I Do Better?
Actually, like all good computer scientist, I have to ask: “Can we do better?” (than having the whole file on one line)?
When I look at the Dockerfile
documentation,
I notice that the \
operator separates multiple line arguments on
different lines.
Let’s see if that works for strings as well:
...
RUN echo "#!/bin/bash\n" \
"if [ ! -e \"Gemfile\" ]; then\n" \
" echo \"Creating default Gemfile\"\n" \
" echo \"ruby '2.5.1'\n" \
" source 'https://rubygems.org'\n" \
" gem 'rails', '~> 5.1.0'\" > Gemfile\n" \
"fi\n" > script.sh
...
Let’s see by building it out:
$ docker build . -t test
$ docker run -it test bash
root:test $ ls
script.sh
root:test $ cat script.sh
#!/bin/bash
if [ ! -e "Gemfile" ]; then
echo "Creating default Gemfile"
echo "ruby '2.5.1'
source 'https://rubygems.org'
gem 'rails', '~> 5.1.0'" > Gemfile
fi
root:test $
So, wow, we can embed a whole shell script inside the Dockerfile and have it on multiple lines, making it more readable. Now this is bootstrapping a Docker image through the Dockerfile!
Dockerfile
The section below is the Dockerfile I used for reference:
FROM ruby:2.5.0-slim
RUN apt-get update && apt-get install -qq -y --no-install-recommends \
build-essential \
libsqlite3-dev \
nodejs \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /app
WORKDIR /app
COPY Dockerfile Gemfile* .
RUN echo "#!/bin/bash\n" \
"IFS=';'\n" \
"additional_gems=\"$add_gems\"\n" \
"if [ ! -e \"Gemfile\" ]; then\n" \
" echo \"Creating default Gemfile\"\n" \
" echo \"ruby '2.5.1'\n" \
" source 'https://rubygems.org'\n" \
" gem 'rails', '~> 5.1.0'\" > Gemfile\n" \
" for i in \$additional_gems; do\n" \
" echo \"gem \$i\" >> Gemfile;\n" \
" done\n" \
"fi\n" > script.sh
RUN chmod +x script.sh
RUN ./script.sh
RUN bundle install
RUN rails new temp_app
RUN rm -rf temp_app
The file is also downloadable here
To build and test, just run:
$ docker build . -t image_name
$ docker run -it image_name bash
/app $ ls
Dockerfile script.sh
Conclusion
I wanted to have a Dockerfile that I can use to:
- Create a new Rails project
- Dockerize and existing Rails project
By having a shell script to create a script to create a new Gemfile, I have solved the former while keeping compatibility with the latter using a file copy trick from my previous article.
So, now I will be using this Dockerfile for my projects, either new or existing!