docker squash 通过merge所有layer从而减小docker image的体积,得到一个“所见即所得”的镜像。
缺点是这样就用不了layer cache了,并且会产生一个很大的最终layer。

曾经docker build有一个--squash选项,是experimental的。最近用的时候发现没了。于是查了一下怎么个事。

Issue - Better documentation for “squash”
docker build (legacy builder)

这个实验性的功能最终是被删除了。因为docker fs的兼容问题,这个功能无法在各种fs上普及。

No plans atm. Squash was never taken out of experimental and is only supported in moby outside the builder component. You should squash layers with a multi-stage dockerfile(if you are sure you know what you are doing and need to squash at all).

正确姿势应该是使用multi-stage dockerfile

那么应该怎么用呢?

先贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#!/bin/sh
################################################################################
# The setups in this file belong to https://code.shin.company/docker-squash
# I appreciate you respecting my intellectual efforts in creating them.
# If you intend to copy or use ideas from this project, please credit properly.
# Author: SHIN Company <shin@shin.company>
# License: https://code.shin.company/docker-squash/blob/main/LICENSE
################################################################################

# Check if Docker is installed, if not exit
if [ ! -x "$(command -v docker)" ]; then
echo "Docker is not installed. Please install Docker and try again." >&2
exit 1
fi

# HELPER FUNCTIONS
################################################################################

usage() {
cat <<EOF

Combines Docker image layers into a single layer, reducing storage space
and improving runtime performance by decreasing mount points.

Usage: ${0##*/} <source_image> [build_options]

Arguments:
source_image The original image ID or name:tag to be squashed.
An absolute path to your Dockerfile can also be used.
build_options Optional Docker build options like --build-arg, --label, etc.

Author: SHIN Company <shin@shin.company>
License: https://code.shin.company/docker-squash/blob/main/LICENSE

EOF
}

upgrade() {
local install=/usr/local/bin/docker-squash.sh
sudo curl -L https://github.com/shinsenter/docker-squash/raw/main/docker-squash.sh \
-o $install && chmod +x $install && $install -h
}

docker_tag_id() {
docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}' 2>/dev/null | grep -wi "$@" | sort | head -n1
}

docker_config() {
docker image inspect --format='{{json .Config}}' "$1" | tr -d '[:cntrl:]'
}

parse_config() {
local id="$1"; shift; docker_config "$id" | jq -r "$@"
}

get_name() { docker_tag_id "$1" | awk '{printf $1}'; }
get_id() { docker_tag_id "$1" | awk '{printf $2}'; }
get_labels() { parse_config "$1" '.Labels // empty|to_entries|map("LABEL \(.key)=\"\(.value)\"")|.[]'; }
get_shell() { parse_config "$1" '.Shell // empty|@json'; }
get_envs() { parse_config "$1" '.Env // empty|map(gsub("\\\\"; "\\\\\\\\"))|sort|.[]|capture("(?<key>[^=]+)=(?<value>.*)")|"ENV \(.key)=\"\(.value)\""'; }
get_onbuilds() { parse_config "$1" '.OnBuild // empty|map("ONBUILD \(.)")|.[]'; }
get_exposes() { parse_config "$1" '.ExposedPorts // empty|keys|map("EXPOSE \(.)")|.[]'; }
get_workdir() { parse_config "$1" '.WorkingDir // empty'; }
get_user() { parse_config "$1" '.User // empty'; }
get_volumes() { parse_config "$1" '.Volumes // empty|keys|map("VOLUME \"\(.)\"")|sort|.[]'; }
get_entrypoint() { parse_config "$1" '.Entrypoint // empty|@json'; }
get_cmd() { parse_config "$1" '.Cmd // empty|@json'; }
get_stopsignal() { parse_config "$1" '.StopSignal // empty'; }
get_healthcheck() { parse_config "$1" '.Healthcheck // empty|(
(if .Interval then "--interval="+(.Interval|tonumber/1000000000|tostring)+"s " else "" end) +
(if .Timeout then "--timeout="+(.Timeout|tonumber/1000000000|tostring)+"s " else "" end) +
(if .StartPeriod then "--start-period="+(.StartPeriod|tonumber/1000000000|tostring)+"s " else "" end) +
(if .StartInterval then "--start-interval="+(.StartInterval|tonumber/1000000000|tostring)+"s " else "" end) +
(if .Retries then "--retries="+(.Retries|tostring)+" " else "" end) +
(.Test[0]|sub("-SHELL";"")) + " " + .Test[1]
)'; }

