- [Instructor] Now let's spend a little time
talking about building the Docker files
that will build your program,
and the importance of splitting out the things that need
to be there in order to compile your program
from the things that only need to be there
when the program runs.
Very often, a lot of the stuff involved in a project
is only required to compile or build it,
and not really required at runtime.
So when you're making a all-inclusive Docker image
for your program, that would have to include
all the stuff to build it and the stuff to run it.
So, let's talk about splitting those apart,
basically, adding a second Docker file to a project
that can be used to build the builder
and one that's used to build the actual thing.
This really can make things a lot smaller,
and it gives you an opportunity to pre-cache
dependencies in the build tool so that if a dependency
disappears from out there on the internet,
you can still rebuild old versions of your programs.
In fact, build tools are a huge source of complexity,
getting all the right versions of everything installed
to build some piece of software
that nobody has built in over a year
can be quite daunting, and honestly, many times impossible.
Maybe an old version of LibImageMagick
is just not available anymore.
Also, the steps required to build an old version
of a program may just not be known anymore.
They could have been forgotten.
And when programs dynamically fetch their dependencies
as they build, which is pretty common these days,
it can be really interesting to go through
and try to figure out what versions of everything
is actually running on a production system,
say, if it's encountering a bug.
This is severely complicated by systems
that allow in-place upgrades,
where version four of a program is replaced
with a new version four of a program or a library.
So, was my program built with the old version four,
or the new version four?
Docker can help capture those moments in time,
and make your builds more reproducible.
So now, let's go do this.
I'm going to head over to the exercise files
for this program for chapter two,
and I'm going to grab this file, example-webapp.zip.
This is just a simple cobbled-together web application
that I designed to be simple,
but still requires some extensive compilation
so we can show off this process.
So I'll start by unzipping it,
then I'm going to head over to the terminal and take a look.
I'm in the directory with my project now, and as you can see
we have two Docker files for this project.
Dockerfile is the one that actually builds the project,
and the second one, Dockerfile.builder,
is the one that builds the build tool
with all of the required dependencies and libraries
and tools to actually build this thing.
So I'm going to go ahead and make this
into a Git project, git init.
Then, git add everything.
Git commit -m,
getting started.
Okay, now our project has a Git history,
which we can use to tag Docker images.
The next thing we're going to need
is a place to push those images,
so I'm going to head over here to the ECS console again,
and the repository section,
and here we see the Hello World example
from the last time we were here.
I'm going to go ahead and create two new repositories.
I'll call this one Example-WebApp-Builder.
Okay, I'm going to open up another tab,
and make one to hold the final result,
and I'll call that Example-WebApp,
to differentiate it from Example WebApp Builder.
Okay, so here we can see the information
about our two new repositories.
Let's begin by heading back to the terminal
and logging into the registry so that we can use
those two new registries we just used.
I'm going to start by running
AWS ecr get-login, no-include-email
just makes the out put command a little shorter,
and I happen to be using region us-east-1.
As we can see, the output of that command
is another command to run.
So we're going to grab that command, copy,
and just paste it right back into the terminal and run it.
Login succeeded.
Okay, now let's take a look at that builder Docker file.
I have Dockerfile.builder open in my editor here.
And see, we're going to start from openjdk version eight.
This is a Docker image with the basic
Java development kits installed.
And from that image, we're going to run apt-get update,
update that image's local packet cache.
Then we're going to install the curl command,
and then use that curl command to fetch a build tool.
This is the build tool for Clojure,
which is one of my my favorite programming languages,
and a simple thing to use for web applications like this.
Then it just turns on the Execute bit
on the file that was downloaded,
and that's all that's required for this project.
Many projects are going to have much more
extensive build dependencies, and those will show up
as more commands here in the Docker file.
Let's try using it.
So back in the project directory,
I'm going to go ahead and run docker build -t,
this very long name here.
But where did that come from?
Well, back in our terminal, so we can see right here
we find the path to our new repo, Example-WebApp-Builder.
So I cut and paste it from there to here.
This little bit here, git rev-parse HEAD
is a little shell shortcut to say
go to git, get the commit hash of the current revision,
and then put it into the command right here.
In fact, if I park this for a second and just run
git rev-parse HEAD,
it just prints out the current Git hash.
So, we're going to tag the thing we build
with that registry and the current Git hash,
and the file to use for building is not Dockerfile,
which is the default, but Dockerfile.builder,
a second Dockerfile in this project.
And the project can be found in the current directory,
represented by a dot.
Now as we can see, we successfully built our builder.
Now let's push our builder to the central repository
so we can never lose it.
I'm going to run docker push, and then that same repository ID.
Now our builder is saved for all time.
And I use this example because in my professional life,
I really did have a build fail
because an old version of the Leiningen build tool
I was using disappeared.
So this example is real.
Now that we've built our builder and saved it off
where it's safe, let's use our builder to build the project.
Here we have docker run, and the --rm says
don't keep the image created by running this build.
We don't really care about the byproducts of the build,
all the temporary files and all that sort of stuff.
We just want the final result.
Then what I'm doing here is I'm saying,
I want to create a shared directory
between where I am right now in my computer,
and a directory called Work within that Docker container.
This way, when the build process builds the final result,
the final result will be left on my computer,
not left in the Docker image
that is subsequently cleaned up.
And one thing to note, if you're on a Mac
and you're using the Docker for Mac project,
this option here will result in this command
running very slowly until a bug
in the current release is fixed.
If you're listening to this years from now
then it shouldn't be an issue.
And here we have a tag of the builder we'd like to use,
and the command to run.
The command is use the Bash shell to run a command,
which is go to the work directory,
that one that we shared up here, and run the lein uberjar.
That's just the build command for this project.
Okay, now that that's done, let's take a Docker file
that we'll use to build the final product.
Now here we'll see we're starting from Java:8-alpine.
That's much, much smaller than the openjdk:8 builder,
because it doesn't include the Java compilers
or any of the other build tools.
And then, all it does is add one file
from the working directory into a fixed location,
offer up its services on Port 3000 and run that file.
And that file was built by the previous docker file,
as we can see here in our project directory.
So now that we have the actual product built,
let's go ahead and build
a runnable Docker container from it.
And again, we're going to call docker build,
but this time we're going to let it use
the default Docker file of just Dockerfile,
the one we were just looking at.
And this time, we're going to tag it
for the Docker registry Example-WebApp,
rather than Example-WebApp-Builder.
Okay.
Now that we've built our product,
let's go ahead and push the Docker image
to our shared Docker repositories
so that later in the course we can go ahead and host it.
And of course, let's see it in action.
I'm going to do docker run --rm,
so it cleans up after it's done,
and I'm going to expose port 3000.
So map port 3000 in the container to port 3000 on my laptop.
If you happen to have something else
running on port 3000, just change the number.
And here's the tag of the image we just built.
And over in the browser, we see, voila!
Our application is actually running
out of our Docker container.
Now that we have our builder built,
we can go ahead and use that builder everywhere.
It's a stable thing that we can depend on.
One interesting pattern I've noticed
is that it gets really tempting to
hand-make custom builder images,
'cause you just want to get the thing shipped,
and that sort of recovers a lot of the problems
that Docker did away with
of you don't know how they were built
and you can't reproduce them.
So do try to avoid that pattern,
of course unless you really do need
to ship that thing today, then be practical.
In this example I put the Docker file
for the builder into the project itself.
If you have several projects that use the same builder,
it can make sense to put the builder into its own project,
though that gets a little crazy
if you use the latest tag for all of them.
Then updating for one project breaks the other projects,
so at the very least, reference the builder images
by their Git hash versus something like latest,
and that will help keep things sane in the long run.