Building Blocks of Docker: Creating Custom Images for Your Applications #Docker Series Part 2

Explanation of Docker image, overview of Dockerfile, what is multi stage build, creating a custom Docker image

Yustina Yasin
6 min readMay 5, 2024

Container is an isolated process for your app’s component. Basically, it’s like separate boxes. You can run your box anywhere. For example you have the frontend component with React, backend component with Go, and database component with PostgreSQL, each get their own box to run. And these boxes keep them totally separate from everything else on your computer.

An image is the blueprint of a container. It contains all necessary resources and instruction for creating and running a container. Containers typically get their files and configurations from images. These images serves as standardized packages containing everything to run containers like files, programs, libraries, and settings. So, when you want to share the environment of a container, you simply share its corresponding container image.

This article is part of my Docker Series. Check out the other part I’ve discussed about:

  1. A beginner’s guide to containerization
  2. Optimizing your multi-container application using Docker Compose
  3. Integrating Containers into Your CI/CD Pipeline using Github Actions

Let’s jump in

  1. What is image?
  2. Overview of Dockerfile
  3. Multi stage build
  4. Building custom docker image
  5. Conclusion
  6. Recommended resources

What is image?

As I mention before, image is a standardized package that includes everything for creating and running a container. The standardization of images make containers can run across different environment. Images provide a portable and consistent way to package and distribute applications along with their dependencies.

Images have two main concepts. First, images are immutable. It means once you create images you can’t modify it. You can only create a new image or add changes on top of it. You can add changes on top of images because in the second concept images are composed of layers. Each layer of images represent file changes.

Docker only save the the changes you made and the underlying images remain the same. Image can be based from another image. The new image inherits the filesystem and configuration settings of the base image. You can then make modifications to the new image, such as installing additional software, adding files, or changing configuration settings. Docker provides lots of official images that can be used as underlying or base image. These images are created using a Dockerfile, which provides the instructions for putting together the image.

Overview of Dockerfile

Dockerfile is a document that’s used to create an image. Dockerfile consists set of instructions to define the steps needed to create the environment and configurations required for running a specific application within a container. Dockerfile typically starts with specifying a base image. This base image often contains a minimal operating system and runtime environment. From that it can includes installing required dependencies, setting environment variables, copying files, running commands, and exposed port. Some of the common syntax are:

  1. FROM <image> : specifying the base image
  2. WORKDIR <path> : create a working directory in the new image once its created where files will be copied and commands will be executed
  3. COPY <host-path> <image-path> : copy files from the host (local) into the image
  4. RUN <command>: running commands
  5. ENV <name> <value> : set environment variables that the container will be used
  6. EXPOSE <port-number> : exposed a port
  7. CMD <command> : set default commands the container will be run

Multi stage build

Multi stage build means you use multiple FROM command in your Dockerfile. Each FROM command can use different base image and it starts a new stage of build. The previous stage can be used as base image in the new stage. You are not limited to only using the previous stage, you can also use external images as a stage or base image using COPY or FROM instruction. Each stage of build can have its own instructions and dependencies. You can selectively copy files from one stage to another leaving behind everything you don’t want in the final image. The benefits of multi stage build are:

  1. reducing image size: using intermediate stages to build and compile code, you can discard unnecessary build dependencies, resulting in smaller and more efficient final images. Intermediate stages refers to the stages between initial stage and final stage.
  2. simplified build process: multi stage build simplify the build process by encapsulating all the build steps in one single Dockerfile, make it easier to manage and maintain complex build pipelines
  3. improved security: potential vulnerability and sensitive information from the build process are not included in the final image because the intermediate stages are discarded after use

Building custom docker image

# Stage 1: Build the backend app
FROM golang:1.22-alpine AS build
# Set the working directory inside the container to the root of the project
WORKDIR /src
# Mount necessary file and download it
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download
# Build the Go application
RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=.,target=. \
go build -o /bin .

# Stage 2: Create a minimal runtime container
FROM alpine:latest
# Copy the binary from the build stage
COPY --from=build /bin .
# Expose the port the application runs on
EXPOSE 8080
# Command to run the executable
CMD ["./florist-gin"]

The first build stage

Based on the Dockerfile above, I separate the build and runtime process. The first stage starts with specify the base image. I use docker official image for Go with alphine to minimize the image size. I named the first stage as build to build. Naming your build stages in a Dockerfile improves readability, documentation, debugging, reusability, and maintenance of the build process. Then, I set the working directory to /src in the container.

The third instruction I use RUN command to download the dependencies required by Go application using go mod download. I use cache for storing the downloaded dependency to speed up subsequent builds. If the dependencies specified in go.mod and go.sum since the previous build haven’t change, Docker can reuse this cache.

Instead of using COPY command, I use bind mount to mount the necessary files. A bind mount in Docker involves mounting a directory from the host machine into a container, allowing the container to access files or directories from the host’s filesystem at runtime. While COPY is used to copy files or directories from the host to the image during the build process. After downloading the dependencies, the first build stage builds the Go application using the go build command, and the resulting binary is outputted to /bin.

Bind mount offer flexibility because any changes made either in the container or in the host machine are immediately visible to each other. Although it offer flexibility, bind mount also encounter potential concerns such as security risks from exposing sensitive files or directories, making the container’s behavior dependent on the host environment, and permission issue if the ownership of those files or directories of the host doesn’t match expected by the containerized application.

I choose bind mount over copy because my host machine is a Virtual Private Server (VPS) and it’ll eliminate some concerns. My project is also a small personal project which is doesn’t involves large data. I recommend using COPY for larger and complex project. It provides better control, consistency, and reproducibility during the build process, resulting in more portable, secure, and efficient images.

The final stage

The final stage creates a minimal runtime container to run the compiled Go application. It starts from the lightweight Alpine Linux base image to keep the final image size small. Then, it copies the binary file (florist-gin) from the build stage into the root directory of the final image. After copying the binary, it exposes port 8080, which is the port the application runs on. Finally, it sets the command to run the executable (florist-gin) when the container starts.

Conclusion

Images are like blueprints for containers, containing all the necessary resources for creating and running them. Dockerfiles are documents that define steps for creating the environment and configurations needed to run an application within a container. They typically start with specifying a base image, then proceed with instructions like installing dependencies, setting environment variables, and copying files. Multi-stage builds simplify this process by encapsulating all steps in one Dockerfile, reducing image size and simplifying management.

Recommended resources

  1. Docker official images
  2. Docker verified publisher
  3. Dockerfile best practice
  4. Explanation of the connection of image layer and Dockerfile
  5. Deep down into multi stage build
  6. What is bind mount and the difference with volume

In the part three I’ll cover about optimizing Docker with Docker Compose. Stay tuned for my upcoming articles, where we’ll delve deeper into related topics and provide even more useful information!

--

--

No responses yet