Our First Image: A Sample Build Environment
Last updated
Last updated
Clone our sample Docker build environment at https://github.com/inLeagueLLC/simple-docker-build and let's look a couple ways to make our own images.
Docker images are built from Dockerfiles. From the invaluable Dockerfile Reference:
Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated build that executes several command-line instructions in succession.
Running the docker build or docker-compose build
commands will create a temporary Docker container based on the previous image (specified in the FROM
directive below) and then translate all of the other directives in the Dockerfile into configuration for the container or else commands to run inside the container. Every individual Dockerfile command creates a new image layer, and Docker will always rebuild the fewest layers possible when it can re-use "higher" layers -- that is, if you take a Dockerfile with 10 directives, build it, and then add an 11th directive to copy a file from the host onto the image, when you build it again it will only execute the 11th directive; the previous 10 layers haven't changed.
It's always a good idea to put the directives that will change most often later in your Dockerfile, and the directives that will not change very often above them; this will keep your Docker builds from re-building layers that haven't changed.
Let's have a line-by-line look at the Dockerfile and an alternate "tag" build, Dockerfile.Alpine. As with docker-compose.yml
, lines begining with #
are comments and are disregarded by Docker.
This is a common, shorthand reference for the following Docker image: docker.io/ortussolutions/commandbox
. ortussolutions is the account name and commandbox is the image name. docker.io is Docker Hub, and Docker will always use that address unless you specify a fully-qualified domain name for a different container registry (e.g. registry.mycompany.com/path/to/image
).
The term "Docker Image" usually refers to an amalgam of several images: for example, the "Ortus Commandbox Docker Image" (singular) is actually several images, each of which has its own Dockerfile. Every Docker image is built on another Docker image, which is why you'll often see Docker images visualized like this:
(source: Neo Kobo's "Docker Container" blog, March 2017)
Every Dockerfile starts with a FROM
directive that points to the image "beneath" it in the stack. This is one reason Docker images are so powerful: it's trivial to build on stable, well-established, "official" images; you may need to customize a few things here or there, but you don't need to re-invent the wheel.
Once you're comfortable building Docker images, you may find that you want several versions of the same image: perhaps you want one Commandbox image for local development and another for production, or perhaps you want to experiment with a different version of Adobe CF or Lucee without changing your existing deployments. You could simply make a whole new image, but this gets unwieldy very quickly, especially when the images may only have small differences between them. The solution is image tags. For example, the Commandbox image is based on Ubuntu Linux, but a common alternative for many official Docker images is Alpine Linux. That is what we're asking for in our alternate Dockerfile, Dockerfile.Alpine
:
FROM ortussolutions/commandbox:alpine
You'll often see image tags used to specify individual versions of applications, e.g. mysql:5.5
versus mysql:8.0
. If no tag is specified, Docker will use the tag latest
by default.
LABELs are optional metadata descriptors for your Dockerfile that will appear whenever an image based on your image is inspected via docker inspect
. You can use any label, or you can conform to the Label Schema Convention. Or you can ignore Labels entirely.
ARG
is similar to ENV
that we'll see in a moment: it sets an environment variable inside the container, in this case telling Ubuntu that the console is being run by an automated tool and to ignore some warning output it might otherwise show us. Unlike ENV
, ARG
environment variables are only set during the build process, while ENV
sets environment variables anytime a container based on this image is created. This particular ARG
is not necessary, but it makes the output of the rest of the build process a little cleaner.
It's three commands in one! Why?
Because each directive in a Dockerfile creates a new layer. A common convention in Dockerfiles is to "chain" commands that all relate to one logical stage of the build process: in this case, apt-get update
(since Docker images usually clean out apt
archives before they finish, as ours will, too); apt-get install -y nano
will install the nano text editor without prompting us to confirm; and rm -rf /var/lib/apt/lists/*
will delete the apt
archives we downloaded with apt-get update
. The resulting layer will have all of those changes processed as a single layer, such that the only difference between this layer and the previous one is that nano
has been installed.
For readability, these chained commands are usually written as some variation of the following:
Commandbox is "warmed up" when it has retrieved and installed the relevant CF Engine .WAR
file. If you're running Lucee, much of the available functionlaity has been moved out of the WAR file and into Lucee Extensions. While we could add individual .lex
files to a /deploy folder in our Docker Build process, that folder only exists after the engine itself has been warmed up; far better to tell Lucee what extensions we want during the warm-up process. As of April 2019, the easiest (if not the friendliest) means of doing so is to specify the unique ID, name, and version number of each extension in the LUCEE_EXTENSIONS
environment variable as follows:
ENV LUCEE_EXTENSIONS "UUID;name=My Extension;version=1.2.0,UUIDForExtension2;name=My Second Extension;version=1.1.0"
The extension list, unique IDs, and available versions are listed at download.lucee.org.
As of Lucee 5.2.9.38 or 5.3.3.13, you can now specify the environment variable LUCEE_ENABLE_WARMUP true
and the Lucee server will wait until all extensions and deployments are complete before exiting.
Another environment variable -- this time, one documented in the Ortus Commandbox Image README. CFENGINE
tells Commandbox which CF engine we want to deploy. It can be a Forgebox stub (as the example above is) or a URL or filesystem location resolving to a WAR file. See other examples in the comments in the Dockerfile!
This directive starts Commandbox (which is already installed by the previous, Ortus image), downloads the commandbox-fusionreactor module from Forgebox, and installs it.
This directive is the whole reason we're here: to tell Commandbox we want the CF engine defined in CFENGINE
downloaded and installed, and then to start it up and install the extensions we've defined in LUCEE_EXTENSIONS
. This process will usually take at least a couple of minutes, and when it's done, the resulting image will be quite a bit bigger than the Ortus "base" image but it will start up in seconds rather than minutes.
Take your Docker Hub account name from the previous section and replace inleague
with it below to get an image tag of mydockerhubusername/cfswarm-commandbox
:
That will be "our" image, instead of ortussolutions/commandbox
. To build it, we'll run docker build
with the -t
option to specify the image tag we want; without it, the image would be built and assigned an alphanumeric ID, but we could only refer to it by that ID. From the top level of the repository directory:
The .
in the above command tells Docker to use the current directory as the build content, so it will look for a Dockerfile
in that directory.
You'll see the temporary build container start and run through all the commands in our Dockerfile. After a few minutes, you'll get:
The image exists locally on your machine, so now let's push it up to Docker Hub. First we have to give Docker our Docker Hub credentials, and then we can push our image up:
Now our image is publicly available to anyone via docker pull mydockerhubusername/cfswarm-commandbox
or else FROM mydockerhubusername/cfswarm-commandbox
in a Dockerfile.
Even if we build our own images every couple of months, it's tedious to type out the tags every time -- especially if you're also specifying a private container registry. docker-compose build
to the rescue! Have a look at docker-compose.yml
in the repository you cloned:
docker-compose build
in the root repository directory will run exactly the same docker build
command that we ran before. The image tag is pulled from the image
directive in our YAML file, and the .
(context) from the build/context
directive.
What if we wanted to build an :alpine
tag of the same image using Dockerfile.Alpine
?
We can use docker build
and specify an alternate Dockerfile:
Or we can use an alternate .yml
file with docker-compose build
:
And then push the resulting image up to Docker Hub:
Which you use is a matter of preference, but docker-compose
lets us specify ARG
s, LABEL
s, ENV
s, and all kinds of other options without changing our Dockerfile.