generate() {
local id="$(get_id "$1")"

if [ -z "$id" ]; then
echo "Invalid docker image: $1. Please build or pull the image first." >&2
exit 1
fi

local base="scratch"
local alias="temp-$id"
local tag="$(get_name "$id")"
local labels="$(get_labels "$id")"
local shell="$(get_shell "$id")"
local envs="$(get_envs "$id")"
local onbuilds="$(get_onbuilds "$id")"
local exposes="$(get_exposes "$id")"
local workdir="$(get_workdir "$id")"
local user="$(get_user "$id")"
local volumes="$(get_volumes "$id")"
local entrypoint="$(get_entrypoint "$id")"
local cmd="$(get_cmd "$id")"
local stopsignal="$(get_stopsignal "$id")"
local healthcheck="$(get_healthcheck "$id")"

awk NF <<Dockerfile


################################################################################
# Base image: $tag (Image ID: $id)
# Created at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Created by: https://code.shin.company/docker-squash

################################################################################
# CLEANING UP THE SOURCE IMAGE. ################################################
# Enable SBOM attestations
# See: https://docs.docker.com/build/attestations/sbom/
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true

FROM $tag AS $alias
ARG BUILDKIT_SBOM_SCAN_STAGE=true
# Pre-squash scripts may be useful to clean the source image before squashing.
# Use build argument to add your pre-squash scripts, and run them in this stage.
# Example:
# ${0##*/} $tag --build-arg PRESQUASH_SCRIPTS="rm -rf /tmp/*"
# ${0##*/} $tag --build-arg PRESQUASH_SCRIPTS="/path/to/script.sh"
ARG PRESQUASH_SCRIPTS="\${PRESQUASH_SCRIPTS:-rm -rf /tmp/* /usr/share/doc/* /var/cache/* /var/lib/apt/lists/* /var/log/*}"
RUN [ ! -z "\$PRESQUASH_SCRIPTS" ] && sh -c "\$PRESQUASH_SCRIPTS" || true

################################################################################
# BUILDING SQUASHED IMAGE FROM SCRATCH. ########################################
FROM $base AS squashed-$id
ARG BUILDKIT_SBOM_SCAN_STAGE=true
COPY --link --from=$alias / /
$(if [ -n "$labels" ]; then echo "$labels"; fi)
$(if [ -n "$shell" ]; then echo "SHELL $shell"; fi)
$(if [ -n "$envs" ]; then echo "$envs"; fi)
$(if [ -n "$onbuilds" ]; then echo "$onbuilds"; fi)
$(if [ -n "$exposes" ]; then echo "$exposes"; fi)
$(if [ -n "$workdir" ]; then echo "WORKDIR $workdir"; fi)
$(if [ -n "$user" ]; then echo "USER $user"; fi)
$(if [ -n "$volumes" ]; then echo "$volumes"; fi)
$(if [ -n "$entrypoint" ]; then echo "ENTRYPOINT $entrypoint"; fi)
$(if [ -n "$cmd" ]; then echo "CMD $cmd"; fi)
$(if [ -n "$stopsignal" ]; then echo "STOPSIGNAL $stopsignal"; fi)
$(if [ -n "$healthcheck" ]; then echo "HEALTHCHECK $healthcheck"; fi)
# FINISH. ######################################################################
################################################################################
Dockerfile
}

# Parse arguments
################################################################################

# Show usage if no arguments are passed
if [ $# -eq 0 ]; then
usage >&2
exit 1
fi

for a; do
shift
case "$a" in
--help | -h) usage >&2; exit 0; ;;
--upgrade | upgrade | -u) upgrade; exit 0; ;;
--print* | -p*) print=1 ;;
*) set -- "$@" "$a" ;;
esac
done

dockerfile="$1"
cleanup=0
shift

# Check Source Image / Build From Dockerfile
################################################################################

# Enable debug mode
if [ ! -z "$DEBUG" ]; then set -ex; fi

