Skip to content

Commit 438b8af

Browse files
authored
Reworked docker docs (nhs-england-tools#155)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description Minor (but important!) changes to how docker is run, `DOCKER_IMAGE` handling, and some docs. ## Context This change wraps up a few different changes I needed to make when using the template. ### Docker context [Here](https://github.com/nhs-england-tools/repository-template/compare/alyo12-docker-docs?expand=1#diff-dddd47b45acd050e3b06771b95c68eee8ebcbfcf8a019b3f0186725a18cfcab6L33) we were `cd`ing into the `infrastructure/images/whatever` directory before running Docker. That restricts the docker context to only that directory and below. You can't `COPY ../../. .` or whatever because that's treated as a security problem. This meant that you couldn't put your application code into a container inside the `infrastructure/images` structure. That's fine if you've got a single application container, and if you're happy with it living at the root of the repo, but as soon as you've got more than one it gets a bit messy (and I've never liked things that clutter the repo root at the best of times). The change I've made gets rid of the `cd` in and out; `docker` is *always* run from the repo root. That means paths in `Dockerfile`s are relative to the root, not to themselves; same with `.dockerignore` (which I've made optional so that it doesn't break if there isn't one). ### `$DOCKER_IMAGE` variable handling Minor tweak but a good quality of life improvement here. If you specify `$DOCKER_IMAGE` to the `make docker-*` tasks but *not* `$dir` then it defaults `$dir` to `infrastructure/images/$DOCKER_IMAGE`. That means the `make` invocation, the filesystem, and the docker image list are all synchronised to the same name, by convention. ### Updated docker usage docs The example in the existing docs was specific to repackaging a third party tool, but gave no guidance about how to package your own application code. I've rewritten the `Quick start` section to show packaging a trivial python script. I'm a little worried that the docs are a bit repetitive now, but I don't think it's worth reworking right now. I've also removed the python example code. It was a good start, but in practice having the tutorial docs is better because what I want to know as a new user is *where to put my own stuff*, and the example wasn't a huge help there and in fact was a bit of a confusion. ## Type of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. --> - [x] Refactoring (non-breaking change) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would change existing functionality) - [ ] Bug fix (non-breaking change which fixes an issue) ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] I am familiar with the [contributing guidelines](../docs/CONTRIBUTING.md) - [ ] I have followed the code style of the project - [ ] I have added tests to cover my changes - [ ] I have updated the documentation accordingly - [ ] This PR is a result of pair or mob programming --- ## Sensitive Information Declaration To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including [PII (Personal Identifiable Information) / PID (Personal Identifiable Data)](https://digital.nhs.uk/data-and-information/keeping-data-safe-and-benefitting-the-public) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter. - [ ] I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.
1 parent abd2548 commit 438b8af

File tree

11 files changed

+171
-197
lines changed

11 files changed

+171
-197
lines changed

docs/developer-guides/Scripting_Docker.md

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
- [Versioning](#versioning)
1212
- [Variables](#variables)
1313
- [Platform architecture](#platform-architecture)
14+
- [`Dockerignore` file](#dockerignore-file)
1415
- [FAQ](#faq)
1516

1617
## Overview
1718

19+
This document provides instructions on how to build Docker images using our automated build process. You'll learn how to specify version tags, commit changes, and understand the build output.
20+
1821
Docker is a tool for developing, shipping and running applications inside containers for Serverless and Kubernetes-based workloads. It has grown in popularity due to its ability to address several challenges faced by engineers, like:
1922

2023
- **Consistency across environments**: One of the common challenges in software development is the "it works on my machine" problem. Docker containers ensure that applications run the same regardless of where the container is run, be it a developer's local machine, a test environment or a production server.
@@ -43,7 +46,6 @@ Here are some key features built into this repository's Docker module:
4346
- Incorporates metadata through `Dockerfile` labels for enhanced documentation and to conform to standards
4447
- Integrates a linting routine to ensure `Dockerfile` code quality
4548
- Includes an automated test suite to validate Docker scripts
46-
- Provides a ready-to-run example to demonstrate the module's functionality
4749
- Incorporates a best practice guide
4850

4951
## Key files
@@ -61,50 +63,108 @@ Here are some key features built into this repository's Docker module:
6163
- [`docker.test.sh`](../../scripts/docker/tests/docker.test.sh): Main file containing all the tests
6264
- [`Dockerfile`](../../scripts/docker/tests/Dockerfile): Image definition for the test suite
6365
- [`VERSION`](../../scripts/docker/tests/VERSION): Version patterns for the test suite
64-
- Usage example
65-
- Python-based example [`hello_world`](../../scripts/docker/examples/python) app showing a multi-staged build
66-
- A set of [make targets](https://github.com/nhs-england-tools/repository-template/blob/main/scripts/docker/docker.mk#L18) to run the example
6766

6867
## Usage
6968

7069
### Quick start
7170

72-
Run the test suite:
71+
The Repository Template assumes that you will want to build more than one docker image as part of your project. As such, we do not use a `Dockerfile` at the root of the project. Instead, each docker image that you create should go in its own folder under `infrastructure/images`. So, if your application has a docker image called `my-shiny-app`, you should create the file `infrastructure/images/my-shiny-app/Dockerfile`. Let's do that.
72+
73+
First, we need an application to package. Let's do the simplest possible thing, and create a file called `main.py` in the root of the template with a familiar command in it:
74+
75+
```python
76+
print("hello world")
77+
```
78+
79+
Run this command to make the directory:
80+
81+
```shell
82+
mkdir -p infrastructure/images/my-shiny-app
83+
```
84+
85+
Now, edit `infrastructure/images/my-shiny-app/Dockerfile` and put this into it:
86+
87+
```dockerfile
88+
FROM python
89+
90+
COPY ./main.py .
91+
92+
CMD ["python", "main.py"]
93+
```
94+
95+
Note the paths in the `COPY` command. The `Dockerfile` is stored in a subdirectory, but when `docker` runs it is executed in the root of the repository so that's where all paths are relative to. This is because you can't `COPY` from parent directories. `COPY ../../main.py .` wouldn't work.
96+
97+
The name of the folder is also significant. It should match the name of the docker image that you want to create. With that name, you can run the following `make` task to run `hadolint` over your `Dockerfile` to check for common anti-patterns:
98+
99+
```shell
100+
$ DOCKER_IMAGE=my-shiny-app make docker-lint
101+
/workdir/./infrastructure/images/my-shiny-app/Dockerfile.effective:1 DL3006 warning: Always tag the version of an image explicitly
102+
make[1]: *** [scripts/docker/docker.mk:34: _docker] Error 1
103+
make: *** [scripts/docker/docker.mk:20: docker-lint] Error 2
104+
```
105+
106+
All the provided docker `make` tasks take the `DOCKER_IMAGE` parameter.
107+
108+
`hadolint` found a problem, so let's fix that. It's complaining that we've not specified which version of the `python` docker container we want. Change the first line of the `Dockerfile` to:
109+
110+
```dockerfile
111+
FROM python:3.12-slim-bookworm
112+
```
113+
114+
Run `DOCKER_IMAGE=my-shiny-app make docker-lint` again, and you will see that it is silent.
115+
116+
Now let's actually build the image. Run the following:
117+
118+
```shell
119+
DOCKER_IMAGE=my-shiny-app make docker-build
120+
```
121+
122+
And now we can run it:
123+
124+
```shell
125+
$ DOCKER_IMAGE=my-shiny-app make docker-run
126+
hello world
127+
```
128+
129+
If you list your images, you'll see that the image name matches the directory name under `infrastructure/images`:
73130

74131
```shell
75-
$ make docker-test-suite-run
132+
$ docker image ls
133+
REPOSITORY TAG IMAGE ID CREATED SIZE
134+
localhost/my-shiny-app latest 6a0adeb5348c 2 hours ago 135 MB
135+
docker.io/library/python 3.12-slim-bookworm d9f1825e4d49 5 weeks ago 135 MB
136+
localhost/hadolint/hadolint 2.12.0-alpine 19b38dcec411 16 months ago 8.3 MB
137+
```
76138

77-
test-docker-build PASS
78-
test-docker-test PASS
79-
test-docker-run PASS
80-
test-docker-clean PASS
139+
Your process might want to add specific tag formats so you can identify docker images by date-stamps, or git hashes. The Repository Template supports that with a `VERSION` file. Create a new file called `infrastructure/images/my-shiny-app/VERSION`, and put the following into it:
140+
141+
```text
142+
${yyyy}${mm}${dd}-${hash}
81143
```
82144

83-
Run the example:
145+
Now, run the `docker-build` command again, and towards the end of the output you will see something that looks like this:
84146

85147
```shell
86-
$ make docker-example-build
148+
Successfully tagged localhost/my-shiny-app:20240314-07ee679
149+
```
87150

88-
#0 building with "desktop-linux" instance using docker driver
89-
...
90-
#12 DONE 0.0s
151+
Obviously the specific values will be different for you. See the Versioning section below for more on this.
91152

92-
$ make docker-example-run
153+
It is usually the case that there is a specific image that you will most often want to build, run, and deploy. You should edit the root-level `Makefile` to document this and to provide shortcuts. Edit `Makefile`, and change the `build` task to look like this:
93154

94-
* Serving Flask app 'app'
95-
* Debug mode: off
96-
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
97-
* Running on all addresses (0.0.0.0)
98-
* Running on http://127.0.0.1:8000
99-
* Running on http://172.17.0.2:8000
100-
Press CTRL+C to quit
155+
```make
156+
build: # Build the project artefact @Pipeline
157+
DOCKER_IMAGE=my-shiny-app
158+
make docker-build
101159
```
102160

161+
Now when you run `make build`, it will do the right thing. Keeping this convention consistent across projects means that new starters can be on-boarded quickly, without needing to learn a new set of conventions each time.
162+
103163
### Your image implementation
104164

105-
Always follow [Docker best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) while developing images. Start with creating your container definition for the service and store it in the `infrastructure/images` directory.
165+
Always follow [Docker best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) while developing images.
106166

107-
Here is a step-by-step guide:
167+
Here is a step-by-step guide for an image which packages a third-party tool. It is mostly similar to the example above, but demonstrates the `.tool-versions` mechanism.
108168

109169
1. Create `infrastructure/images/cypress/Dockerfile`
110170

@@ -212,6 +272,10 @@ Set the `docker_image` or `DOCKER_IMAGE` variable for your image. Alternatively,
212272

213273
For cross-platform image support, the `--platform linux/amd64` flag is used to build Docker images, enabling containers to run without any changes on both `amd64` and `arm64` architectures (via emulation).
214274

275+
### `Dockerignore` file
276+
277+
If you need to exclude files from a `COPY` command, put a [`Dockerfile.dockerignore`](https://docs.docker.com/build/building/context/#filename-and-location) file next to the relevant `Dockerfile`. They do not live in the root directory. Any paths within `Dockerfile.dockerignore` must be relative to the repository root.
278+
215279
## FAQ
216280

217281
1. _We built our serverless workloads based on AWS Lambda and package them as `.zip` archives. Why do we need Docker?_

scripts/config/markdownlint.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# SEE: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
22

3+
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md010.md
4+
MD010: # no-hard-tabs
5+
ignore_code_languages:
6+
- make
7+
- console
8+
39
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
410
MD013: false
511

scripts/docker/docker.lib.sh

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ function docker-build() {
2727

2828
version-create-effective-file
2929
_create-effective-dockerfile
30-
# The current directory must be changed for the image build script to access
31-
# assets that need to be copied
32-
current_dir=$(pwd)
33-
cd "$dir"
30+
31+
tag=$(_get-effective-tag)
32+
3433
docker build \
3534
--progress=plain \
3635
--platform linux/amd64 \
@@ -43,16 +42,36 @@ function docker-build() {
4342
--build-arg GIT_COMMIT_HASH="$(git rev-parse --short HEAD)" \
4443
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S%z")" \
4544
--build-arg BUILD_VERSION="$(_get-effective-version)" \
46-
--tag "${DOCKER_IMAGE}:$(_get-effective-version)" \
45+
--tag "${tag}" \
4746
--rm \
4847
--file "${dir}/Dockerfile.effective" \
4948
.
50-
cd "$current_dir"
49+
5150
# Tag the image with all the stated versions, see the documentation for more details
5251
for version in $(_get-all-effective-versions) latest; do
53-
docker tag "${DOCKER_IMAGE}:$(_get-effective-version)" "${DOCKER_IMAGE}:${version}"
52+
if [ ! -z "$version" ]; then
53+
docker tag "${tag}" "${DOCKER_IMAGE}:${version}"
54+
fi
5455
done
55-
docker rmi --force "$(docker images | grep "<none>" | awk '{print $3}')" 2> /dev/null ||:
56+
}
57+
58+
# Create the Dockerfile.effective file to bake in version info
59+
# Arguments (provided as environment variables):
60+
# dir=[path to the Dockerfile to use, default is '.']
61+
function docker-bake-dockerfile() {
62+
63+
local dir=${dir:-$PWD}
64+
65+
version-create-effective-file
66+
_create-effective-dockerfile
67+
}
68+
69+
# Run hadolint over the generated Dockerfile.
70+
# Arguments (provided as environment variables):
71+
# dir=[path to the image directory where the Dockerfile.effective is located, default is '.']
72+
function docker-lint() {
73+
local dir=${dir:-$PWD}
74+
file=${dir}/Dockerfile.effective ./scripts/docker/dockerfile-linter.sh
5675
}
5776

5877
# Check test Docker image.
@@ -81,12 +100,13 @@ function docker-check-test() {
81100
function docker-run() {
82101

83102
local dir=${dir:-$PWD}
103+
local tag=$(dir="$dir" _get-effective-tag)
84104

85105
# shellcheck disable=SC2086
86106
docker run --rm --platform linux/amd64 \
87107
${args:-} \
88-
"${DOCKER_IMAGE}:$(dir="$dir" _get-effective-version)" \
89-
${cmd:-}
108+
"${tag}" \
109+
${DOCKER_CMD:-}
90110
}
91111

92112
# Push Docker image.
@@ -114,7 +134,8 @@ function docker-clean() {
114134
done
115135
rm -f \
116136
.version \
117-
Dockerfile.effective
137+
Dockerfile.effective \
138+
Dockerfile.effective.dockerignore
118139
}
119140

120141
# Create effective version from the VERSION file.
@@ -207,6 +228,13 @@ function _create-effective-dockerfile() {
207228

208229
local dir=${dir:-$PWD}
209230

231+
# If it exists, we need to copy the .dockerignore file to match the prefix of the
232+
# Dockerfile.effective file, otherwise docker won't use it.
233+
# See https://docs.docker.com/build/building/context/#filename-and-location
234+
# If using podman, this requires v5.0.0 or later.
235+
if [ -f "${dir}/Dockerfile.dockerignore" ]; then
236+
cp "${dir}/Dockerfile.dockerignore" "${dir}/Dockerfile.effective.dockerignore"
237+
fi
210238
cp "${dir}/Dockerfile" "${dir}/Dockerfile.effective"
211239
_replace-image-latest-by-specific-version
212240
_append-metadata
@@ -276,6 +304,20 @@ function _get-effective-version() {
276304
head -n 1 "${dir}/.version" 2> /dev/null ||:
277305
}
278306

307+
# Print the effective tag for the image with the version. If you don't have a VERSION file
308+
# then the tag will be just the image name. Otherwise it will be the image name with the version.
309+
# Arguments (provided as environment variables):
310+
# dir=[path to the image directory where the Dockerfile is located, default is '.']
311+
function _get-effective-tag() {
312+
313+
local tag=$DOCKER_IMAGE
314+
version=$(_get-effective-version)
315+
if [ ! -z "$version" ]; then
316+
tag="${tag}:${version}"
317+
fi
318+
echo "$tag"
319+
}
320+
279321
# Print all Docker image versions.
280322
# Arguments (provided as environment variables):
281323
# dir=[path to the image directory where the Dockerfile is located, default is '.']

scripts/docker/docker.mk

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,39 @@
44
# Custom implementation - implementation of a make target should not exceed 5 lines of effective code.
55
# In most cases there should be no need to modify the existing make targets.
66

7-
docker-build: # Build Docker image - optional: docker_dir|dir=[path to the Dockerfile to use, default is '.'] @Development
8-
make _docker cmd="build" \
7+
DOCKER_IMAGE ?= $(or ${docker_image}, $(or ${IMAGE}, $(or ${image}, ghcr.io/org/repo)))
8+
DOCKER_TITLE ?= $(or "${docker_title}", $(or "${TITLE}", $(or "${title}", "Service Docker image")))
9+
10+
docker-bake-dockerfile: # Create Dockerfile.effective - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development
11+
make _docker cmd="bake-dockerfile" \
912
dir=$(or ${docker_dir}, ${dir})
10-
file=$(or ${docker_dir}, ${dir})/Dockerfile.effective
11-
scripts/docker/dockerfile-linter.sh
13+
14+
docker-build: # Build Docker image - optional: docker_dir|dir=[path to the Dockerfile to use, default is '.'] @Development
15+
dir=$(or ${docker_dir}, ${dir})
16+
make _docker cmd="build"
17+
docker-build: docker-lint
18+
19+
docker-lint: # Run hadolint over the Dockerfile - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development
20+
dir=$(or ${docker_dir}, ${dir})
21+
make _docker cmd="lint"
22+
docker-lint: docker-bake-dockerfile
1223

1324
docker-push: # Push Docker image - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development
1425
make _docker cmd="push" \
1526
dir=$(or ${docker_dir}, ${dir})
1627

28+
docker-run: # Run Docker image - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Development
29+
make _docker cmd="run" \
30+
dir=$(or ${docker_dir}, ${dir})
31+
1732
clean:: # Remove Docker resources (docker) - optional: docker_dir|dir=[path to the image directory where the Dockerfile is located, default is '.'] @Operations
1833
make _docker cmd="clean" \
1934
dir=$(or ${docker_dir}, ${dir})
2035

2136
_docker: # Docker command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to the image directory where the Dockerfile is located, relative to the project's top-level directory, default is '.']
2237
# 'DOCKER_IMAGE' and 'DOCKER_TITLE' are passed to the functions as environment variables
23-
DOCKER_IMAGE=$(or ${DOCKER_IMAGE}, $(or ${docker_image}, $(or ${IMAGE}, $(or ${image}, ghcr.io/org/repo))))
24-
DOCKER_TITLE=$(or "${DOCKER_TITLE}", $(or "${docker_title}", $(or "${TITLE}", $(or "${title}", "Service Docker image"))))
38+
dir=$(realpath $(or ${dir}, infrastructure/images/${DOCKER_IMAGE}))
2539
source scripts/docker/docker.lib.sh
26-
dir=$(realpath ${dir})
2740
docker-${cmd} # 'dir' is accessible by the function as environment variable
2841

2942
# ==============================================================================
@@ -40,44 +53,15 @@ docker-shellscript-lint: # Lint all Docker module shell scripts @Quality
4053
docker-test-suite-run: # Run Docker test suite @ExamplesAndTests
4154
scripts/docker/tests/docker.test.sh
4255

43-
docker-example-build: # Build Docker example @ExamplesAndTests
44-
source scripts/docker/docker.lib.sh
45-
cd scripts/docker/examples/python
46-
DOCKER_IMAGE=repository-template/docker-example-python
47-
DOCKER_TITLE="Repository Template Docker Python Example"
48-
TOOL_VERSIONS="$(shell git rev-parse --show-toplevel)/scripts/docker/examples/python/.tool-versions.example"
49-
docker-build
50-
51-
docker-example-lint: # Lint Docker example @ExamplesAndTests
52-
dockerfile=scripts/docker/examples/python/Dockerfile
53-
file=$${dockerfile} scripts/docker/dockerfile-linter.sh
54-
55-
docker-example-run: # Run Docker example @ExamplesAndTests
56-
source scripts/docker/docker.lib.sh
57-
cd scripts/docker/examples/python
58-
DOCKER_IMAGE=repository-template/docker-example-python
59-
args=" \
60-
-it \
61-
--publish 8000:8000 \
62-
"
63-
docker-run
64-
65-
docker-example-clean: # Remove Docker example resources @ExamplesAndTests
66-
source scripts/docker/docker.lib.sh
67-
cd scripts/docker/examples/python
68-
DOCKER_IMAGE=repository-template/docker-example-python
69-
docker-clean
70-
7156
# ==============================================================================
7257

7358
${VERBOSE}.SILENT: \
7459
_docker \
7560
clean \
61+
docker-bake-dockerfile \
7662
docker-build \
77-
docker-example-build \
78-
docker-example-clean \
79-
docker-example-lint \
80-
docker-example-run \
63+
docker-lint \
8164
docker-push \
65+
docker-run \
8266
docker-shellscript-lint \
8367
docker-test-suite-run \

scripts/docker/examples/python/.tool-versions.example

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)