
Intro Guide to Dockerfile Best Practices - rubinelli
https://blog.docker.com/2019/07/intro-guide-to-dockerfile-best-practices/
======
itamarst
As is the case with the Docker's best practices for Dockerfiles in the
official documentation, they're leaving out some really important details.

Specifically, they don't really express how Docker packaging is a process
integrating the way you build, where you build, and how you build, not just
the Dockerfile.

1\. Caching is great... but it can also lead to insecure images because you
don't get system package updates if you're only ever building off a cached
image. Solution: rebuild once a week from scratch.
([https://pythonspeed.com/articles/docker-cache-insecure-
image...](https://pythonspeed.com/articles/docker-cache-insecure-images/))

2\. Multi-stage builds give you smaller images, but if you don't use them
right they result in breaking caching completely, destroying all the speed and
size benefits you get from layer caching. Solution: you need to tag and push
the build-stage images too, and then pull them before the build, if you want
caching to work. (Long version, this is a bit tricky to get right:
[https://pythonspeed.com/articles/faster-multi-stage-
builds/](https://pythonspeed.com/articles/faster-multi-stage-builds/))

~~~
javagram
> Solution: rebuild once a week from scratch.

I’ve noticed the official docker images don’t seem to do this. E.g. the
official “java” images seem to be uploaded and then are never changed, the
only way to get a newer underlying base system is to upgrade to a newer
version tag release. Is this true of all the official images, I wonder?

~~~
cpuguy83
This is not true. The images are rebuilt automatically when base images are
updated.

~~~
javagram
What is the source for that? When I looked into this before, I wasn't able to
find anything in the documentation stating this would happen.

Here's the official Node.JS image from a couple years ago, for example...

    
    
      $ sudo docker inspect node:6.1 | grep 'Z"'
             "Created": "2016-05-06T21:57:54.091444751Z",
                 "LastTagTime": "0001-01-01T00:00:00Z"
    

Node 6.1.0 was released on May 6 2016, it looks to me like the image was never
changed after that? And if I run `ls -lah /var/lib/dpkg/info/*.list` inside
the image, I get a modification time of May 3, 2016 on all the files... I
tried the "node:10.0" image as well and I see similar behavior.

~~~
cpuguy83
This is how the official images work.

[https://github.com/docker-library/official-
images/tree/maste...](https://github.com/docker-library/official-
images/tree/master/library)

Each image has a manifest with all the source repos, tags, what commit to pull
from, what arches are supported, etc.

As long as the tag is listed in that manifest, it is automatically rebuilt
when the base image changes.

~~~
javagram
Perhaps we’re talking past each other? When I go to
[https://github.com/docker-library/official-
images/blob/maste...](https://github.com/docker-library/official-
images/blob/master/library/node)

It only is showing the newest version of node 8.16 listed in the manifest
file. In other words, if I had an image based off node 8.15, it isn’t going to
be updated ever.

So it’s not a matter of just rebuilding regularly, if you aren’t updating your
dockerfiles to use newer language versions, you also aren’t going to get
system updates.

Edit: I think i do see your point which is that if you are completely up to
date on language versions, clearing the build cache every once in a while may
still help get a system update if an upstream image is changed in between the
release of a new language tag.

~~~
cpuguy83
Yes, and it is mostly up to the maintainer of the image on how to handle tags.
Typically minor patch releases are not kept around once the new patch is out.
May be worth filing an issue if this is problematic?

------
AnthonBerg
Use the experimental BuildKit Dockerfile frontend for much improved build time
mounting:
[https://github.com/moby/buildkit/blob/master/frontend/docker...](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md)

* You can mount build-time secrets in safely with `--mount-type=secret`, instead of passing them in. (Multistage builds do alleviate the problems with passing secrets in, but not completely.)

* Buildkit automatically parallelizes build stages. (Of course!)

* Mount apt-cache dirs in at build time with `--mount-type bind` so that you don't have to apt-get update every single time, and you don't have to clear apt-caches either.

And lots more.

Notice that this mostly involves capabilities that Docker already has to
_build time_.

~~~
pulse7
But in order to build with BuildKit you MUST BE connected to the internet
during the build... So not ready to go yet...

~~~
toniti
I assume you mean the helper for COPY/ADD is pulled from the registry. That is
not the case since Buildkit v0.5 / Docker v19.03 . If you have images you use
locally, network connectivity is not required, otherwise, BuildKit will verify
that the mutable tags have not changed in the registry (eg. it doesn't use the
string "latest" to verify the validity of cache).

~~~
pulse7
The message without internet connection is: ERROR resolve image config for
docker.io/docker/dockerfile:1.0-experimental (the most current version of
Docker Desktop for Windows - 2.0.0.3 with Docker Engine 18.09.2)

~~~
toniti
As I mentioned you need to have the images you use available locally (eg.
`docker images | grep docker/dockerfile`). In your Dockerfile you explicitly
say that you want to use docker/dockerfile:1.0-experimental from the hub as a
build frontend. If there is no such name/tag locally it needs to check the
state in the registry as `1.0-experimental` tag is updated on new releases.

------
nisa
If you use multi-stage builds be aware that COPY --from also can do a chown
and save image size - doing a RUN chown -R that is sometimes necassary to run
stuff as a regular user _duplicates_ the image size because changed metadata
equals a copy for Docker.

Also if you dare to enable user-namespaces for Docker because, well also
security - multi-stage builds fail
([https://github.com/moby/moby/issues/34645](https://github.com/moby/moby/issues/34645))

~~~
avip
COPY --chown

~~~
AnthonBerg
COPY --chown is a big improvement, but the way that COPY works differently
than a Unix cp can be an impediment. (It flattens directory structures.)

------
glckr
Tip #6 (Use official images when possible) is certainly convenient when you're
just spinning up something (I use them in local docker-composes all the time),
but it's surely opening yet another security hole when it comes to prod. We're
not lacking examples where packages are hijacked (feels like it happens
constantly on npm, rubygems had it just the other day...), and docker hub has
already had one security breach.

Perhaps worth a mention in this blogpost?

~~~
sudhirj
You can use the official images and tag them with the SHA image ID - that
should give cryptographically enforced security and reproducibility.

~~~
zoobab
Actually, if you use the SHA256 of the image as a reference for the FROM (ex:
tomcat@sha256:c34ce3c1fcc0c7431e1392cc3abd0dfe2192ffea1898d5250f199d3ac8d8720f),
and if there is no tag associated to that SHA, there is a chance that the
Docker Registry will garbage clean it. The Docker Inc garbage cleaning
frequency is not very documented.

~~~
manishyt
Hello, I am one of the engineers on Docker Hub. If the image was ever pushed
via a tag which must be the case if it was done via docker CLI then that image
is never deleted (unless the tag is deleted from Hub UI and no other tag refer
to it). This means if
sha256:c34ce3c1fcc0c7431e1392cc3abd0dfe2192ffea1898d5250f199d3ac8d8720f was
referred by latest tag which was pushed to another image sometime later then
`FROM
tomcat@sha256:c34ce3c1fcc0c7431e1392cc3abd0dfe2192ffea1898d5250f199d3ac8d8720f`
will continue to work. Apologies for not having this documented. I'll work on
getting this documented on [https://docs.docker.com/docker-
hub/](https://docs.docker.com/docker-hub/).

------
denisstepanov
For Java it’s better to use Jib Gradle/Maven plugin from Google. It produces
docker image directly and creates layers with dependencies and class files.

------
avip
I've read several "Dockerfile best practices" would-be tutorials, and this one
stands out as both correct, concise, well explained, and ordered from simple
and important to more nuanced. To the author - great job.

------
gorn
Could someone explain why tip#9 is a good idea? To me it makes more sense to
build the application in the CI pipeline and use Dockerfile only to package
the app.

The post is focused on Java apps but, for example, there is a distinction on
runtime and SDK images in .NET Core. If you want to build in Docker, you have
to pull the heavier SDK image. If you copy the built binaries to image, you
can use the runtime image. I guess there could be similar situations in other
platforms too.

Other than that, it looks like a decent guide. Thanks to the author.

~~~
tda
For me the big advantage of doing more in docker and lees in the CIT
environment is that I have less lock-in/dependency to whatever my CI provider
does. I try to reduce my CI scripts to something like

    
    
        docker build
        docker run image test 
    

All complexities of building and collecting dependencies go in dockerfiles, so
I can reproduce it locally, or anywhere else. And importantly, without messing
with any system settings/packages. No more makefiles or shell scripts that
make a ton of assumptions about your laptop that need to be set just right to
build something from source; just docker build and off you go. Such a hassle
when you need to follow pages of readme just to build something from source
(plus a lot of installed dependencies that you have to clean up afterwards)

------
sjmulder
I thought about using Docker for a reproducible build environment but, in that
context, found it problematic that every time a Dockerfile is built you may
end up with new base images and different package versions. That's hardly
reproducible.

Perhaps I'm coming at this from a wrong angle.

~~~
emj
Build a java application with RHEL-base-jdk:10 and from commit id:[HASH], that
is as reproducible as it gets. As is stated you can save all the build
dependencies in an Docker image, so you can go back and look at it later.
Usually it is enough to have the build logs though.

We set system time as well.

------
barrkel
Dockerfiles are a mostly adequate prototyping tool but are not great for
generating production builds. Lack of modularity, cascading versioning /
dependency management, reproducible builds, ... every time I've used
Dockerfiles in anger I've cobbled together another 60% of a build system out
of bash scripts.

I wish Dockerfiles would just fade away into the background, and be replaced
by something more similar to an archiver but with better integration with
repositories and versioning metadata.

~~~
itamarst
There are some people trying to build alternative toolchains for Docker builds
that remove Dockerfiles altogether, e.g.
[https://buildpacks.io/](https://buildpacks.io/) and (in a very different
approach) Nix-based builds.

My personal approach for Python applications' Docker packaging
([https://pythonspeed.com/products/pythoncontainer/](https://pythonspeed.com/products/pythoncontainer/))
was similar to yours: wrap the build process in a script. I wrote it in Python
rather than bash, so it's more maintainable, but a Ruby shop might want to
write theirs in Ruby, etc..

------
pulse7
Dockerfile = docker run + docker commit + MANY unneeded limitations

~~~
AnthonBerg
Please don't downvote parent. This is true.

It is a fact that everything that happens in a Dockerfile execution via
`docker build` can be done with a `docker run` and executing commands and
ending with `docker commit`. Even the caching mechanism can be replicated.
(Except with more control in my opinion.) It is also a fact that `docker run`
has more capabilities than `docker build`, such as being able to mount things
in.

~~~
quickthrower2
Thanks as an occasional docker user this wouldn’t have occurred to me.

------
mothsonasloth
Order of copying is especially important with Java (Maven/Gradle) builds and
NPM, it saves time.

Its also good to speed up builds with configuring them to cache artifacts in a
separate layer before the build happens or you can get them to use the host
machines cached .m2 /.npm folders as a volume, however that might not work
with pipelines etc. that build the docker containers.

------
zimbatm
COPY caching doesn't work between computers.

This was surprising to me. I thought I could `docker pull` the layers from the
registry and only re-build what had changed on my machine. But no, this
doesn't work.

The reason is that the docker client archives the source files, including all
the file attributes like uid,gid,mtime, ... Between two computers those are
bound to be different.

~~~
toniti
I recommend you to upgrade to BuildKit. Also, the above isn't really true for
Docker CLI, mtime was never taken into account in the old builder and Docker
CLI never sends uname/gname. With API it was possible in the old builder
though (hence the incompatibility with compose for example). In more practical
cases I've seen it caused by git directory instead, that isn't stable. In that
case, you can put .git into .dockerignore or if you don't use it, BuildKit
will discard it automatically.

------
zapita
I highly recommend checking out
[https://github.com/docker/buildx](https://github.com/docker/buildx) . It’s
still in tech preview but it’s an exciting look at the future of docker-build.

------
oliveralbertini
What about changing the default USER in the dockerfile?

~~~
yebyen
I think you're not the only one that mentioned this omission, I'll throw in
since it seems to be pretty poorly highlighted and has some surprising failure
modes:

When you use the USER directive to set up a working user in Dockerfile, be
explicit about UID and use the numeric UID.

What works 99% of the time is to use alpha username in the "USER" directive.
You can get some surprising artifacts if the container runtime hits this rare
bug[1]. There are likely several other great ways this can go wrong, as well.
Somewhere deep in the manual, it is suggested that you should only use a
numeric UID, even though USER accepts an alphanumeric username most of the
time, but if you look at container images built by OpenShift and other pro
docker-ers, you will see they always do this with a numeric UID.

[1]: [https://forums.docker.com/t/unable-to-find-user-root-no-
matc...](https://forums.docker.com/t/unable-to-find-user-root-no-matching-
entries-in-passwd-file/26545)

------
liveoneggs
so if you want a vi in your image (the article has a vim line) you can get one
(because they are occasionally useful) for a low cost by installing _nvi_
instead of vim. It's great and only a few K.

~~~
mkesper
You should not include binaries not needed for production for security
reasons, not because of size alone.

~~~
remram
Can you elaborate on this? If someone pwned my container and is able to
execute commands, what does it change whether they get to use a text editor?
Otherwise, what does it change whether unused programs are around?

~~~
geezerjay
I believe the whole point is that adding unnecessary software to a container
also increases the attack surface unnecessarily.

------
musicale
My personal best practice is to never use docker.

