Task Overview
In this article, we will build an example Go application. Then we will optimize the build instructions to substantial reduce image size with using mount directives.
Requirements
- Installed werf dependencies on the host system.
- Installed multiwerf on the host system.
Select werf version
This command should be run prior running any werf command in your shell session:
. $(multiwerf use 1.1 stable --as-file)
Sample application
The example application is the Go Web App, written in Go.
Building
Create a gowebapp
directory and place the following werf.yaml
in the gowebapp
directory:
project: gowebapp
configVersion: 1
---
{{ $_ := set . "GoDlPath" "https://dl.google.com/go/" }}
{{ $_ := set . "GoTarball" "go1.14.7.linux-amd64.tar.gz" }}
{{ $_ := set . "GoTarballChecksum" "sha256:4a7fa60f323ee1416a4b1425aefc37ea359e9d64df19c326a58953a97ad41ea5" }}
{{ $_ := set . "BaseImage" "ubuntu:18.04" }}
image: gowebapp
from: {{ .BaseImage }}
docker:
WORKDIR: /app
ansible:
beforeInstall:
- name: Install essential utils
apt:
name: ['curl','git','tree']
update_cache: yes
- name: Download the Go tarball
get_url:
url: {{ .GoDlPath }}{{ .GoTarball }}
dest: /usr/local/src/{{ .GoTarball }}
checksum: {{ .GoTarballChecksum }}
- name: Extract the Go tarball if Go is not yet installed or not the desired version
unarchive:
src: /usr/local/src/{{ .GoTarball }}
dest: /usr/local
copy: no
install:
- name: Getting packages
shell: |
{{ include "export go vars" . | indent 6 }}
go get github.com/josephspurrier/gowebapp
setup:
- file:
path: /app
state: directory
- name: Copying config
shell: |
{{ include "export go vars" . | indent 6 }}
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/config /app/config
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/static /app/static
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/template /app/template
cp $GOPATH/bin/gowebapp /app/
{{- define "export go vars" -}}
export GOPATH=/go
export PATH=$GOPATH/bin:$PATH:/usr/local/go/bin
{{- end -}}
Build the application by executing the following command in the gowebapp
directory:
werf build --stages-storage :local
Running
Run the application by executing the following command in the gowebapp
directory:
werf run --stages-storage :local --docker-options="-d -p 9000:80 --name gowebapp" gowebapp -- /app/gowebapp
Check that container is running by executing the following command:
docker ps -f "name=gowebapp"
You should see a running container with the gowebapp
name, like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41d6f49798a8 werf-stages-storage/gowebapp:84d7...44992265 "/app/gowebapp" 2 minutes ago Up 2 minutes 0.0.0.0:9000->80/tcp gowebapp
Open in a web browser the following URL — http://localhost:9000.
The Go Web App
page should open. Click “Click here to login” to a login page where you can create a new account and login to the application.
Getting the application image size
Determine the image size by executing:
docker images `docker ps -f "name=gowebapp" --format='{{.Image}}'`
The output will be something like this:
REPOSITORY TAG IMAGE ID CREATED SIZE
werf-stages-storage/gowebapp 84d7...44992265 07cdc430e1c8 10 minutes ago 708MB
You can check the size of all ancestor images in the output of the werf build --stages-storage :local
command. After werf built or used any image it outputs some information like image name and tag, image size or image size difference, used instructions and commands.
Pay attention, that the image size of the application is above 700 MB.
Optimizing
There are often a lot of useless files in the image. In our example application, these are — APT cache and GO sources. Also, after building the application, the GO itself is not needed to run the application and can be removed from the image.
Optimizing APT cache
APT saves the package list in the /var/lib/apt/lists/
directory and also saves packages in the /var/cache/apt/
directory when installs them. So, it is useful to store /var/cache/apt/
outside the image and share it between builds. The /var/lib/apt/lists/
directory depends on the status of the installed packages, and it’s no good to share it between builds, but it is useful to store it outside the image to reduce its size.
To optimize using APT cache add the following directives to the gowebapp
image in the config:
mount:
- from: tmp_dir
to: /var/lib/apt/lists
- from: build_dir
to: /var/cache/apt
Read more about mount directives here.
The /var/lib/apt/lists
directory is filling in the build-time, but in the image, it is empty.
The /var/cache/apt/
directory is caching in the ~/.werf/shared_context/mounts/projects/gowebapp/var-cache-apt-28143ccf/
directory but in the image, it is empty. Mounts work only during werf assembly process. So, if you change stages instructions and rebuild your project, the /var/cache/apt/
will already contain packages downloaded earlier.
Official Ubuntu image contains special hooks that remove APT cache after image build. To disable these hooks, add the following task to a beforeInstall stage of the config:
- name: Disable docker hook for apt-cache deletion
shell: |
set -e
sed -i -e "s/DPkg::Post-Invoke.*//" /etc/apt/apt.conf.d/docker-clean
sed -i -e "s/APT::Update::Post-Invoke.*//" /etc/apt/apt.conf.d/docker-clean
Optimizing builds
In the example application, the GO is downloaded and extracted. The GO source is not needed in the image. After the application is built, the GO itself is also not needed in the image. So mount /usr/local/src
and /usr/local/go
directories to place them outside the image.
Building application on the setup stage uses the /go
directory, specified in the GOPATH
environment variable. This directory contains necessary packages and application source. After the build, the result is placed in the /app
directory, and the /go
directory is not needed to run the application. So, the /go
directory can be mounted to a temporary place, outside of the image.
Add the following to mount directives into config:
- from: tmp_dir
to: /var/lib/apt/lists
- from: build_dir
to: /var/cache/apt
- from: tmp_dir
to: /go
- from: build_dir
to: /usr/local/src
- from: build_dir
to: /usr/local/go
Complete werf.yaml config
project: gowebapp
configVersion: 1
---
{{ $_ := set . "GoDlPath" "https://dl.google.com/go/" }}
{{ $_ := set . "GoTarball" "go1.14.7.linux-amd64.tar.gz" }}
{{ $_ := set . "GoTarballChecksum" "sha256:4a7fa60f323ee1416a4b1425aefc37ea359e9d64df19c326a58953a97ad41ea5" }}
{{ $_ := set . "BaseImage" "ubuntu:18.04" }}
image: gowebapp
from: {{ .BaseImage }}
docker:
WORKDIR: /app
mount:
- from: tmp_dir
to: /var/lib/apt/lists
- from: build_dir
to: /var/cache/apt
- from: tmp_dir
to: /go
- from: build_dir
to: /usr/local/src
- from: build_dir
to: /usr/local/go
ansible:
beforeInstall:
- name: Disable docker hook for apt-cache deletion
shell: |
set -e
sed -i -e "s/DPkg::Post-Invoke.*//" /etc/apt/apt.conf.d/docker-clean
sed -i -e "s/APT::Update::Post-Invoke.*//" /etc/apt/apt.conf.d/docker-clean
- name: Install essential utils
apt:
name: ['curl','git','tree']
update_cache: yes
- name: Download the Go tarball
get_url:
url: {{ .GoDlPath }}{{ .GoTarball }}
dest: /usr/local/src/{{ .GoTarball }}
checksum: {{ .GoTarballChecksum }}
- name: Extract the Go tarball if Go is not yet installed or not the desired version
unarchive:
src: /usr/local/src/{{ .GoTarball }}
dest: /usr/local
copy: no
install:
- name: Getting packages
shell: |
{{ include "export go vars" . | indent 6 }}
go get github.com/josephspurrier/gowebapp
setup:
- file:
path: /app
state: directory
- name: Copying config
shell: |
{{ include "export go vars" . | indent 6 }}
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/config /app/config
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/static /app/static
cp -r $GOPATH/src/github.com/josephspurrier/gowebapp/template /app/template
cp $GOPATH/bin/gowebapp /app/
{{- define "export go vars" -}}
export GOPATH=/go
export PATH=$GOPATH/bin:$PATH:/usr/local/go/bin
{{- end -}}
Build the application with the modified config:
werf build --stages-storage :local
Running
Before running the modified application, you need to stop and remove running gowebapp
container we built. Otherwise, the new container can’t start or bind to 9000 port on localhost. E.g., execute the following command to stop and remove the gowebapp
container:
docker rm -f gowebapp
Run the modified application by executing the following command:
werf run --stages-storage :local --docker-options="-d -p 9000:80 --name gowebapp" gowebapp -- /app/gowebapp
Check that container is running by executing the following command:
docker ps -f "name=gowebapp"
You should see a running container with a gowebapp
name, like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41d6f49798a8 werf-stages-storage/gowebapp:84d7...44992265 "/app/gowebapp" 2 minutes ago Up 2 minutes 0.0.0.0:9000->80/tcp gowebapp
Open in a web browser the following URL — http://localhost:9000.
The Go Web App
page should open. Click “Click here to login” to a login page where you can create a new account and login to the application.
Getting images size
Determine the image size of optimized build, by executing:
docker images `docker ps -f "name=gowebapp" --format='{{.Image}}'`
The output will be something like this:
REPOSITORY TAG IMAGE ID CREATED SIZE
werf-stages-storage/gowebapp 84d7...44992265 07cdc430e1c8 10 minutes ago 181MB
Analysis
The ~/.werf/shared_context/mounts/projects/gowebapp/
path contains directories mounted with from: build_dir
directives in the werf.yaml
file. Execute the following command to analyze:
tree -L 3 ~/.werf/shared_context/mounts/projects/gowebapp
The output will be like this (some lines skipped):
/home/user/.werf/shared_context/mounts/projects/gowebapp/
├── usr-local-go-a179aaae
│ ├── api
│ ├── lib
│ ├── pkg
...
│ └── src
├── usr-local-src-6ad4baf1
│ └── go1.14.7.linux-amd64.tar.gz
└── var-cache-apt-28143ccf
└── archives
├── ca-certificates_20190110~18.04.1_all.deb
...
└── xauth_1%3a1.0.10-1_amd64.deb
As you may see, there are separate directories on the host for every mount in config exists.
Check the directories size, by executing:
sudo du -kh --max-depth=1 ~/.werf/shared_context/mounts/projects/gowebapp
The output will be like this:
19M /home/user/.werf/shared_context/mounts/projects/gowebapp/var-cache-apt-28143ccf
119M /home/user/.werf/shared_context/mounts/projects/gowebapp/usr-local-src-6ad4baf1
348M /home/user/.werf/shared_context/mounts/projects/gowebapp/usr-local-go-a179aaae
485M /home/user/.werf/shared_context/mounts/projects/gowebapp
485M
is a size of files excluded from image, but these files are accessible, in case of rebuild image and also they can be mounted in other images in this project. E.g., if you add image based on Ubuntu, you can mount /var/cache/apt
with from: build_dir
and use already downloaded packages.
Also, approximately 70MB
of space occupy files in directories mounted with from: tmp_dir
. These files also excluded from the image and deleted from the host at the end of image building.
The total size difference between images with and without using mounts is about 527MB
(the result of 708MB — 181MB).
Our example shows that with using werf mounts the image size smaller by more than 70% than the original image size!
What Can Be Improved
- Use a smaller base image instead of ubuntu, such as alpine or golang.
- Using werf artifacts in many cases can give more efficient.
The size of
/app
directory in the image is about only 15 MB (you can check it by executingwerf run --stages-storage :local --docker-options="--rm" gowebapp -- du -kh --max-depth=0 /app
). So you can build files into the/app
in werf artifact and then import only the resulting/app
directory.