- Containerize flow
- Publishing an image into the DockerHub
- Technical stuff about build
- Keywords into the Dockerfile
- Moving to production with multi-stage builds
Docker aims to make it easy to build, ship and run applications. We call this containerization and the process looks like this:
- Write your applications
- Define a list of all dependencies
- Create a Dockerfile that tells Docker how to build and run the app
- Build the app into an image
- Push the image to a registry (optional)
- Run a container from the image
This flow can be summarized into the following diagram:
Let's say we want to make the image we created in the first exercise public, that is, we want everyone to be able to run cowsay.
To do this, we need to use a registry. There are several, but we are going to use a free one like Docker Hub. In order to register our image in this repository we must first have an account.
Once the account has been generated, we must create a repository where the different cowsays images will be registered.
Once we have generated our repository we can go back to our original task, i.e. publish the cowsay image. To do this we need to log in to Docker Hub via docker login.
NOTE: When executing this command, we will be shown the instructions to be able to log in.
Once logged in we must include a new tag on our existing image (in case it is not displayed with the docker images command, go back to cowsay's exercise to recreate it).
Tags are necessary to publish images to a repository, as they define the account and the repository itself. In my case, if I were to push the cowsays image as is, I would publish the image to a repository called cowsay.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cowsay latest dbdf27b44e96 14 hours ago 282MBAs the repository we created is called cowsays it should work as is, however... if we do a push we will see the following:
docker push cowsay:latest
The push refers to repository [docker.io/library/cowsay]
a73d74ee7120: Waiting
1f4a3d643eb9: Waiting
155ad54a8b28: Waiting
ed39b113210b: Waiting
push access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failedThis is because the image we are posting is missing information in the tag, namely the account where it will be posted. If we go back to the previous image we can see that the account is joeyratt.
This means that we must generate a new tag over the existing image where all the information is included:
docker tag cowsay:latest joeyratt/cowsay:latestAfter this action, we can publish the image via the push command.
docker push joeyratt/cowsay:latest
The push refers to repository [docker.io/joeyratt/cowsay]
1f4a3d643eb9: Pushed
155ad54a8b28: Pushed
ed39b113210b: Pushed
a73d74ee7120: Pushed
latest: digest: sha256:dbdf27b44e960394f4a5783c1dea13c37b98618c96b7d10dc7ab8cca92e1454d size: 855After these steps we can say that we have published a Docker image to our registry. We will be able to access this registry from any computer with an internet connection.
In order to test it we are going to delete all the images from our local repository and make a run of joeyratt/cowsay:latest, let's see what happens...
# Here we are checking that there are no images in our local repo
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
# Here we are downloading our published image in docker
docker run joeyratt/cowsay:latest
Unable to find image 'joeyratt/cowsay:latest' locally
latest: Pulling from joeyratt/cowsay
Digest: sha256:d821dbf36dbb3fbf8438a7a29933955e1366bde60dbbc60683c47c4ad90e9147
Status: Downloaded newer image for joeyratt/cowsay:latest
_______________________________________
/ I hope your day is going well because \
\ mine is going phenomenal /
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||Behind the scenes, Docker's build system has a client and server:
- Client: Buildx
- Server: BuildKit
Buildx is Docker's latest and greatest build client. It's implemented as a CLI plugin and supports all the latest features of BuildKit, such as multi-stage build, multi-architecture images, advanced caching, and more.
If you point buildx at a local builder, image builds will be done on your local machine. If you point it at a remote builder, such as Docker Build Cloud, builds will be done on remote infrastructure. When you run a Docker build command, buildx interprets the command and sends the build request to the selected builder. This includes the:
- Dockerfile
- Command line arguments
- Caching options
- Export options
- Build context (app and dependency list)
The builder performs the build and exports the image, while the buildx client reports on progress. Docker uses docker-container driver that uses QEMU to emulate target hardware. It usually works but can be slow.
You can use the Docker build command to build images for multiple architectures including ones different from your local machine.
These builds can be performed locally or in the cloud. Both work with the standard docker build command and only require minimal backend configuration. Run the following command to list your current builders.
docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.20.0 linux/amd64 (+3)NOTE: Remember, a builder is an instance of BuildKit that will perform builds.
You can create more builders that serve the creation of images in concrete platforms. This can be done with the following command:
docker buildx create --driver=docker-container --name=containerThese new builders will remain dormant until they are used or enabled.
In the following command it shows how to use this new builder and specify which architecture the image should be built for:
docker buildx build --builder=container \
--platform=linux/amd64,linux/arm64 \
-t joeyratt/cowsay:latest --push .Now that you've containerized the application let's take a closer look at how some of the machinery works.
The docker build command parses the Dockerfile one file at a time, starting from the top. You can insert comments by starting a line with the # character and the builder will ignore them.
All non-comment lines are called instructions or steps and take the format <INSTRUCTION> <arguments>. Instruction names are not case-sensitive, but it's common to write them in UPPERCASE to make them easier to read.
There are two types of instructions:
- Layers creators: These include
FROM,RUNandWORKDIR - Metadata creators:
EXPOSE,ENV,CMDandENTRYPOINT
With the command history against any image you will see the instructions that created it:
docker history joeyratt/cowsay:latest
IMAGE CREATED CREATED BY SIZE COMMENT
d821dbf36dbb 15 hours ago CMD ["phenomenal"] 0B buildkit.dockerfile.v0
<missing> 15 hours ago ENTRYPOINT ["cowsay" "I hope your day is goi… 0B buildkit.dockerfile.v0
<missing> 15 hours ago ENV PATH=/usr/local/sbin:/usr/local/bin:/usr… 0B buildkit.dockerfile.v0
<missing> 15 hours ago RUN /bin/sh -c apt install -y cowsay # build… 54MB buildkit.dockerfile.v0
<missing> 15 hours ago RUN /bin/sh -c apt update # buildkit 19.6MB buildkit.dockerfile.v0
<missing> 2 weeks ago # debian.sh --arch 'amd64' out/ 'bookworm' '… 133MB debuerreotype 0.15The main keywords used in a Docker file and its use are the following:
FROM: Sets the base image for the containerRUN: Executes a command inside the image during buildCOPY: Copies files from the host into the imageEXPOSE: Declares a container's listening port (only documents it)ENTRYPOINT: Defines the main process of the containerCMD: Provides default arguments for the containerLABEL: Adds metadata to the imageENV: Sets environment variables inside the containerONBUILD: Triggers instructions only when the image is used as a baseHEALTHCHECK: Defines a command to check if the container is still running correctly
When it comes to container images... big is bad!
Because:
- It means slow
- Means more potential vulnerabilities
- Larger attack surface
At a high, multi-stage builds use a single Dockerfile with multiple FROM instructions - each FROM instruction represents a new build stage. This allows you to have a stage where you do the heavy lifting of building the app inside a large image with compilers and other build tools, but then you have another stage where you copy the compiled app into a slim image for production.
Here is a dockerfiles as an example:
FROM golang:1.22.1-alpine AS base # STAGE 0
WORKDIR /src
COPY go.mod go.sum .
RUN go mod download
COPY . .
FROM base AS build-client # STAGE 1
RUN go build -o /bin/client ./cmd/client
FROM base AS build-server # STAGE 2
RUN go build -o /bin/server ./cmd/server
FROM scratch AS prod # STAGE 3
COPY --from=build-client /bin/client /bin/
COPY --from=build-server /bin/server /bin/
ENTRYPOINT ["bin/server"]In each stage, the following is done:
- base: Builds an image with compilation tools
- build-client: Compiles the client executable
- build-server: Compiles the server executable
- prod: Copies the client and server executables into a slim image
Each stage outputs an intermediate image that later stage can use. However, Docker deletes them when the final stage is complete. The builder will run the base stage first, then run the build-client and build-server stage in parallel, and finally run the prod stage.
It will always attempt to run stages in parallel, but it can only do this when no dependencies exist. For example, the build-client and build-server stages depend on the base stage and cannot run until that stage completes (both start with FROM base...). However, the build-client and build-server can run in parallel because they don't depend on each other.
Anyway. The final production image is only 25 MB, much smaller than the 300MB+ base image pulled by the base stage to build and compile the app.

