Book Image

Docker Deep Dive - Second Edition

By : Nigel Poulton
5 (1)
Book Image

Docker Deep Dive - Second Edition

5 (1)
By: Nigel Poulton

Overview of this book

Most applications, even the funky cloud-native microservices ones, need high-performance, production-grade infrastructure to run on. Having impeccable knowledge of Docker will help you thrive in the modern cloud-first world. With this book, you will gain the skills you need in order to work with Docker and its containers. The book begins with an introduction to containers and explains their functionality and application in the real world. You will then get an overview of VMware, Kubernetes, and Docker and learn to install Docker on Windows, Mac, and Linux. Once you have understood the Ops and Dev perspective of Docker, you will be able to see the big picture and understand what Docker exactly does. The book then turns its attention to the more technical aspects, guiding you through practical exercises covering Docker engine, Docker images, and Docker containers. You will learn techniques for containerizing an app, deploying apps with Docker Compose, and managing cloud-native applications with Swarm. You will also build Docker networks and Docker overlay networks and handle applications that write persistent data. Finally, you will deploy apps with Docker stacks and secure your Docker environment. By the end of this book, you will be well-versed in Docker and containers and have developed the skills to create, deploy, and run applications on the cloud.
Table of Contents (2 chapters)

4: The big picture

The aim of this chapter is to paint a quick big-picture of what Docker is all about before we dive in deeper in later chapters.

We’ll break this chapter into two:

  • The Ops perspective
  • The Dev perspective

In the Ops Perspective section, we’ll download an image, start a new container, log in to the new container, run a command inside of it, and then destroy it.

In the Dev Perspective section, we’ll focus more on the app. We’ll clone some app code from GitHub, inspect a Dockerfile, containerize the app, run it as a container.

These two sections will give you a good idea of what Docker is all about and how the major components fit together. It’s recommended that you read both sections to get the dev and the ops perspectives. DevOps anyone?

Don’t worry if some of the stuff we do here is totally new to you. We’re not trying to make you an expert in this chapter. This is about giving you a feel of things — setting you up so that when we get into the details in later chapters, you have an idea of how the pieces fit together.

If you want to follow along, all you need is a single Docker host with an internet connection. I recommend Docker Desktop for your Mac or Windows PC. However, the examples will work anywhere that you have Docker installed. We’ll be showing examples using Linux containers and Windows containers.

If you can’t install software and don’t have access to a public cloud, another great way to get Docker is Play With Docker (PWD). This is a web-based Docker playground that you can use for free. Just point your web browser to https://labs.play-with-docker.com/ and you’re ready to go (you’ll need a Docker Hub or GitHub account to be able to login).

As we progress through the chapter, we may use the terms “Docker host” and “Docker node” interchangeably. Both refer to the system that you are running Docker on.

The Ops Perspective

When you install Docker, you get two major components:

  • The Docker client
  • The Docker engine (sometimes called the “Docker daemon”)

The engine implements the runtime, API and everything else required to run containers.

In a default Linux installation, the client talks to the daemon via a local IPC/Unix socket at /var/run/docker.sock. On Windows this happens via a named pipe at npipe:////./pipe/docker_engine. Once installed, you can use the docker version command to test that the client and daemon (server) are running and talking to each other.

> docker version
Client: Docker Engine - Community
 Version:           24.0.0
 API version:       1.43
 Go version:        go1.20.4
 Git commit:        98fdcd7
 Built:             Mon May 15 18:48:45 2023
 OS/Arch:           linux/arm64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.0
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.4
  Git commit:       1331b8c
  Built:            Mon May 15 18:48:45 2023
  OS/Arch:          linux/arm64
  Experimental:     false
  <Snip>

If you get a response back from the Client and Server, you’re good to go.

If you are using Linux and get an error response from the Server component, make sure that Docker is up and running. Also, try the command again with sudo in front of it – sudo docker version. If it works with sudo you will need to add your user account to the local docker group, or prefix all docker commands with sudo.

Images

It’s useful to think of a Docker image as an object that contains an OS filesystem, an application, and all application dependencies. If you work in operations, it’s like a virtual machine template. A virtual machine template is essentially a stopped virtual machine. In the Docker world, an image is effectively a stopped container. If you’re a developer, you can think of an image as a class.

Run the docker images command on your Docker host.

$ docker images
REPOSITORY    TAG        IMAGE ID       CREATED       SIZE

If you are working from a freshly installed Docker host, or Play With Docker, you’ll have no images and it will look like the previous output.

