Let's say we're in an environment where we have a modern CI/CD pipeline for a number of containerized apps. We also have a number legacy apps which haven't been containerized yet.

I'm wondering how sound/unusual/crazy the following idea is: even for apps which don't run in containers in production, use container images and a container registry as a vehicle to deploy binary build artifacts.

Here's a very simple example to illustrate the idea: let's say we have a .NET app which consists of two projects. One that rarely changes and contains a large number of static assets (ChangesRarely), and another small, lightweight one which changes often (ChangesOften).

Now, let's say we're using the following Dockerfile to build the app in a multi-stage build:

FROM mcr.microsoft.com/dotnet/sdk:3.1 AS builder
ADD src src
RUN dotnet publish -o buildOutput/ChangesRarely src/ChangesRarely
RUN dotnet publish -o buildOutput/ChangesOften src/ChangesOften

FROM alpine
COPY --from=builder buildOutput/ChangesRarely /buildOutput/ChangesRarely
COPY --from=builder buildOutput/ChangesOften /buildOutput/ChangesOften

Note that because of the two separate COPY instructions, the resulting image will contain two separate layers. This will be important in just a second.

When our CI/CD build system (e.g. GitHub Actions) needs to create a new build, it'll basically run docker build -t myapp . and push the resulting image to a container registry. So far, pretty standard.

Now let's say we want to deploy a new version to production. Again, "production" isn't some sort of container orchestrator (yet) - assume it's a set of plain old servers in a data center. So here's where the approach might become a tad unusual:

Each production server runs a Docker engine- but only to pull the container image and subsequently copy the build artifacts out of it, deploying them to their final destination (some local folder):

id=$(docker create myapp)                 # pulls image from container registry, if necessary
docker cp $id:/buildOutput - | tar zxf -  # copy build artifacts
docker rm -v $id                          # destroy temp container

I've confirmed that this really works. But, why would you do that? Three main reasons:

  • The container registry acts as a unified store for all build artifacts (for containerized and non-containerized apps). No need to manage two different artifact stores.

  • Image layers which are identical across builds (such as the large ChangesRarely layer in the example above) only need to be stored once in the container registry, preserving storage space

  • They also will be cached by the production server's Docker engine, preserving network bandwidth and reducing deployment times.

How usual or unusual is this approach?

Any problems with the approach that I might be missing?


I've seen it done before but wouldn't call it common. There are other artifact servers, and many git repositories have the concept of a release with artifacts that makes more sense.

I suspect Docker Hub is one of many artifact servers to introduce rate limits and other usage caps, so if you do this, make sure you control (or pay for) the registry server. Otherwise the risk is you'll exceed limits and lose access to your artifacts.

If you only want to copy binaries, then I'd skip the base image and use FROM scratch.

It's also possible to bypass the docker engine entirely and pull the artifacts from the registry server directly to the filesystem. I have a regclient project that ships a regctl command with regctl image manifest and regctl layer pull commands. E.g.:

$ regctl image manifest regclient/regctl
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 3019,
    "digest": "sha256:e3abac7c7ce78aaa5efddb5abedd54f8ed3077d9d5b03b5a66ecf9f4c3502454"
  "layers": [
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 930,
      "digest": "sha256:41437cd6a6e3d3098329474c62ccc5bd01fcf6b41bf35f0e8c89db96110b23ac"
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 122413,
      "digest": "sha256:69d6afda22ccf4f223c016bfd3a108fe0a1ddd15c72978bd238b19201e0e1973"
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 161,
      "digest": "sha256:57dd9736a25ab0b55d9d364dc20ee0946a844a8ac72d7f986ef902826f933e89"
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 3963283,
      "digest": "sha256:282c5a3b4b6c8bf0b7038204919bbfc26c8b120cd87f4a1f573c12c808774c1c"

$ regctl layer pull regclient/regctl sha256:282c5a3b4b6c8bf0b7038204919bbfc26c8b120cd87f4a1f573c12c808774c1c | tar -tzvf -
-rwxr-xr-x root/root  10235904 2020-11-05 20:52 regctl

Note that the sha's are unique and persistent, so if that hash is unchanged there's no need to pull the binary again. And the above example has a few extra layers since the command is expected to run in the image as a non-root user (/etc/passwd, ca-certificates, and a home directory). For a non-running image, that could be a single layer. E.g.:

# change image to your scratch-based source
sha=$(regctl image manifest $image --format '{{ (index .Layers 0).Digest}}')
if [ ! -d "$store/$sha" ]; then
  mkdir -p "$store/$sha"
  regctl layer pull $image $sha | tar -xzvf - -C "$store/$sha"
echo "result in $store/$sha"
  • Excellent points, thanks a lot! – Max Nov 16 '20 at 14:45
  • "make sure you control (or pay for) the registry server" - yes, absolutely, great point. In my specific case, Azure Container Registry (ACR) acts as the (paid) registry. – Max Nov 16 '20 at 14:51
  • "If you only want to copy binaries, then I'd skip the base image and use FROM scratch" - yes, indeed. No need for any base image, not even alpine is needed. – Max Nov 16 '20 at 14:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.