To do anything truly worth doing, I must not stand back shivering and thinking of the cold and danger, but jump in with gusto and scramble through as well as I can.
Enough with the theory; let’s start working with Kubernetes and containers. In this chapter, you’ll build a simple containerized application and deploy it to a local Kubernetes cluster running on your machine. In the process, you’ll meet some very important cloud native technologies and concepts: Docker, Git, Go, container registries, and the
This chapter is interactive! Throughout this book, we’ll ask you to follow along with the examples by installing things on your own computer, typing commands, and running containers. We find that’s a much more effective way to learn than just having things explained in words. You can find all of the examples at https://github.com/cloudnativedevops/demo.
As we saw in Chapter 1, the container is one of the key concepts in cloud native development. The most popular tool for building and running containers is Docker. There are other tools for running containers, but we will cover that more later.
In this section, we’ll use the Docker Desktop tool to build a simple demo application, run it locally, and push the image to a container registry.
If you’re already very familiar with containers, skip straight to “Hello, Kubernetes”, where the real fun starts. If you’re curious to know what containers are and how they work, and to get a little practical experience with them before you start learning about Kubernetes, read on.
Docker Desktop is a free package for Mac and Windows. It comes with a complete Kubernetes development environment that you can use to test your applications locally on your laptop or desktop.
Let’s install Docker Desktop now and use it to run a simple containerized application. If you already have Docker installed, skip this section and go straight on to “Running a Container Image”.
Download a version of the Docker Desktop Community Edition suitable for your computer, then follow the instructions for your platform to install Docker and start it up.
Once you’ve done that, you should be able to open a terminal and run the following command:
The exact output will be different depending on your platform, but if Docker is correctly installed and running, you’ll see something like the example output shown.
On Linux systems, you may need to run
sudo docker version instead. You can add your account to the docker group with
sudo usermod -aG docker $USER && newgrp docker and then you won’t need to use
sudo each time.
Docker is actually several different, but related things: a container image format, a container runtime library that manages the lifecycle of containers, a command-line tool for packaging and running containers, and an API for container management. The details needn’t concern us here, since Kubernetes supports Docker containers as one of many components, though an important one.
What exactly is a container image? The technical details don’t really matter for our purposes, but you can think of an image as being like a ZIP file. It’s a single binary file that has a unique ID and holds everything needed to run the container.
Whether you’re running the container directly with Docker, or on a Kubernetes cluster, all you need to specify is a container image ID or URL, and the system will take care of finding, downloading, unpacking, and starting the container for you.
We’ve written a little demo application that we’ll use throughout the book to illustrate what we’re talking about. You can download and run the application using a container image we prepared earlier. Run the following command to try it out:
docker container run -p 9999:8888 --name hello cloudnatived/demo:hello
Leave this command running, and point your browser to http://localhost:9999/.
You should see a friendly message:
Any time you make a request to this URL, our demo application will be ready and waiting to greet you.
Once you’ve had as much fun as you can stand, stop the container by pressing Ctrl-C in your terminal.
So how does it work? Let’s download the source code for the demo application that runs in this container and have a look.
You’ll need Git installed for this part.1 If you’re not sure whether you already have Git, try the following command:
git version 2.32.0
If you don’t already have Git, follow the installation instructions for your platform.
git clone https://github.com/cloudnativedevops/demo.git
This Git repository contains the demo application we’ll be using throughout this book. To make it easier to see what’s going on at each stage, the repo contains each successive version of the app in a different subdirectory. The first one is named simply hello. To look at the source code, run this command:
Open the file main.go in your favorite editor (we recommend Visual Studio Code which has excellent support for Go, Docker, and Kubernetes development). You’ll see this source code:
Go is a modern programming language (developed at Google since 2009) that prioritizes simplicity, safety, and readability, and is designed for building large-scale concurrent applications, especially network services. It’s also a lot of fun to program in.2
Kubernetes itself is written in Go, as are Docker, Terraform, and many other popular open source projects. This makes Go a good choice for developing cloud native applications.
As you can see, the demo app is pretty simple, even though it implements an HTTP server (Go comes with a powerful standard library). The core of it is this function, called
As the name suggests, it handles HTTP requests. The request is passed in as an argument to the function (though the function doesn’t do anything with it, yet).
An HTTP server also needs a way to send something back to the client. The
http.ResponseWriter object enables our function to send a message back to the user to display in her browser: in this case, just the string
The first example program in any language traditionally prints
Hello, world. But because Go natively supports Unicode (the international standard for text representation), example Go programs often print
Hello, 世界 instead, just to show off. If you don’t happen to speak Chinese, that’s okay: Go does!
The rest of the program takes care of registering the
handler function as the handler for HTTP requests, and actually starting the HTTP server to listen and serve on port 8888.
That’s the whole app! It doesn’t do much yet, but we will add capabilities to it as we go on.
You know that a container image is a single file that contains everything the container needs to run, but how do you build an image in the first place? Well, to do that, you use the
docker image build command, which takes as input a special text file called a Dockerfile. The Dockerfile specifies exactly what needs to go into the container image.
One of the key benefits of containers is the ability to build on existing images to create new images. For example, you could take a container image containing the complete Ubuntu operating system, add a single file to it, and the result will be a new image.
In general, a Dockerfile has instructions for taking a starting image (a so-called base image), transforming it in some way, and saving the result as a new image.
golang:1.16-alpine AS build
/src/COPY main.go go.* /src/
0go build -o /bin/demo
=build /bin/demo /bin/demo
The exact details of how this works don’t matter for now, but it uses a fairly standard build process for Go containers called multi-stage builds. The first stage starts from an official
golang container image, which is just an operating system (in this case Alpine Linux) with the Go language environment installed. It runs the
go build command to compile the main.go file we saw earlier.
The result of this is an executable binary file named demo. The second stage takes a completely empty container image (called a scratch image, as in from scratch) and copies the demo binary into it.
Why the second build stage? Well, the Go language environment, and the rest of Alpine Linux, is really only needed in order to build the program. To run the program, all it takes is the demo binary, so the Dockerfile creates a new scratch container to put it in. The resulting image is very small (about 6 MiB)—and that’s the image that can be deployed in production.
Without the second stage, you would have ended up with a container image about 350 MiB in size, 98% of which is unnecessary and will never be executed. The smaller the container image, the faster it can be uploaded and downloaded, and the faster it will be to start up.
Minimal containers also have a reduced attack surface for security issues. The fewer programs there are in your container, the fewer potential vulnerabilities.
Because Go is a compiled language that can produce self-contained executables, it’s ideal for writing minimal (scratch) containers. By comparison, the official Ruby container image is 850 MB; about 140 times bigger than our alpine Go image, and that’s before you’ve added your Ruby program!
We’ve seen that the Dockerfile contains instructions for the
docker image build tool to turn our Go source code into an executable container. Let’s go ahead and try it. In the hello directory, run the following command:
docker image build -t myhello .
Sending build context to Docker daemon 4.096kB
Step 1/7 : FROM golang:1.16-alpine AS build
Successfully built eeb7d1c2e2b7
Successfully tagged myhello:latest
Congratulations, you just built your first container! You can see from the output that Docker performs each of the actions in the Dockerfile in sequence on the newly formed container, resulting in an image that’s ready to use.
When you build an image, by default it just gets a hexadecimal ID, which you can use to refer to it later (for example, to run it). These IDs aren’t particularly memorable or easy to type, so Docker allows you to give the image a human-readable name, using the
-t switch to
docker image build. In the previous example you named the image
myhello, so you should be able to use that name to run the image now.
Let’s see if it works:
docker container run -p 9999:8888 myhello
You’re now running your own copy of the demo application, and you can check it by browsing to the same URL as before (http://localhost:9999/).
You should see
Hello, 世界. When you’re done running this image, press Ctrl-C to stop the
docker container run command.
The demo application listens for connections on port 8888, but this is the container’s own private port 8888, not a port on your computer. In order to connect to the container’s port 8888, you need to forward a port on your local machine to that port on the container. It could be (almost) any port, including 8888, but we’ll use 9999 instead, to make it clear which is your port, and which is the container’s.
To tell Docker to forward a port, you can use the
-p switch, just as you did earlier in “Running a Container Image”:
docker container run -p HOST_PORT:CONTAINER_PORT ...
Once the container is running, any requests to
HOST_PORT on the local computer will be forwarded automatically to
CONTAINER_PORT on the container, which is how you’re able to connect to the app with your browser.
We said that you can use almost any port earlier because any port number below
1024 is considered a priviliged port, meaning that in order to use those ports your process must run as a user with special permissions, such as
root. Normal non-administrator users cannot use ports below 1024 so to avoid permission issues, we’ll stick with higher port numbers in our example.
In “Running a Container Image”, you were able to run an image just by giving its name, and Docker downloaded it for you automatically.
You might reasonably wonder where it’s downloaded from. While you can use Docker perfectly well by just building and running local images, it’s much more useful if you can push and pull images from a container registry. The registry allows you to store images and retrieve them using a unique name (like
For now, let’s stick with Docker Hub. While you can download and use any public container image from Docker Hub, to push your own images you’ll need an account (called a Docker ID). Follow the instructions at https://hub.docker.com/ to create your Docker ID.
docker login Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username: YOUR_DOCKER_ID Password: YOUR_DOCKER_PASSWORD Login Succeeded
In order to be able to push a local image to the registry, you need to name it using this format:
docker image tag myhello YOUR_DOCKER_ID/myhello
This is so that when you push the image to the registry, Docker knows which account to store it in.
docker image push YOUR_DOCKER_ID/myhello The push refers to repository [docker.io/YOUR_DOCKER_ID/myhello] b2c591f16c33: Pushed latest: digest: sha256:7ac57776e2df70d62d7285124fbff039c9152d1bdfb36c75b5933057cefe4fc7 size: 528
Now that you’ve built and pushed your first container image to a registry, you can run it using the
docker container run command, but that’s not very exciting. Let’s do something a little more adventurous and run it in Kubernetes.
There are lots of ways to get a Kubernetes cluster, and we’ll explore some of them in more detail in Chapter 3. If you already have access to a Kubernetes cluster, that’s great, and if you like you can use it for the rest of the examples in this chapter.
If not, don’t worry. Docker Desktop includes Kubernetes support (Linux users, see “Minikube” instead). To enable it, open the Docker Desktop Preferences, select the Kubernetes tab, and check Enable. See the Docker Desktop Kubernetes docs for more info.
It will take a few minutes to install and start Kubernetes. Once that’s done, you’re ready to run the demo app!
Linux users will also need to install the
kubectl tool following the instructions on the Kubernetes Documentation site
kubectl run demo --image=YOUR_DOCKER_ID/myhello --port=9999 --labels app=demo pod/demo created
Don’t worry about the details of this command for now: it’s basically the Kubernetes equivalent of the
docker container run command you used earlier in this chapter to run the demo image. If you haven’t built your own image yet, you can use ours:
Recall that you needed to forward port 9999 on your local machine to the container’s port 8888 in order to connect to it with your web browser. You’ll need to do the same thing here, using
kubectl port-forward pod/demo 9999:8888
Forwarding from 127.0.0.1:9999 -> 8888
Forwarding from [::1]:9999 -> 8888
Leave this command running and open a new terminal to carry on.
Connect to http://localhost:9999/ with your browser to see the
Hello, 世界 message.
kubectl get pods --selector app=demo
NAME READY STATUS RESTARTS AGE
demo 1/1 Running 0 9m
When the container is running and you connect to it with your browser, you’ll see this message in the terminal:
Handling connection for 9999
STATUS is not shown as
Running, there may be a problem. For example, if the status is
ImagePullBackoff, it means Kubernetes wasn’t able to find and download the image you specified. You may have made a typo in the image name; check your
kubectl run command.
If the status is
ContainerCreating, then all is well; Kubernetes is still downloading and starting the image. Just wait a few seconds and check again.
Once you are done, you’ll want to clean up your demo container:
kubectl delete pod demo
pod "demo" deleted
We’ll cover more of the Kubernetes terminology in the coming chapters, but for now you can think of a Pod as a container running in Kubernetes, similar to how you ran a Docker container on your computer.
If you don’t want to use, or can’t use, the Kubernetes support in Docker Desktop, there is an alternative: the well-loved Minikube. Like Docker Desktop, Minikube provides a single-node Kubernetes cluster that runs on your own machine (in fact, in a virtual machine, but that doesn’t matter).
To install Minikube, follow these Minikube installation instructions.
If, like us, you quickly grow impatient with wordy essays about why Kubernetes is so great, we hope you enjoyed getting to grips with some practical tasks in this chapter. If you’re an experienced Docker or Kubernetes user already, perhaps you’ll forgive the refresher course. We want to make sure that everybody feels quite comfortable with building and running containers in a basic way, and that you have a Kubernetes environment you can play and experiment with, before getting on to more advanced things.
Here’s what you should take away from this chapter:
All the source code examples (and many more) are available in the demo repository that accompanies this book.
The Docker tool lets you build containers locally, push them to or pull them from a container registry such as Docker Hub, and run container images locally on your machine.
A container image is completely specified by a Dockerfile: a text file that contains instructions about how to build the container.
Docker Desktop lets you run a small (single-node) Kubernetes cluster on your Mac or Windows machine. Minikube is another option and works on Linux.
kubectl tool is the primary way of interacting with a Kubernetes cluster, and can be used either imperatively (to run a public container image, for example, and implicitly creating the necessary Kubernetes resources), or declaratively, to apply Kubernetes configuration in the form of YAML manifests.