Our First Image: A Sample Build Environment
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.
git clone https://github.com/inLeagueLLC/simple-docker-build.git
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 buildcommands will create a temporary Docker container based on the previous image (specified in the
FROMdirective 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.
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:
Every Dockerfile starts with a
FROMdirective 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,
You'll often see image tags used to specify individual versions of applications, e.g.
mysql:8.0. If no tag is specified, Docker will use the tag
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.
ARGis similar to
ENVthat 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
ARGenvironment variables are only set during the build process, while
ENVsets environment variables anytime a container based on this image is created. This particular
ARGis 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
aptarchives before they finish, as ours will, too);
apt-get install -y nanowill install the nano text editor without prompting us to confirm; and
rm -rf /var/lib/apt/lists/*will delete the
aptarchives 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
nanohas been installed.
For readability, these chained commands are usually written as some variation of the following:
RUN doSomeCommand &&
\ doSecondCommand &&
\ doThirdCommand &&
Commandbox is "warmed up" when it has retrieved and installed the relevant CF Engine
.WARfile. 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
.lexfiles 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_EXTENSIONSenvironment variable as follows:
ENV LUCEE_EXTENSIONS "UUID;name=My Extension;version=1.2.0,UUIDForExtension2;name=My Second Extension;version=1.1.0"
As of Lucee 126.96.36.199 or 188.8.131.52, you can now specify the environment variable
LUCEE_ENABLE_WARMUP trueand 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.
CFENGINEtells 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
CFENGINEdownloaded 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
inleaguewith it below to get an image tag of
That will be "our" image, instead of
ortussolutions/commandbox. To build it, we'll run
docker buildwith the
-toption 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:
docker build -t mydockerhubusername/cfswarm-commandbox .
.in the above command tells Docker to use the current directory as the build content, so it will look for a
Dockerfilein 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:
(input Docker Hub credentials)
docker push mydockerhubusername/cfswarm-commandbox
Now our image is publicly available to anyone via
docker pull mydockerhubusername/cfswarm-commandboxor else
FROM mydockerhubusername/cfswarm-commandboxin 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 buildto the rescue! Have a look at
docker-compose.ymlin the repository you cloned:
docker-compose buildin the root repository directory will run exactly the same
docker buildcommand that we ran before. The image tag is pulled from the
imagedirective in our YAML file, and the
.(context) from the
What if we wanted to build an
:alpinetag of the same image using
We can use
docker buildand specify an alternate Dockerfile:
docker build -f Dockerfile.Alpine -t mydockerhubusername/cfswarm-commandbox:alpine .
Or we can use an alternate
docker-compose -f commandbox-alpine.yml build
And then push the resulting image up to Docker Hub:
docker push mydockerhubusername/cfswarm-commandbox:alpine
Which you use is a matter of preference, but
docker-composelets us specify
ENVs, and all kinds of other options without changing our Dockerfile.