2. Docker Basics

2.1 Concepts

  • Dockermanagement tool for the items below.
  • Image – a blueprint for building containers; self-container definition that includes everything needed to run an app.
  • Container – an independent running environment, created based on an image; multiple containers can be created based on an image.
  • Volumepersistent storage that is attached to container to prevent data loss when containers are stopped (containers have a virtual file system).

Why use Docker containers?

  • Isolation (black box) – everything is inside a docker image; your machine doesn't get polluted with libraries that are required just for one application. In the same way, no conflicts if different apps require different versions of the same library.
  • Simplicity / Consistency – your production environment will mirror your dev environment – no more mismatch versions or missing dependencies surprises.
  • Ease of use / Shared OS – all containers share the same resources (they share the OS kernel). I.e. containers don't need to be assigned or reserved specific resources in the host system.
  • Collaboration / Flexibility / Image inheritance – your image will be built on top of other predefined images making it easier to perform basic tasks (eg. Install a Node.js environment). You get to customise everything you want.

Docker uses a client-server architecture:

  • The server, called the “Docker Engine”, runs on the host machine and is responsible for the management of Docker images and containers (we’ll cover them in the next 2 chapters). The Docker Engine runs as a continuous background process (daemon) that exposes a RESTful API used by the client.
  • The client is a command-line interface (CLI) tool that allows users to interact with the Docker daemon, sending commands to build, run, and manage Docker containers and images.

2.2 Installation

Go to https://docs.docker.com/get-docker/ and download the package for your platform. Once installed, make sure to run it. You can check if everything is ok by running docker version in a terminal:

docker version
Client: Docker Engine - Community
 Version:           24.0.7
 API version:       1.43
 Go version:        go1.21.3
 Git commit:        afdd53b4e3
 Built:             Thu Oct 26 07:06:42 2023
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.27.2 (137060)
 Engine:
  Version:          25.0.3
  API version:      1.44 (minimum version 1.24)
  Go version:       go1.21.6
  Git commit:       f417435
  Built:            Tue Feb  6 21:14:22 2024
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.28
  GitCommit:        ae07eda36dd25f8a1b98dfbf587313b99c0190bb
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

2.3 Dockerise a Web App

I believe in “getting your hands dirty” as soon as possible, so, in this section, we’ll take a simple app and "dockerise" is. We will see what that process involves and use some of the concepts we’re going to explain in the next chapters in the book.

2.3.1 The App

We will be creating a simple Node.js web app using the Express library.

You will need to have Node.js installed - I’m using v.20.10.0 but any new-ish version will do. To check what version you have installed use:

node -v
v20.10.0

If you don’t have it installed, visit nodejs.org/en/download to find how to do that.

Let's create the app

  • Navigate to our Documents folder (or equivalent for Windows):
cd ~/Documents
  • Create a folder named my-app to host the app:
mkdir my-app
  • Navigate into the my-app folder:
cd my-app
  • Run npm init to create a package.json  for the app:
npm init -y
Wrote to /Users/paul/Documents/my-app/package.json:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
  • Add the express library:
npm install express

added 62 packages, and audited 63 packages in 3s
 
11 packages are looking for funding
  run `npm fund` for details
 
found 0 vulnerabilities
  • Create a file named index.js – it will be the entry point for the app (see output above):
touch index.js
  • Open the project is VS Code (or other IDE or text editor of your choice):
code .
  • Open index.js and add the following lines:
const express = require('express')
const app = express()
const port = 3000
 
app.get('/', (req, res) => {
  res.send('Hello World!')
})
 
app.listen(port, () => {
  console.log(`my-app is running on port ${port}`)
})
  • Run the app – in terminal:
node index.js
my-app is running on port 3000
NB: Any changes to the source code will require a restart of the server – tools like nodemon are available if you’d like to experiment.
  • Make sure to stop the server before proceeding to the next step.

2.3.2 Building an Image

The process a “dockerising” an app starts with creating a Dockerfile, a simple text file contains instructions to be used by Docker to package the application into an image.

Chapter 3 covers Docker images – at a high level, an image contains:

  • A stripped-down OS (only the things required to run the app)
  • The app we're building
  • 3-rd party dependencies
  • Environment variables

The Dockerfile

  • It is usually placed inside your app folder at the top level - its name, Dockerfile, is a convention and can be any valid filename.
  • Will need to include the following instructions:
    • An OS to run that included platform dependencies - Node.js in our case.
    • Installed app dependencies via npm install
    • Copy the app files across - only index.js for our app.
    • Run the start command - node index.js.

Create a file named Dockerfile, at the top of our app structure, with the following content:

