Hello, it is a common phenomenon that Zeabur's Docker builds have layer cache, but your apt steps re-run every time. Here is the explanation and solution:
Why apt re-downloads every time
Zeabur uses BuildKit under the hood, which runs standard Docker layer cache:
- If a layer's instructions are identical and all layers above it hit the cache, this layer will be reused directly from the previous cache.
- If any layer above it misses, all subsequent layers will re-run.
However, Zeabur's build environment has a few special considerations:
- Build Node Pool: Each build may be scheduled to a different builder node. Layer cache is not automatically shared between different nodes, so your build might hit the cache once (same node) and miss the next time (different node).
- Long periods without builds: Builder node cache is garbage collected. During a cold start, all layers must re-run.
- ARG changes above FROM: If your Dockerfile has
ARG NODE_VERSION=24.14.1 before FROM, and if zbpack or Zeabur injects any variables that affect the ARG at build-time (e.g., build args, commit SHA), this invalidates the entire FROM instruction, causing all subsequent layers (including apt) to re-run.
- No built-in apt cache persistence: Even if the layer hits, the downloaded apt files are inside the layer. If the layer misses, you must re-run
apt-get update && install completely.
Solution: Use BuildKit cache mount
This is the most reliable method — even if the layer cache misses, the downloaded apt files are persisted in the BuildKit cache, so you don't need to re-download them next time:
Add the syntax instruction at the very top of your Dockerfile (important, otherwise cache mount syntax won't work), and change your RUN apt-get to:
# syntax=docker/dockerfile:1.4
ARG NODE_VERSION=24.14.1
FROM node:${NODE_VERSION}-slim AS base
EXPOSE 8080
WORKDIR /app
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean && \
apt-get update -qq && \
apt-get install -y python-is-python3 pkg-config build-essential openssl
Key points:
--mount=type=cache,target=/var/cache/apt — Caches the .deb files downloaded by apt.
--mount=type=cache,target=/var/lib/apt/lists — Caches the apt metadata (generated by apt-get update).
rm -f /etc/apt/apt.conf.d/docker-clean — The default Debian/Ubuntu base image clears /var/cache/apt after apt operations. You must remove this hook, otherwise the cache mount will be empty every time.
sharing=locked — Ensures that multiple concurrent builds do not pollute each other.
Other Suggestions
- Place the apt RUN instruction as early as possible in the Dockerfile, and ensure its content "almost never changes." This keeps the
FROM / WORKDIR above it stable, so the layer cache will hit as long as that line doesn't change.
- Do not place
COPY . . before apt. If you copy files that change (like package.json) before RUN apt-get, any file change will invalidate the apt layer.
Verification
After making the changes, build twice. In the second build log, the apt-get install step should show something like:
CACHED [base 3/6] RUN --mount=type=cache,... apt-get install ...
Or, even if the layer isn't cached and needs to re-run, the apt download speed will be significantly faster (because the .deb files are pulled from the cache mount, not re-downloaded from the Debian repo).
If you are still re-downloading apt every time after adding the cache mount, please reply with your build log, and we will continue to investigate if there is any special behavior in Zeabur's BuildKit cache that needs a patch.