Contents
Introduction
In a production environment, Docker makes it easy to build, deploy, and run applications inside containers. Containers allow developers to bundle applications and all of their essential needs and dependencies into a single package that you can turn into a Docker image and replicate. Docker images are generated from Dockerfiles. The Dockerfile is a file in which you define the image, the base operating system it will have, and the commands to run.
Large Docker images can increase the time it takes to create and send images between clusters and cloud providers. If, for example, you have a gigabyte-sized image to apply every time one of your developers triggers a build, the throughput you create on your network builds up during the CI / CD process, makes your application slow and ultimately costs you resources. . For this reason, Docker images suitable for production should contain only the bare essentials.
There are several ways to reduce the size of Docker images in order to optimize production. First of all, these images usually don’t need any developer tools to run their apps, so it doesn’t need to add them at all. Using a multi-stage build process, you can use intermediate images to compile and generate the code, install dependencies, and integrate everything into the smallest size possible, then copy the final version of your app to an empty image without generation tools. Additionally, you can use an image with a tiny base, such as Alpine Linux. Alpine is a Linux distribution suitable for production because it only has the bare essentials for your application to function.
In this tutorial, you’ll optimize Docker images in a few simple steps, making them smaller, faster, and better suited for production. You’ll build images for a sample Go API in several different Docker containers, starting with Ubuntu and language-specific images, and then moving to Alpine. Distribution. You’ll also use multistage versions to optimize your images for production. The end goal of this tutorial is to show the difference in size between using default Ubuntu images and optimized counterparts, as well as showing the benefit of multi-stage builds. After reading this tutorial, you will be able to apply these techniques to your own CI / CD projects and pipelines.
Preconditions
Before you begin, you will need:
- An Ubuntu 18.04 server with a non-root user account with privileges
+ sudo +
. Follow our tutorial https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04 for initial server setup with Ubuntu 18.04]. Although this tutorial has been tested on Ubuntu 18.04, you can follow most of the steps in any Linux distribution. - Docker installed on your server. Follow steps 1 and 2 of How to Install and Use Docker on Ubuntu 18.04 for installation instructions.
Step 1 – Downloading the Go API Sample
Before optimizing your Docker image, you must first download the sample API from which you will build your Docker images. Using a simple Go API will walk you through all the key steps in building and running an app in a Docker container. This tutorial uses Go because it is a compiled language such as C ++ or Java, but unlike them has a very small footprint.
On your server, start by cloning the example Go API:
git clone https://github.com/do-community/mux-go-api.git
Once the project is cloned, you will have a named directory + mux-go-api w
on your server. Move into this directory with + cd +
:
cd mux-go-api
This will be the base directory for your project. You will build your Docker images from this directory. Inside you’ll find the source code for an API written with Go in the file + api.go +
. While this API is minimal and has only a few endpoints, it will be fine to simulate a production-ready API for the purposes of this tutorial.
Now that you’ve downloaded the sample Go API, you’re ready to build an Ubuntu Docker base image, against which you can compare the most recent optimized Docker images.
Step 2 – Build an Ubuntu Base Image
For your first Docker image, it will be helpful to see what it looks like when you start with an Ubuntu base image. This will condition your sample API in an environment similar to the software you are already running on your Ubuntu server. In the image, you will install the various packages and modules required to run your application. You will find, however, that this process creates a rather heavy Ubuntu image which will affect the build time and code readability of your Docker file.
Start by writing a Docker file that tells Docker to build an Ubuntu image, install Go, and run the sample API. Make sure to create the Docker file in the directory of the cloned repository. If you cloned to the home directory, it should be “+ $HOME/mux-go-api“.
Create a new file named + Dockerfile.ubuntu
. Open it in + nano +
or in your favorite text editor:
nano ~/mux-go-api/Dockerfile.ubuntu
In this Docker file, you define an Ubuntu image and install Golang. Next, you will install the necessary dependencies and build the binary. Add the following content to + Dockerfile.ubuntu
:
~/mux-go-api/Dockerfile.ubuntu
FROM ubuntu:18.04
RUN apt-get update -y \
&& apt-get install -y git gcc make golang-
ENV GOROOT /usr/lib/go-
ENV PATH $GOROOT/bin:$PATH
ENV GOPATH /root/go
ENV APIPATH /root/go/src/api
WORKDIR $APIPATH
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]
Starting at the top, the command + FROM +
specifies the base operating system for the image. Then, the command + RUN +
installs the Go language when creating the image. + ENV +
sets specific environment variables that the Go compiler needs to function properly. + WORKDIR +
specifies the directory we want to copy the code to, and the command + COPY +
takes the code from the directory where it is located + Dockerfile.ubuntu +
and copies it to the image. The last command + RUN
installs the dependencies necessary for the source code to compile and run the API.
Save and exit the file. You can now run the command + build +
to create a Docker image from the Docker file you just created:
docker build -f Dockerfile.ubuntu -t ubuntu .
The command + build +
builds an image from a Docker file. The flag + -f +
indicates that you want to build from the file + Dockerfile.ubuntu +
, while + -t +
means tag, which means that you are tagging it with the name + ubuntu +
. The last point represents the current context where is located + Dockerfile.ubuntu +
.
It will take a while, so feel free to take a break. Once the build is complete, you will have an Ubuntu image ready to run your API. But the final image size might not be ideal; anything over a few hundred MB for this API would be considered too large an image.
Run the following command to list all Docker images and find your Ubuntu image size:
docker images
The output displays the image you just created:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 61b2096f6871 33 seconds ago
. . .
As shown in the output, this image has a size of * 636MB * for a basic Golang API, a number that may vary slightly from machine to machine. On several versions, this large size will significantly affect deployment times and network throughput.
In this section, you’ve built an Ubuntu image with all the necessary Go tools and dependencies to run the API cloned in step 1. In the next section, you’ll use a predefined, language-specific Docker image for simplicity. your Docker file and streamline the build process.
Step 3 – Create a language-specific base image
Preset images are ordinary base images that users have edited to include tools specific to the situation. Users can then upload these images to the Docker Hub image repository, allowing other users to use the shared image instead of having to write their own Dockerfiles. This is a common process in a production situation. You can find various pre-built images on Docker Hub for almost any use case. In this step, you’ll build your sample API using a Go-specific image that has the compiler and dependencies already installed.
With predefined base images already containing the tools you need to build and run your application, you can significantly reduce build time. Since you are starting with a base that has all the necessary tools preinstalled on it, you can skip adding them to your Docker file, making it look a lot cleaner and ultimately reduce compilation time.
Go ahead and create another Docker file and name it + Dockerfile.golang +
. Open it in your text editor:
nano ~/mux-go-api/Dockerfile.golang
This file will be much more concise than the previous one as it has all of the Go-specific dependencies, tools, and compilers preinstalled.
Now add the following lines:
~/mux-go-api/Dockerfile.golang
FROM golang:
WORKDIR /go/src/api
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]
Starting at the top, you will find that the instruction + FROM +
is now + golang: +
. This means that Docker will grab a pre-built Go image from Docker Hub that has all the necessary Go tools already installed.
Now again build the Docker image with:
docker build -f Dockerfile.golang -t golang .
Check the final image size with the following command:
docker images
This will give a result similar to this:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
golang latest eaee5f524da2 40 seconds ago
. . .
While the Docker file itself is more efficient and the build time is shorter, the overall image size has actually increased. The preset Golang image is around 744MB *, which is quite a bit.
This is the preferred way to create Docker images. It gives you a base image that the community has approved as a standard to use for the specified language, in this case, Go. However, to prepare an image for production, you need to remove unnecessary elements from the application by running.
Remember, using these large images is okay when you are unsure of your needs. Feel free to use them both as disposable containers and as a base for creating other images. For development or testing purposes, when you don’t need to consider sending images over the network, you can use large images perfectly. But if you want to optimize deployments, you need to do your best to make your images as small as possible.
Now that you’ve tested a language-specific image, you can move on to the next step, which is to use the Alpine Linux lightweight distribution as the base image to lighten your Docker image.
Step 4 – Build Basic Alpine Images
One of the easiest steps to optimize your Docker images is to use smaller base images. Alpine is a lightweight Linux distribution designed for security and resource efficiency. The Alpine Docker image uses musl libc and BusyBox to keep it compact, requiring no more than 8MB in a container to run. . The small size is due to the fact that the binary packages have been upgraded and split, giving you more control over what you install, helping to keep the environment as small and efficient as possible.
The process of creating an Alpine image is similar to creating the Ubuntu image in step 2. First, create a new file named + Dockerfile.alpine +
:
nano ~/mux-go-api/Dockerfile.alpine
Now add this snippet:
~/mux-go-api/Dockerfile.alpine
FROM alpine:
RUN apk add --no-cache \
ca-certificates \
git \
gcc \
musl-dev \
openssl \
go
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
ENV APIPATH $GOPATH/src/api
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" "$APIPATH" && chmod -R 777 "$GOPATH"
WORKDIR $APIPATH
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]
Here you add the command + apk add +
to use Alpine’s package manager to install Go and all the libraries it needs. As with the Ubuntu image, you also need to set the environment variables.
Go ahead and build the image:
docker build -f Dockerfile.alpine -t alpine .
Again, check the size of the image:
docker images
You will receive output similar to this:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest ee35a601158d 30 seconds ago
. . .
The size has come down to around * 426MB *.
The small size of the Alpine base image reduced the size of the final image, but you can do a few more operations to reduce it further.
Then try to use a pre-built Alpine image for Go. This will shorten the Dockerfile and also reduce the size of the final image. Since the pre-built Alpine image for Go is built with Go compiled from the source, its footprint is significantly reduced.
Start by creating a new file called ++
:
nano ~/mux-go-api/Dockerfile.golang-alpine
Add the following content to the file:
~/mux-go-api/Dockerfile.golang-alpine
FROM golang:
RUN apk add --no-cache --update git
WORKDIR /go/src/api
COPY . .
RUN go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]
The only differences between + Dockerfile.golang-alpine +
and + + Dockerfile.alpine +
are the order + FROM +
and the first order + RUN +
. Now the command + FROM +
specifies an image + golang +
with the tag ++
, and + RUN +
only has a Git install command. You need Git for the command to + go get`
work in the second au bas de
` + RUN + + Dockerfile.golang-alpine` command.
Build the image with the following command:
docker build -f Dockerfile.golang-alpine -t golang-alpine .
Get your list of images:
docker images
You will receive the following output:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
golang-alpine latest 97103a8b912b 49 seconds ago
Now the image size is reduced to approximately * 288MB *.
Although you have managed to reduce the size considerably, there is one more thing you need to do to get the image ready for production. This is called a multi-stage construction. By using multistage versions, you can use one image to build the application, while using another, lighter image to package the compiled application for production, a process you will perform in the next step.
Step 5 – Exclude build tools with a multistep build
Ideally, the images you run in production should not have a build tool or redundant dependencies for running the production application. You can remove them from the final Docker image using multi-step builds. It works by building the binary, or in other words, the compiled Go app, in an intermediate container, and then copying it to an empty container that doesn’t have any unnecessary dependencies.
Start by creating another file called ++
:
nano ~/mux-go-api/Dockerfile.multistage
What you will add here will be familiar to you. Start by adding the exact same code as with + Dockerfile.golang-alpine +
. But this time also add a second image to which you will copy the binary from the first image.
~/mux-go-api/Dockerfile.multistage
FROM golang:1.10-alpine3.8
RUN apk add --no-cache --update git
WORKDIR /go/src/api
COPY . .
RUN go get -d -v \
&& go install -v \
&& go build
##
FROM alpine:3.8
COPY /go/bin/api /go/bin/
EXPOSE 3000
CMD ["/go/bin/api"]
Save and close the file. Here you have two commands + FROM +
. The first is identical to + Dockerfile.golang-alpine +
, except for the presence of an + AS à plusieurs étages +
in the command + FROM +
. This will give it the name + multiétage +
, which you will then reference at the bottom of the file + Dockerfile.multistage +
. In the second command + FROM
, you will use an `alpine ‘image de base et + + COPY +
on the Go app compiled from the image + multiétage
. This process will further reduce the size of the final image, thus preparing it for production.
Run the build with the following command:
docker build -f Dockerfile.multistage -t prod .
Check the image size now, after using a multi-step build.
docker images
You will find two new images instead of just one:
OutputREPOSITORY TAG IMAGE ID CREATED SIZE
prod latest 82fc005abc40 38 seconds ago
<none> <none> d7855c8f8280 38 seconds ago
. . .
The image + <none> +
is the image + plusieurs étages +
built with the command + FROM golang: 1.10-alpine3.8 +
. It is only an intermediary used to build and compile the Go application, while the image + prod +
in this context is the final image which only contains the compiled Go application.
Starting at 744MB *, you have now reduced the image size to approximately * 11.3MB *. Keeping track of an image as small as this and sending it over the network to your production servers will be much easier than with an image larger than 700MB and will save you significant resources in the long run. term.
Conclusion
In this tutorial, you optimized Docker images for production by using different base Docker images and an intermediate image to compile and generate the code. That way, you’ve packaged your sample API in the smallest size possible. You can use these techniques to improve the speed of building and deploying your Docker applications and any CI / CD pipelines you may have.
Discussion about this post