diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94cd4cea..53a381d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,7 +117,41 @@ jobs: test -f "$CFG" || { echo "宿主机配置缺失,中止部署"; exit 1; } + # Old images of this repository are removed here and nowhere else. + # Every deployment pulls one tagged with its commit and nothing ever + # removed the previous one, so they only accumulated: 68 of them + # filled the disk and the next deployment could not pull. + # + # Three are kept so a release can be re-run by tag by hand. + # + # Only this repository's images are listed, because the host runs + # other services whose images are not this script's business. The + # image the live container is on is excluded by id rather than by + # position, so it survives even if the listing order is not what + # it looks like. With no container to ask, the function returns + # rather than running the pipeline on an empty id - which would + # also delete nothing, but by way of grep -v matching every line, + # which reads like the opposite of what it does. No -f, so an image + # any container still holds - including the one kept for rollback - + # is refused rather than taken away from it. + prune_old_images() { + REPO="${IMG%:*}" + LIVE=$(sudo docker inspect -f '{{.Image}}' "$NAME" 2>/dev/null | sed 's/^sha256://' | cut -c1-12) + [ -n "$LIVE" ] || return 0 + sudo docker images "$REPO" --format '{{.ID}} {{.Repository}}:{{.Tag}}' \ + | grep -v "^$LIVE" \ + | tail -n +3 \ + | awk '{print $2}' \ + | xargs -r -n1 sudo docker rmi >/dev/null 2>&1 || true + } + sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }} + # Before the pull, not only after a successful deploy. The pull + # is the first thing here that needs space and it is where a full + # disk stops this script, so a cleanup that only runs afterwards + # never runs on the host that needs it: rerunning the workflow + # fails at the same pull, and the disk has to be cleared by hand. + prune_old_images sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; } # 迁移用新镜像跑。失败时线上仍是旧版本配旧 schema,是自洽的; @@ -155,6 +189,10 @@ jobs: if [ "$ok" = "1" ]; then sudo docker rm -f "$PREV" >/dev/null 2>&1 || true + + # Again, so the image this deployment replaced falls out of the + # window rather than waiting for the next deployment to notice. + prune_old_images else echo "健康检查失败,回滚到上一版本" sudo docker logs --tail 40 "$NAME" 2>&1 || true diff --git a/Dockerfile b/Dockerfile index fb3e023d..22a060e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,26 @@ FROM alpine RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories -RUN apk update --no-cache -RUN apk add --update gcc g++ libc6-compat -RUN apk add --no-cache ca-certificates -RUN apk add --no-cache tzdata +# Runtime packages only. +# +# gcc and g++ used to be installed here and were 273MB of a 381MB image. The +# binary this image runs is compiled and linked before the image is built and +# arrives as a COPY, so nothing in the container ever invokes a compiler - +# there is no toolchain to drive it with either, since Go itself is not +# installed. +# +# That layer was also why a host could not share storage between images. apk +# resolves against an index that moves, so the layer digest differed on every +# build and no two images shared it: a host keeping one image per deployed +# commit stored a private 273MB copy each time. 68 of them filled the disk +# and the next deployment could not pull. +# +# libc6-compat stays. Nothing measured needs it - the binary CI produces is +# statically linked, and a container built without libc6-compat resolves a +# hostname and opens a database connection exactly as one built with it - but +# it is half a megabyte and it covers a ./main that was linked dynamically, +# which this Dockerfile has no way to check. +RUN apk add --no-cache ca-certificates tzdata libc6-compat ENV TZ Asia/Shanghai COPY ./main /main @@ -15,4 +31,4 @@ COPY ./config/settings.demo.yml /config/settings.yml COPY ./go-admin-db.db /go-admin-db.db EXPOSE 8000 RUN chmod +x /main -CMD ["/main","server","-c", "/config/settings.yml"] \ No newline at end of file +CMD ["/main","server","-c", "/config/settings.yml"]