# Build tempoprary image from Dockerfile
if [ -f "$dockerfile" ]; then
hash="$(sha256sum "$dockerfile")/$(echo $@ | sha256sum)"
temptag="docker-squash-build-$(echo $hash | sha256sum | head -c 8)"
context="$(dirname "$dockerfile")"

if ! docker build -f "$dockerfile" "$context" "$@" -t "$temptag"; then
echo "Failed to build image from $dockerfile." >&2
exit 1
fi

source="$temptag"
cleanup="${CLEAR_TEMP_BUILD:-1}"
else
# try pulling the docker image from Docker Hub
if [ -z "$(get_name "$dockerfile")" ]; then
docker pull "$dockerfile" 2>&1
fi

source="$(get_name "$dockerfile")"
fi

# Check if source exists
if [ -z "$source" ]; then
echo "Invalid image data from '$dockerfile'. Please build or pull the image first." >&2
exit 1
fi

# Squash / Output to New Dockerfile
################################################################################

# Print Dockerfile to stdout
if [ ! -z "$print" ]; then
generate "$source"
exit 0
fi

# Use Docker buildx if available
BUILD_CMD="docker build"
if docker buildx version &>/dev/null; then
BUILD_CMD="docker buildx build"
fi

# Squash image to single layer
echo "Start squashing the image $source"
echo " Build options: $@"
generate "$source" | $BUILD_CMD "$@" -
[ "$cleanup" -eq 1 ] && docker image rm -f "$source"
echo "Done."

这个代码构造了一个dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true

FROM $tag AS $alias
ARG BUILDKIT_SBOM_SCAN_STAGE=true
# Pre-squash scripts may be useful to clean the source image before squashing.
# Use build argument to add your pre-squash scripts, and run them in this stage.
# Example:
# ${0##*/} $tag --build-arg PRESQUASH_SCRIPTS="rm -rf /tmp/*"
# ${0##*/} $tag --build-arg PRESQUASH_SCRIPTS="/path/to/script.sh"
ARG PRESQUASH_SCRIPTS="\${PRESQUASH_SCRIPTS:-rm -rf /tmp/* /usr/share/doc/* /var/cache/* /var/lib/apt/lists/* /var/log/*}"
RUN [ ! -z "\$PRESQUASH_SCRIPTS" ] && sh -c "\$PRESQUASH_SCRIPTS" || true

################################################################################
# BUILDING SQUASHED IMAGE FROM SCRATCH. ########################################
FROM $base AS squashed-$id
ARG BUILDKIT_SBOM_SCAN_STAGE=true
COPY --link --from=$alias / /
$(if [ -n "$labels" ]; then echo "$labels"; fi)
$(if [ -n "$shell" ]; then echo "SHELL $shell"; fi)
$(if [ -n "$envs" ]; then echo "$envs"; fi)
$(if [ -n "$onbuilds" ]; then echo "$onbuilds"; fi)
$(if [ -n "$exposes" ]; then echo "$exposes"; fi)
$(if [ -n "$workdir" ]; then echo "WORKDIR $workdir"; fi)
$(if [ -n "$user" ]; then echo "USER $user"; fi)
$(if [ -n "$volumes" ]; then echo "$volumes"; fi)
$(if [ -n "$entrypoint" ]; then echo "ENTRYPOINT $entrypoint"; fi)
$(if [ -n "$cmd" ]; then echo "CMD $cmd"; fi)
$(if [ -n "$stopsignal" ]; then echo "STOPSIGNAL $stopsignal"; fi)
$(if [ -n "$healthcheck" ]; then echo "HEALTHCHECK $healthcheck"; fi)

把原镜像里面的ENV,LABEL,Volume啥的都拿出来,然后在dockerfile里面以base="scratch"为基镜像,把这些变量,都加进去,然后在把原镜像直接把根目录都copy过去COPY --link --from=$alias / /

scratch是docker提供的,“空的”base image

Create a minimal base image using scratch
The reserved, minimal scratch image serves as a starting point for building containers. Using the scratch image signals to the build process that you want the next command in the Dockerfile to be the first filesystem layer in your image.

这个脚本使用方法:

1
2
3
bash -ex docker-squash.sh \
${原镜像tag} \
--tag ${压缩后tag}