Getting images onto your Docker host is called pulling. Pull the ubuntu:latest image.

$ docker pull ubuntu:latest
latest: Pulling from library/ubuntu
dfd64a3b4296: Download complete
6f8fe7bff0be: Download complete
3f5ef9003cef: Download complete
79d0ea7dc1a8: Download complete
docker.io/library/ubuntu:latest

Run the docker images command again to see the image you just pulled.

$ docker images
REPOSITORY      TAG      IMAGE ID        CREATED         SIZE
ubuntu          latest   dfd64a3b4296    1 minute ago    106MB

We’ll get into the details of where the image is stored and what’s inside of it in later chapters. For now, it’s enough to know that an image contains enough of an operating system (OS), as well as all the code and dependencies to run whatever application it’s designed for. The ubuntu image that we’ve pulled has a stripped-down version of the Ubuntu Linux filesystem and a few of the common Ubuntu utilities.

If you pull an application container, such as nginx:latest, you’ll get an image with a minimal OS as well as the code to run the app (NGINX).

It’s also worth noting that each image gets its own unique ID. When referencing images, you can refer to them using either IDs or names. If you’re working with image ID’s, it’s usually enough to type the first few characters of the ID — as long as it’s unique, Docker will know which image you mean.

Containers

Now that we have an image pulled locally we can use the docker run command to launch a container from it.

$ docker run -it ubuntu:latest /bin/bash
root@6dc20d508db0:/#

Look closely at the output from the previous command. You’ll see that the shell prompt has changed. This is because the -it flags switch your shell into the terminal of the container — your shell is now inside of the new container!

Let’s examine that docker run command.

docker run tells Docker to start a new container. The -it flags tell Docker to make the container interactive and to attach the current shell to the container’s terminal (we’ll get more specific about this in the chapter on containers). Next, the command tells Docker that we want the container to be based on the ubuntu:latest image. Finally, it tells Docker which process we want to run inside of the container – a Bash shell.

Run a ps command from inside of the container to list all running processes.

root@6dc20d508db0:/# ps -elf
F S UID    PID  PPID   NI ADDR SZ WCHAN  STIME TTY      TIME CMD
4 S root     1     0    0 -  4560 -      13:38 pts/0    00:00:00 /bin/bash
0 R root     9     1    0 -  8606 -      13:38 pts/0    00:00:00 ps -elf

There are only two processes:

  • PID 1. This is the /bin/bash process that we told the container to run with the docker run command.
  • PID 9. This is the ps -elf command/process that we ran to list the running processes.

The presence of the ps -elf process in the Linux output can be a bit misleading as it is a short-lived process that dies as soon as the ps command completes. This means the only long-running process inside of the container is the /bin/bash process.

Press Ctrl-PQ to exit the container without terminating it. This will land your shell back at the terminal of your Docker host. You can verify this by looking at your shell prompt.

Now that you are back at the shell prompt of your Docker host, run the ps command again.

Notice how many more processes are running on your Docker host compared to the container you just ran.

Pressing Ctrl-PQ from inside a container will exit you from the container without killing it. You can see all running containers on your system using the docker ps command.

$ docker ps
CONTAINER ID   IMAGE          COMMAND       CREATED  STATUS    NAMES
6dc20d508db0   ubuntu:latest  "/bin/bash"   7 mins   Up 7 min  vigilant_borg

The output shows a single running container. This is the one you created earlier and proves it’s still running. You can also see it was created 7 minutes ago and has been running for 7 minutes.

Attaching to running containers

You can attach your shell to the terminal of a running container with the docker exec command. As the container from the previous steps is still running, let’s make a new connection to it.

This example references a container called “vigilant_borg”. The name of your container will be different, so remember to substitute “vigilant_borg” with the name or ID of the container running on your Docker host.

$ docker exec -it vigilant_borg bash
root@6dc20d508db0:/#

Notice that your shell prompt has changed again. You are logged into the container again.

The format of the docker exec command is: docker exec <options> <container-name or container-id> <command/app>. We used the -it flags to attach our shell to the container’s shell. We referenced the container by name and told it to run the bash shell. We could easily have referenced the container by its hex ID.

Exit the container again by pressing Ctrl-PQ.

Your shell prompt should be back to your Docker host.

Run the docker ps command again to verify that your container is still running.

$ docker ps
CONTAINER ID   IMAGE          COMMAND       CREATED  STATUS    NAMES
6dc20d508db0   ubuntu:latest  "/bin/bash"   9 mins   Up 9 min  vigilant_borg