FROM node:alpine

# Install app dependencies
COPY package.json .
RUN npm install

# Copy app source code
COPY index.js .

# Expose port
EXPOSE 3000

# Start Application
CMD ["node", "index.js"]

Run the following command to build an image for our project:

docker build -t my-app .
[+] Building 19.0s (10/10) FINISHED                        docker:desktop-linux
 => [internal] load .dockerignore                                          0.1s
 => => transferring context: 2B                                            0.0s
 => [internal] load build definition from Dockerfile                       0.1s
 => => transferring dockerfile: 233B                                       0.0s
 => [internal] load metadata for docker.io/library/node:alpine             2.2s
 => [auth] library/node:pull token for registry-1.docker.io                0.0s
 => [1/4] FROM docker.io/library/node:alpine@sha256:a8beafd69068c05d0918  13.5s
 => => resolve docker.io/library/node:alpine@sha256:a8beafd69068c05d09183  0.0s
 => => sha256:df05226f86d249aee174ac8d9aef6fd559a5b2aae57 1.16kB / 1.16kB  0.0s
 => => sha256:6a28ec2adefbad5b790c6c86721c8eb39795c77efb3 7.15kB / 7.15kB  0.0s
 => => sha256:bca4290a96390d7a6fc6f2f9929370d06f8dfcacba5 3.35MB / 3.35MB  0.7s
 => => sha256:1addaa6b9e454d168497e681e1f0a4852270d6e3 43.23MB / 43.23MB  12.2s
 => => sha256:50e303709daab332239871e5107eb44c5f4c317934c 2.37MB / 2.37MB  0.7s
 => => sha256:a8beafd69068c05d09183e75b9aa679b520ba68f94b 1.43kB / 1.43kB  0.0s
 => => extracting sha256:bca4290a96390d7a6fc6f2f9929370d06f8dfcacba591c76  0.1s
 => => sha256:e13d12776e46fd6868ed836ddf5a85a59e4ef440c774a24 449B / 449B  1.0s
 => => extracting sha256:1addaa6b9e454d168497e681e1f0a4852270d6e30bd9f624  1.1s
 => => extracting sha256:50e303709daab332239871e5107eb44c5f4c317934cbb47f  0.0s
 => => extracting sha256:e13d12776e46fd6868ed836ddf5a85a59e4ef440c774a244  0.0s
 => [internal] load build context                                          0.0s
 => => transferring context: 565B                                          0.0s
 => [2/4] COPY package.json .                                              0.2s
 => [3/4] RUN npm install                                                  3.0s
 => [4/4] COPY index.js .                                                  0.0s
 => exporting to image                                                     0.1s
 => => exporting layers                                                    0.1s
 => => writing image sha256:9541a3ce7b9f3402a4fa45ea02c72c0ff99e19c162571  0.0s
 => => naming to docker.io/library/my-app                                  0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/0lv3gxpjvvtzwesnuaudrstk2

Depending on your computer specs and internet connection speed, this might take a while. The newly created image is not store inside the app folder but inside the Docker library. To see it use the `docker images` command:

docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
my-app       latest    cfd4dcdda3ea   6 seconds ago   146MB

2.3.3 Running the App

docker run -d my-app
6de639cc93b450763d590d9e17d0b851b04b4faa3f53edb8f0b6d4a4c2c2f621

That's it! We have a Node.js web app running in a container. The -d flag allows the container to run in the background.

Available containers can be listed via:

docker container ls -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS      NAMES
6de639cc93b4   my-app    "docker-entrypoint.s…"   18 seconds ago   Up 18 seconds   3000/tcp   heuristic_kalam
NB: You might be tempted to load http://localhost:3000 in a browser and expect to find the app running – unfortunately, this is not possible with our current setup as the container’s port 3000 is not “exposed” to the Docker host (your computer). We’ll learn about that in the next chapters.

2.4 Docker Desktop

So far, we’ve been using the Docker CLI. A great way to visualise your images and containers is via the Docker Desktop app – launch it and select Containers area from the left menu:

Notice our Container running from the my-app image. As we didn’t specify a name when running it, a random one was assigned (heuristic_kalam in my case).

Click on it to see its details:

Selecting the Files tab will show the Containers files – notice that our index.js is at the OS top level – not the best place for it – we’ll find a different place for it later.

The Stats tab contains useful information about the resources used by the container:

That’s it! Well done!

You created a Node.js web-app, dockerise it and run it in a container.

We’ll now look in details at each of the Docker concepts we used above – let’s start with images.

But before doing that, let’s stop the container using the Stop button: