Multi-stage builds, BuildKit --output and reproducible releases
Docker is not just a runtime, it is a portable build environment. Teams that create deployment artifacts from Docker builds get reproducible binaries, static assets and archives without installing the build toolchain on the CI server or the developer machine.
Table of contents
- 1. Docker as a build environment: the core idea
- 2. Multi-stage Dockerfiles for clean artifact separation
- 3. Extracting artifacts from the container with docker cp
- 4. BuildKit --output: writing directly to the host filesystem
- 5. BuildKit cache mounts for fast builds
- 6. Producing PHP Composer and Node asset artifacts
- 7. Exporting SBOM and provenance from Docker builds
- 8. Integrating artifact builds into GitHub Actions and GitLab CI
- 9. Comparing artifact extraction methods
- 10. Summary
- 11. FAQ
1. Docker as a build environment: the core idea
The core promise of Docker as a build environment is this: if the build runs inside a container, the result is identical on every machine, regardless of which version of PHP, Node, Go or Rust happens to be installed on the CI server or the development machine. Creating deployment artifacts from Docker builds means treating the Dockerfile not merely as a recipe for a runtime image, but as a complete, versioned build environment that takes input (source code) and produces output (compiled binaries, archives, static assets).
This approach solves the classic "it works on my machine" problem. Teams that create deployment artifacts from Docker builds get the same toolchain version in development, CI and production. The build environment is declared in the Dockerfile, versioned and reproducible. A Go binary compiled inside a golang:1.22 container is bit identical whether the build runs on a developer's laptop, on a GitHub Actions runner, or on a Kubernetes node.
2. Multi-stage Dockerfiles for clean artifact separation
Multi-stage builds are the cleanest way to create deployment artifacts from Docker builds while keeping the final image small. Every FROM statement starts a new build stage. With COPY --from=build-stage /app/dist ./dist, only the finished artifacts are copied into the next stage, while the entire build context with compiler, test dependencies and temporary files stays in the intermediate layer and never reaches the final image. This matters not only for image size but also for security: development dependencies with known CVEs are not present in the production image.
An important technique when producing deployment artifacts from Docker builds is targeting individual stages with docker build --target artifact-stage. This allows a single build to have several artifact stages, one for the compiled binary, one for static assets, one for test results, and the CI pipeline calls the stage it needs. With BuildKit, independent stages run in parallel, which noticeably reduces build time in multi-stage scenarios.
# syntax=docker/dockerfile:1.7
# Multi-stage Dockerfile: build PHP app + compile Node assets + export artifacts
# --- Stage 1: PHP dependency build ---
FROM composer:2.7 AS php-deps
WORKDIR /app
COPY composer.json composer.lock ./
# Cache mount: Composer cache survives across builds
RUN --mount=type=cache,target=/root/.composer \
composer install --no-dev --optimize-autoloader --no-interaction
# --- Stage 2: Node asset build ---
FROM node:22-alpine AS node-assets
WORKDIR /build
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --prefer-offline
COPY web/tailwind/ ./web/tailwind/
RUN npm run build
# --- Stage 3: Artifact collector (lightweight) ---
FROM scratch AS artifacts
COPY --from=php-deps /app/vendor ./vendor/
COPY --from=node-assets /build/web/css ./web/css/
COPY --from=node-assets /build/web/js ./web/js/
# --- Stage 4: Final runtime image ---
FROM php:8.4-fpm-alpine AS runtime
COPY --from=php-deps /app/vendor /app/vendor
COPY --from=node-assets /build/web /app/web
COPY src/ /app/src/
3. Extracting artifacts from the container with docker cp
The classic way to extract deployment artifacts from Docker builds is a combination of docker build, docker create and docker cp. After the build, docker create creates a container from the image without starting it, which makes the container layer available and enables file access. Then docker cp copies the desired artifacts to the host filesystem. Finally, docker rm removes the temporary container. This method works with any Docker client without BuildKit and is therefore still common in older CI environments.
The downside of this method is that it requires three separate commands and can leave a dangling container behind on failure if the cleanup script is not properly guarded. The pattern trap "docker rm -f $CONTAINER_ID" EXIT in a shell script ensures the temporary container is cleaned up even when something fails. For deployment artifacts from Docker builds in modern environments with BuildKit, the --output method is more elegant.
#!/usr/bin/env bash
# extract-artifacts.sh: build and extract deployment artifacts from Docker
set -euo pipefail
IMAGE_TAG="my-app-build:$(git rev-parse --short HEAD)"
ARTIFACT_DIR="./dist"
# Build the image targeting the artifact stage
docker build \
--target artifacts \
--tag "$IMAGE_TAG" \
--build-arg BUILD_ENV=production \
.
# Create a temporary container (not started) to access the filesystem
CONTAINER_ID=$(docker create "$IMAGE_TAG")
trap "docker rm -f '$CONTAINER_ID' >/dev/null 2>&1" EXIT
mkdir -p "$ARTIFACT_DIR"
# Copy artifacts from container to host
docker cp "${CONTAINER_ID}:/vendor" "${ARTIFACT_DIR}/vendor"
docker cp "${CONTAINER_ID}:/web/css" "${ARTIFACT_DIR}/web/css"
docker cp "${CONTAINER_ID}:/web/js" "${ARTIFACT_DIR}/web/js"
echo "Artifacts extracted to ${ARTIFACT_DIR}"
ls -lh "${ARTIFACT_DIR}"
# Remove intermediate image to free disk space
docker rmi "$IMAGE_TAG"
4. BuildKit --output: writing directly to the host filesystem
BuildKit's --output flag offers a more direct alternative to the docker cp workflow. With docker buildx build --output type=local,dest=./dist ., files from the build context are exported straight into the given host directory, with no intermediate container involved. This is especially convenient for deployment artifacts from Docker builds that need to land directly in an artifact store such as GitHub Artifacts, S3 or Nexus. Combined with a FROM scratch AS artifacts stage, the exported directory contains exactly the files that stage holds.
The tar output type exports a TAR archive instead of a directory: --output type=tar,dest=./app.tar. This is ideal for deployment artifacts from Docker builds that need to be stored or transferred as versioned archives. With the image type and push=true, the image is pushed straight to a registry without being loaded locally, which saves disk I/O and suits CI environments with limited local storage.
5. BuildKit cache mounts for fast builds
BuildKit cache mounts (RUN --mount=type=cache,target=/root/.composer) are the single most important performance feature for deployment artifacts from Docker builds in CI/CD pipelines. They persist the download cache of package managers, Composer, NPM, pip, the Go module cache, apt packages, between builds without writing it into the image layer. In practice that means: on a second build with the same dependencies, no packages are downloaded again, even though the layer cache is invalidated by source code changes.
The difference from a regular layer cache is important: a layer cache is invalidated as soon as any input changes. A cache mount survives regardless. If only a single PHP class changes, the Composer layer cache becomes invalid, but the cache mount still holds every downloaded package, so composer install finishes in seconds instead of minutes. For deployment artifacts from Docker builds in GitHub Actions or GitLab CI, cache mounts can be persisted through the GitHub Actions cache with docker buildx build --cache-from type=gha --cache-to type=gha.
6. Producing PHP Composer and Node asset artifacts
For PHP projects built on Magento or Symfony, two kinds of deployment artifacts from Docker builds matter most: the optimized Composer vendor folder and the compiled frontend assets. Generating the vendor folder with composer install --no-dev --optimize-autoloader inside a Composer container and extracting it as an artifact means the deployment process on the target server no longer needs a Composer invocation at all. That removes a dependency from the production server and makes the deployment more deterministic.
Node assets, Tailwind CSS, JavaScript bundles, are typically compiled in a Node stage using the appropriate builder. The result is a dist/ or pub/static/ directory that is copied to the production server as a deployment artifact from Docker builds or uploaded to a CDN. By encapsulating the build inside Docker, teams can guarantee that exactly the same Node version and the same package versions are used for every build, which removes a common source of errors in manual asset builds.
# BuildKit --output: write artifacts directly to host filesystem (no docker cp needed)
docker buildx build \
--target artifacts \
--output type=local,dest=./dist \
--build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--build-arg GIT_SHA="$(git rev-parse --short HEAD)" \
.
# Export as TAR archive for versioned storage
docker buildx build \
--target artifacts \
--output "type=tar,dest=./releases/app-$(git rev-parse --short HEAD).tar" \
.
# Build and push image directly to registry without local storage
docker buildx build \
--platform linux/amd64,linux/arm64 \
--output type=image,push=true \
--tag registry.example.com/my-app:$(git rev-parse --short HEAD) \
.
# Use GitHub Actions cache backend for persistent cache across CI runs
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--output type=local,dest=./dist \
.
7. Exporting SBOM and provenance from Docker builds
A Software Bill of Materials (SBOM) and build provenance are increasingly relevant deployment artifacts from Docker builds that address compliance and security requirements. Docker BuildKit 0.11+ can automatically generate an SBOM as an image attestation: docker buildx build --sbom=true --provenance=true. The SBOM lists every package and dependency in the image and is pushed to the registry alongside the image as a signable OCI attestation manifest. docker buildx imagetools inspect image:tag displays the attestation.
For deployment artifacts from Docker builds in regulated environments, combining SBOM and provenance is especially valuable: provenance documents which source commit, which CI job and which BuildKit version produced the artifact. That makes the entire build path traceable, a requirement increasingly demanded by EU cybersecurity regulation (the CRA) and supply chain security frameworks such as SLSA.
8. Integrating artifact builds into GitHub Actions and GitLab CI
In GitHub Actions and GitLab CI, deployment artifacts from Docker builds are typically produced and stored in two steps: build and upload. GitHub Actions offers the actions/upload-artifact action, which stores arbitrary files as build artifacts and hands them off via download or between jobs. Combined with docker buildx build --output type=local,dest=./dist, the files land in the dist/ directory after the build and can be uploaded right away. The artifacts are then downloadable in the GitHub Actions UI and available to subsequent jobs via actions/download-artifact.
GitLab CI uses the artifacts directive in .gitlab-ci.yml. With artifacts: paths: [dist/] and expire_in: 7 days, the resulting deployment artifacts from Docker builds are stored after the job runs and passed on to subsequent jobs in the pipeline. That enables the typical CI/CD flow: the build job produces artifacts, the test job checks them, the deploy job ships them to the production server, all without every job repeating the full build.
9. Comparing artifact extraction methods
The different methods of extracting deployment artifacts from Docker builds come with different trade offs depending on the CI environment and the requirements at hand.
| Method | Requirement | Advantage | Drawback |
|---|---|---|---|
| docker cp | Standard Docker | Universally compatible | 3 commands, container cleanup needed |
| BuildKit --output local | BuildKit enabled | Direct export, no container | Requires BuildKit (default since Docker 23) |
| BuildKit --output tar | BuildKit enabled | Versioned archive usable right away | Needs unpacking at deploy time |
| Registry push and pull | Registry access | Versioning and caching built in | Requires registry infrastructure |
| SBOM + provenance | BuildKit 0.11+ | Compliance and supply chain security | Requires an OCI-compatible registry |
For modern CI/CD pipelines, BuildKit --output local combined with a downstream upload-artifact action is the recommended way to produce deployment artifacts from Docker builds. It combines the reproducibility of a Docker build with the simplicity of a direct file export and needs no registry infrastructure. For images that get deployed, a registry push remains the standard.
Mironsoft
CI/CD pipelines, Docker build optimization and deployment automation
Need reproducible deployment artifacts from Docker builds?
We build CI/CD pipelines that combine clean multi-stage Dockerfiles with BuildKit cache mounts and automated artifact export, for deployments that behave the same way every time.
Build optimization
Multi-stage Dockerfiles and cache mounts for fast, reproducible builds
CI/CD integration
Configuring GitHub Actions and GitLab CI with artifact upload and cache backend
SBOM & compliance
Automatic SBOM generation and provenance attestation for security requirements
10. Summary
Creating deployment artifacts from Docker builds is the safest way to ensure reproducible, toolchain-independent builds in CI/CD pipelines. Multi-stage Dockerfiles cleanly separate build and runtime context and keep the final image size minimal. BuildKit cache mounts eliminate redundant package downloads and speed up iterative builds considerably. BuildKit's --output method is the most modern way to write artifacts directly to the host filesystem, skipping the detour through container creation and docker cp.
For PHP projects that means, in practice: generate the Composer vendor folder inside a Composer container, compile Node assets inside a Node container, extract both artifacts and ship them to the production server, all without a local PHP or Node installation. SBOM and provenance as additional deployment artifacts from Docker builds satisfy growing compliance requirements and make the entire build path traceable.
Deployment Artifacts from Docker Builds: The Key Points at a Glance
Multi-stage builds
Build environment in early stages, artifacts in later stages. FROM scratch AS artifacts for a minimal export.
BuildKit --output
--output type=local,dest=./dist exports directly. No docker cp, no temporary container needed.
Cache mounts
RUN --mount=type=cache,target=/root/.composer persists the package cache beyond layer cache invalidations.
SBOM & provenance
--sbom=true --provenance=true on BuildKit 0.11+ for supply chain security and compliance documentation.