Stop the container and kill it using the docker stop and docker rm commands. Remember to substitute the names/IDs of your own containers.

$ docker stop vigilant_borg
vigilant_borg

It may take a few seconds for the container to gracefully stop.

$ docker rm vigilant_borg
vigilant_borg

Verify that the container was successfully deleted by running the docker ps command with the -a flag. Adding -a tells Docker to list all containers, even those in the stopped state.

$ docker ps -a
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Congratulations, you’ve just pulled a Docker image, started a container from it, attached to it, executed a command inside it, stopped it, and deleted it.

The Dev Perspective

Containers are all about the apps.

In this section, we’ll clone an app from a Git repo, inspect its Dockerfile, containerize it, and run it as a container.

The Linux app can be cloned from: https://github.com/nigelpoulton/psweb.git

Run all of the following commands from a terminal on your Docker host.

Clone the repo locally. This will pull the application code to your local Docker host ready for you to containerize it.

$ git clone https://github.com/nigelpoulton/psweb.git
Cloning into 'psweb'...
remote: Enumerating objects: 63, done.
remote: Counting objects: 100% (34/34), done.
remote: Compressing objects: 100% (22/22), done.
remote: Total 63 (delta 13), reused 25 (delta 9), pack-reused 29
Receiving objects: 100% (63/63), 13.29 KiB | 4.43 MiB/s, done.
Resolving deltas: 100% (21/21), done.

Change directory into the cloned repo’s directory and list its contents.

$ cd psweb
$ ls -l
total 40
-rw-r--r--@ 1 ubuntu ubuntu  338 24 Apr 19:29 Dockerfile
-rw-r--r--@ 1 ubuntu ubuntu  396 24 Apr 19:32 README.md
-rw-r--r--@ 1 ubuntu ubuntu  341 24 Apr 19:29 app.js
-rw-r--r--  1 ubuntu ubuntu  216 24 Apr 19:29 circle.yml
-rw-r--r--@ 1 ubuntu ubuntu  377 24 Apr 19:36 package.json
drwxr-xr-x  4 ubuntu ubuntu  128 24 Apr 19:29 test
drwxr-xr-x  3 ubuntu ubuntu   96 24 Apr 19:29 views

The app is a simple nodejs web app running some static HTML.

The Dockerfile is a plain-text document that tells Docker how to build the app and dependencies into a Docker image.

List the contents of the Dockerfile.

$ cat Dockerfile

FROM alpine
LABEL maintainer="[email protected]"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN  npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]

For now, it’s enough to know that each line represents an instruction that Docker uses to build the app into an image.

At this point we’ve pulled some application code from a remote Git repo and we’ve looked at the application’s Dockerfile that contains the instructions Docker uses to build it as an image.

Use the docker build command to create a new image using the instructions in the Dockerfile. This example creates a new Docker image called test:latest.

Be sure to run the command from within the directory containing the app code and Dockerfile.

$ docker build -t test:latest .
[+] Building 36.2s (11/11) FINISHED
 => [internal] load .dockerignore                                0.0s
 => => transferring context: 2B                                  0.0s
 => [internal] load build definition from Dockerfile             0.0s
 <Snip>
 => => naming to docker.io/library/test:latest                   0.0s
 => => unpacking to docker.io/library/test:latest                0.7s

Once the build is complete, check to make sure that the new test:latest image exists on your host.

$ docker images
REPO     TAG      IMAGE ID        CREATED         SIZE
test     latest   1ede254e072b   7 seconds ago    154MB

You have a newly-built Docker image with the app and dependencies inside.

Run a container from the image and test the app.

$ docker run -d \
  --name web1 \
  --publish 8080:8080 \
  test:latest

Open a web browser and navigate to the DNS name or IP address of the Docker host that you are running the container from, and point it to port 8080. You will see the following web page.

If you’re following along on Docker Desktop, you’ll be able to connect to localhost:8080 or 127.0.0.1:8080. If you’re following along on Play With Docker, you will be able to click the 8080 hyperlink above the terminal screen.

Figure 4.1
Figure 4.1

Well done. You’ve copied some application code from a remote Git repo, built it into a Docker image, and ran it as a container. We call this “containerizing an app”.

Chapter Summary

In the Ops section of the chapter, you downloaded a Docker image, launched a container from it, logged into the container, executed a command inside of it, and then stopped and deleted the container.

In the Dev section you containerized a simple application by pulling some source code from GitHub and building it into an image using instructions in a Dockerfile. You then ran the containerized app.

This big picture view should help you with the up-coming chapters where we’ll dig deeper into images and containers.