CI-Node is a docker image intended to be used in continuous integration services such as GitLab CI, GitHub Actions, Semaphore CI and Circle CI
Example for bash and node version 24:
docker build \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
-t panascais/ci-node:24 \
./24Example for fish and node version 24:
docker build \
--build-arg BUILD_DATE=(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--build-arg VCS_REF=(git rev-parse --short HEAD) \
-t panascais/ci-node:24 \
./24BuildKit cache mounts speed up rebuilds by reusing downloaded packages between builds. Both apk and pnpm use them in this image.
The large apk install layer mounts /apk/cache so .apk files are reused across builds. Cache IDs include ${TARGETARCH} so amd64 and arm64 packages do not mix during multi-platform builds. sharing=locked avoids races when matrix jobs build in parallel.
RUN --mount=type=cache,id=ci-node-apk-${TARGETARCH},sharing=locked,target=/apk/cache \
apk update --cache-dir /apk/cache \
&& apk add --cache-dir /apk/cache --cache-predownload \
...Alpine 3.23 ships apk-tools 3.x, where --update / -U means --cache-max-age 0 (always refetch). Do not use it with cache mounts. Pass 1 writes into /apk/cache; when the apk layer re-runs, --cache-predownload reuses cached .apk files. The mount never lands in the final image.
pnpm v11 (Node 22+): the base node image disables the global virtual store (enableGlobalVirtualStore: false in /root/.config/pnpm/config.yaml) so global installs behave like pnpm v10. A single pnpm add -g pass cache-mounts the store at /root/.local/share/pnpm/store (id=ci-node-pnpm-11-${TARGETARCH},sharing=locked). Without that setting, mounting the store cache breaks global CLIs at runtime (ae38d12).
pnpm v10 and below (Node 21 and older): single-pass global install with the store cache-mounted at pnpm’s default path for that image (no --store-dir override). Measure with the same ENV as the Dockerfile (pnpm store path).
| pnpm | PNPM_HOME in ci-node |
pnpm store path |
Cache mount target= |
Cache id |
|---|---|---|---|---|
| 6 (Node 12) | (none; bins in /usr/local/bin) |
/root/.pnpm-store/v3 |
/root/.pnpm-store |
ci-node-pnpm-6-${TARGETARCH} |
| 7–10 (Node 14–21) | .../pnpm/bin |
.../pnpm/bin/store/v3 or .../v10 |
/root/.local/share/pnpm/bin/store |
ci-node-pnpm-${TARGETARCH} |
| 11 (Node 22+) | /root/.local/share/pnpm |
.../pnpm/store/v10 |
/root/.local/share/pnpm/store |
ci-node-pnpm-11-${TARGETARCH} |
# Node 12 (pnpm 6; store at ~/.pnpm-store, global bins in /usr/local/bin)
RUN --mount=type=cache,id=ci-node-pnpm-6-${TARGETARCH},sharing=locked,target=/root/.pnpm-store \
packages=" ... " \
&& pnpm i -g $packages
# Node 14–21 (pnpm 7–10)
RUN --mount=type=cache,id=ci-node-pnpm-${TARGETARCH},sharing=locked,target=/root/.local/share/pnpm/bin/store \
packages=" ... " \
buildable=" --allow-build=... " \
&& mkdir -p /root/.local/share/pnpm/bin \
&& pnpm i -g $packages $buildable
# Node 22+ (pnpm 11; inherits enableGlobalVirtualStore: false from base node image)
RUN --mount=type=cache,id=ci-node-pnpm-11-${TARGETARCH},sharing=locked,target=/root/.local/share/pnpm/store \
packages=" ... " \
buildable=" --allow-build=... " \
&& mkdir -p /root/.local/share/pnpm/bin \
&& pnpm add -g $packages $buildableMount the parent directory (pnpm creates v3 / v10 subdirs inside). ${TARGETARCH} and sharing=locked avoid cross-arch mixing and parallel-build store corruption.
When building application images on top of ci-node, use a separate project store (pnpm Docker docs):
ENV PNPM_HOME=/pnpm
ENV PATH=${PNPM_HOME}:${PATH}
RUN --mount=type=cache,id=pnpm-${TARGETARCH},sharing=locked,target=/pnpm/store \
pnpm install --frozen-lockfilepnpm itself comes from /usr/local/bin/pnpm on the base node image.
- Silas Rech (silas@panascais.net)
- Maximilian Schagginger (max@panascais.net)
Interested in contributing to CI-Node? Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests.