CNCF Buildpacks: Escaping the Copy-Pasta Game of Dockerfiles

Andrew Poydence
4 min readFeb 6, 2019

--

Docker is great. Create containers that work everywhere. It’s simple, repeatable and has amazing adoption.

However, I’ve noticed that I really don’t write new docker files… I kind of just copy the ones I already made and adjust it a little. It’s not like writing my Dockerfile is the part I’m excited about… It’s just a necessary step in releasing and deploying my service. Most the time the Dockerfile is a template depending on the language. I don’t have anything special in there.

Have you noticed yourself doing this at all? I didn’t notice it at first, but when I took the time to reflect I saw that I have a few templates I’ve copied all over the place. This is obviously bad. If I were doing this with code, I would have caught it much earlier. Yet with configuration, it took me a little longer.

Is this really a problem?

If you only have a few Dockerfiles then maybe not. However if you have several, what happens if you realize there is a problem in one of your templates? Welp, you get to go fix a bunch of files… Potentially by hand… Yuck.

So how do we do better?

I’m rarely (if ever) the first to notice a problem, so I figured I should look around and see how other developers have fixed this. First stop, CNCF. After poking around I found a project called Buildpacks. Now, I worked on Cloud Foundry for a few years, so I’m familiar with buildpacks. The basic idea is, you let the buildpacks convert source code into a container. No dockerfiles or anything, it just detects the language and framework and goes from there. Therefore, no configuration files getting duplicated all over the place.

If something has to be adjusted, then the buildpack is updated and new containers can be created without me having to mess with it.

pack CLI

To locally create a container, you’ll want to install the pack CLI. Lets go through a simple example.

First lets clone some source code (we’ll use a simple node application from knative docs):

$ git clone https://github.com/knative/docs knative-docs
$ cd knative-docs/serving/samples/helloworld-nodejs

Notice there is a Dockerfile. Lets delete it to prove the point that we don’t need it.

$ rm Dockerfile

Next, we’ll use the pack CLI to create a new container:

$ pack build helloworld-nodejs # helloworld-nodejs is the image name

Note, if you plan to push this container to docker you might change it to:

$ pack build {docker-name}/helloworld-nodejs

Now, you should see some output about that looks about like:

===> DETECTING
...
===> ANALYZING
...
===> BUILDING
...
Successfully built image helloworld-nodejs

If we want to run our container, we can do so just like normal. First lets find the container we just made:

$ docker images --format '{{.Repository}}'        
helloworld-nodejs

Now lets run it:

$ docker run -d -p 8080:8080 helloworld-nodejs
429721f7ebb4a7a342e1ec63caf03687c1b1714abf847a190c34dbf489573b3e

The container is bound to port 8080 and is ready to accept traffic:

$ curl localhost:8080
Hello World!

Boom! We built and deployed a container sans a Dockerfile .

Knative

Realistically, we don’t care about containers if we are just doing stuff locally. We want to deploy things! Also, developer workstations should NEVER be used for anything other than development. This means building containers should be done elsewhere… So can we use buildpacks with something like Knative? Absolutely!

We’re going to follow the Knative build sample where we build a container and then deploy it. We’re going to modify the service.yaml however to utilize buildpacks to build the container.

First we need to create the buildpack template (more information can be found here):

$ kubectl apply -f https://raw.githubusercontent.com/knative/build-templates/master/buildpack/buildpack.yaml

Make sure to follow the instructions in the Knative build sample to enable Knative to push containers to the docker registry.

Lets adjust the service.yaml to look like:

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
name: app-from-source
namespace: default
spec:
runLatest:
configuration:
build:
apiVersion: build.knative.dev/v1alpha1
kind: Build
spec:
serviceAccountName: build-bot
source:
git:
url: https://github.com/poy/knative-testing.git
revision: nodejs
template:
name: buildpack
arguments:
- name: IMAGE
value: docker.io/{username}/app-from-source:latest
revisionTemplate:
spec:
container:
image: docker.io/andrewpoydence/app-from-source:latest
imagePullPolicy: Always
env:
- name: SIMPLE_MSG
value: "Hello from the sample app!"

Note that I used a different repo than before. The buildpacks don’t yet support source code that is not at the root of the repo.

Finally, lets apply the service:

$ kubectl apply --filename service.yaml

At this point we can watch our pods and wait for app-from-source to become ready and then we can curl it:

$ curl -H "Host: app-from-source.default.example.com" http://{IP_ADDRESS}Hello World!

There you have it! Knative downloaded our source code and used buildpacks to create a container. It then uploaded it to docker so it could serve the container. All without a Dockerfile!

Conclusion

Dockerfiles aren’t a problem by any means… However I noticed that I was copying the same ones around everywhere. This made me feel uncomfortable as I started to speculate what it would be like if I had to go adjust them. Buildpacks offer a solution to this and enable a developer to care a little less about how the container is created. My container behaved the same as it would have if I had hand rolled the Dockerfile, so I feel like this saves me time.

The nice thing about Knative is I can use buildpacks for most of my services and then switch to a Dockerfile if I were to have a special case that required it.

--

--

Andrew Poydence

#Cloud Developer at @Google. I am love with writing #Go, exploring #GCP and the cloud in general. Opinions stated here are my own.