2. Docker Basics
2.1 Concepts
- Docker – management 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.
- Volume – persistent 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 apackage.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}`)
})
![](https://www.pardel.dev/content/images/2024/02/node-app.png)
- Run the app – in terminal:
node index.js
my-app is running on port 3000
- Open a browser window and navigate to http://localhost:3000/ - the “Hello World!” message will be show:
![](https://www.pardel.dev/content/images/2024/02/browser-hello-world.png)
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"]
![](https://www.pardel.dev/content/images/2024/02/dockerfile.png)
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:
![](https://www.pardel.dev/content/images/2024/02/Image-2024-02-08-at-20.18.40@2x.png)
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:
![](https://www.pardel.dev/content/images/2024/02/Image-2024-02-08-at-20.18.57@2x.png)
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.
![](https://www.pardel.dev/content/images/2024/02/Image-2024-02-08-at-20.19.15@2x.png)
The Stats
tab contains useful information about the resources used by the container:
![](https://www.pardel.dev/content/images/2024/02/Image-2024-02-08-at-20.19.28@2x.png)
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:
![](https://www.pardel.dev/content/images/2024/02/Image-2024-02-08-at-20.19.55@2x.png)