Everything you do with Docker concern containers and containers are built from images. While there are plenty of cases where prebuilt images are useful (MySql for instance) most of the time you’ll want to create your own image. You can start from scratch if you wish, but most of the time you’ll want build on top of an existing image. For example, most of the work I do starts with Microsoft’s Asp.Net Core images.
Images come from Docker registries and Docker will pull them. Once an image has been pulled it will be stored locally on your machine. If you are building a new container and the image is already on your machine Docker will use it. If the image isn’t present Docker will locate the image in a registry (Docker Hub by default) and download it.
You can manually pull images using the Docker Pull command. This will cause Docker to download an image from Docker Hub (or another configured registry) and store it locally.
docker pull jakewatkins/ancbuildenv
This will cause Docker to download my customized Asp.Net build image. If I update the image and you want the old one you can add a tag to the image name:
docker pull jakewatkins/ancbuildenv:1.0
If you don’t add the tag (the stuff after the full colon) Docker assumes you want the latest version (who wouldn’t want the latest and greatest? Cobol and FORTRAN programmers, that’s who).
Building your custom image is a little more involved. You first have to create a docker file which is a script with instructions that tells docker how to build the image. Once you have your docker file you have to actually build it.
Building your docker file is done as follows:
docker build . -t jakewatkins/example1 -f DockerFile
I jumped us forward over some boring stuff, so let me explain. The -t flag means “tag” which is the name we are giving our image. The tag I’m using starts with my docker account name and then after the slash is the actual name for the image. I could have just tagged the image as “example1” but then if I wanted to push it to a registry (Docker Hub) I would have to tag the image with my account name anyway. I’m lazy so I just go ahead and tag images the way I will push them in case I decide to push them. Less work, laziness preserved. The -f isn’t absolutely necessary if you name your doker file “DockerFile”, but I occasionally will use different names. Later I’ll explain how to do multistage builds and I will give those docker files a name like “DockerFile-selfcontained”. If you don’t provide the -f flag Docker will look for a file called “DockerFile”.
Now for the fun part, how to write your docker file. Below is an example of a typical DockerFile.
COPY ./out /app
ENTRYPOINT ["dotnet", "ExampleApp.dll"]
This is easy and you generally won’t get much more complicated than this. What does it all mean?
The hash or pound sign is for leaving comments and telling people lies.
The FROM directive tells Docker which image you are starting with to build your image. You don’t have to include a FROM, but without it you are building on a very stripped-down Linux distribution. Assume that you will have to install everything yourself if you start from scratch. Save yourself the time and start with a base image.
WORKDIR tells Docker where you are working. If the directory you specify doesn’t exist it will create it. You can think of WORKDIR as doing an mkdir and cd in to the directory you want. That directory becomes your working directory for everything that follows.
COPY will copy files from your local file system in to the image’s filesystem. If the destination directory doesn’t exist it will be created for you.
RUN will execute commands inside the image. A common RUN sequence in a docker file is
RUN apt-get update
This will update the packages already installed in the image to make sure you have the latest patches.
VOLUME allows you to specify mount points where Docker can attach persistent storage to your image. When a container is shutdown or if it crashes any changes in the container are lost. If you are running a database server in a container and you shut down the container any data in the database will be lost. Using persistent volumes gives the container a place to store data.
EXPOSE tells Docker which ports in the container should be exposed so network traffic from the host computer can be routed to the container. The -p flag in the Docker Run command on the command line not in the DockerFile) allows you to specify how to route the traffic.
ENTRYPOINT tells Docker what should be run when the container is started. In the example, we are running dotnet and telling it to use exampleapp3.dll.
There is more you can do in the DockerFile but this will get you started and covers 80% of what you need. You can ship really useful images just using the information above. Keep going though because a little more knowledge will help make your life easier (i.e. help you be as lazy as possible too).
All of that said, I do recommend spending some quality time reading the DockerFile reference and Best practices for writing DockerFiles. It is time well spent.
The previous example demonstrated a usual build for Docker. You start with an image, copy some files in, set a few configuration options and call it a day. What I don’t like about this is that you have to first compile your application on and then copy the output in to the image. What if your stuff was updated recently but the base image you are working with is using an older version? What if one of the people on your team is doing it a little differently? You’ll end up working harder trying to figure out why the application works in one environment but not another. We’re back to “it works on my machine”. We can use multistage builds to do away with that. This is one of the reasons why I have my custom build image. I added the git client to Microsoft’s image so when I build an image my Docker file actually gets the source code for Git Hub and builds that inside the Microsoft Asp.Net Core Build image and then copies the output in to the Asp.Net Core image which serves as the actual runtime image we’ll use to push to other environments. Here is an example where I’m building a sample application whose source code is pulled from Git Hub during the build process.
# stage 1 - build the solution
FROM jakewatkins/ancbuildenv AS builder
# Pull source code from Git repository
RUN git clone https://github.com/jakewatkins/ancSample.git /source
# restore the solution's packages
RUN dotnet restore
#build the solution
RUN dotnet publish --output /app/ --configuration Release
# stage 2 - build the container image
COPY --from=builder /app .
# Set the image entry point
ENTRYPOINT ["dotnet", "/app/ancSample.dll"]
You can download the entire project from my github here: https://github.com/jakewatkins/ancSample
Notice in stage 1 that the FROM statement added an AS at the end. The builder is used in the copy statement in stage 2 telling Docker where to find the files we want to copy. The other thing to notice is that there are a lot more RUN statements here. There are a few tricks that could be added to help optimize our image size. For example it would probably be good to chain the two dotnet statements together using the shell “&&” operator. However, I’m also learning this stuff so bear with me.
There are other tricks you can use in your Docker file. For example you can parameterize docker files so you can pass in parameters. I plan to refactor the above Docker file so I can pass in the version of Asp.Net Core that I want to use and the url for the Git Hub repository. That way I won’t have to write a new Docker file for each project I start.
Now that we have our image we will want to push it to a registry. I recommend that you create an account on Docker Hub to store images. The only downside of the free Docker Hub account is that you can only have 1 private registry.
To push an image to Docker Hub, you first create the registry on their web-site. The cleverly hidden blue button at on the top right will get the job done for you. Name the registry to match the tag you used to create your image. This means you need to have your account name followed by a slash and then the image name. Like this:
Image names must be all lowercase but you can use dashes and underscores to make them readable. If you didn’t tag your image during the build process you’ll have to do it now. If you gave your image the name testimage and your account name is ‘spacecommando’ and you want to name the image in the registry ‘mysuperimage’ the command will look like this:
docker tag testimage spacecommando/mysuperimage
Once you have your image tagged correctly you can push it:
docker push spacecommando/mysuperimage
You can hit refresh and see you image on Docker Hub.
How do I setup my own registry for images?
You can setup your own registry. Docker provides a container to do it. All you do is run it! However, you will want to do some configuration to setup persistent storage.
You can read about it here: Deploy a registry server
Their instructions are good and I’m too lazy to write a different version of them.
Setting up my own base images
As I’ve already stated I’ve started creating my own base images to make it easier for me to get work done. You should do the same. In my case all I did was take Microsoft’s image and add the git client to it. I can see adding other packages as well down the road (npm and bower to name a few) but for now the image is doing what I want.
The docker build file looks like this:
# Jake's ASP.NET Core Build image
# This image starts with Microsoft's ASP.NET Core Build image which already
# has the .net tools pre-loaded so projects can be built inside the image
# build process. This creates an environment where everybody builds the
# project the same way. On to this git has been added so the build process can
# pull the project source code from a git repository further decouples
# developer workstations from the build process.
# this can also allow the DockerFile to be used directly by OpenShift in a
#Add git to the image
RUN apt-get update && apt-get install -y git
That’s it. The comment header is longer than the actual build script! If you’re as lazy as me, you can grab this from my git hub here: https://github.com/jakewatkins/ancbuildenv
With this image you can setup your development work flow so that once you’re satisfied with your code, you push it to GIT and then kick off a build and run tests. In a future post we’ll use this to setup a CI/CD pipeline in different environment (I want to do OpenShift first).
I’ve covered the barest sliver of what you can do with a Docker file and Docker images. In my next post I’ll take this a step further to start building an actual application and start introducing docker-compose to orchestration containers so they can work together.