Skip to content

Latest commit

 

History

History
237 lines (175 loc) · 11.2 KB

File metadata and controls

237 lines (175 loc) · 11.2 KB

🧭 HOME

⬅️ PREVIOUS CHAPTER | NEXT CHAPTER ➡️


INDEX:


Containerize flow

Docker aims to make it easy to build, ship and run applications. We call this containerization and the process looks like this:

  1. Write your applications
  2. Define a list of all dependencies
  3. Create a Dockerfile that tells Docker how to build and run the app
  4. Build the app into an image
  5. Push the image to a registry (optional)
  6. Run a container from the image

This flow can be summarized into the following diagram:

image

Publishing an image into the DockerHub

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.

image

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   282MB

As 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 failed

This 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:latest

After 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: 855

After 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 |
                ||     ||

Technical stuff about build

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=container

These 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 .

Keywords into the Dockerfile

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, RUN and WORKDIR
  • Metadata creators: EXPOSE, ENV, CMD and ENTRYPOINT

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.15

The main keywords used in a Docker file and its use are the following:

  • FROM: Sets the base image for the container
  • RUN: Executes a command inside the image during build
  • COPY: Copies files from the host into the image
  • EXPOSE: Declares a container's listening port (only documents it)
  • ENTRYPOINT: Defines the main process of the container
  • CMD: Provides default arguments for the container
  • LABEL: Adds metadata to the image
  • ENV: Sets environment variables inside the container
  • ONBUILD: Triggers instructions only when the image is used as a base
  • HEALTHCHECK: Defines a command to check if the container is still running correctly

Moving to production with multi-stage builds

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:

  1. base: Builds an image with compilation tools
  2. build-client: Compiles the client executable
  3. build-server: Compiles the server executable
  4. 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.


⬅️ PREVIOUS CHAPTER | NEXT CHAPTER ➡️