Front-end World
Front-end World Podcast
Think Twice Before Using COPY --link in Your Docker Builds
0:00
-14:21

Think Twice Before Using COPY --link in Your Docker Builds

A Deep Dive into Docker's COPY --link: When It Helps, When It Hurts, and Why Small Apps Should Be Cautious

Introduction

Docker 25.0 introduced a powerful but often overlooked enhancement: the --link flag for the COPY command. This feature is available only when using BuildKit, Docker’s modern build engine, and it can significantly boost your build performance and reduce disk usage—especially in complex projects.


What Is COPY --link?

Normally, COPY creates new copies of the files—one per layer, even if the files are identical. That means wasted space and slower builds.

When you use COPY --link, Docker preserves hard links between identical files. That way, multiple references to the same file don't occupy extra space in your image. It’s essentially a space-saving technique that makes builds more efficient when file deduplication is possible.

COPY --link tells Docker:

"Copy this folder as its own independent layer. If the source files didn't change, reuse the layer even if earlier steps did."

This creates better cache separation, helping you:

  • Skip unnecessary rebuilds

  • Speed up CI/CD pipelines

  • Push smaller diffs to registries

More info:

  • hard links - a feature of file systems that allow multiple directory entries to refer to the same physical file on a disk.

  • Read more about COPY --link in Docker Docs References.


When Should You Use It?

Use COPY --link in these situations:

  1. Large Shared Libraries
    You have common libraries or packages reused across multiple containers or build stages. This avoids duplicating them physically.

  2. Monorepo Structures
    You copy the same modules or folders in different COPY steps. This prevents Docker from storing multiple identical copies.

  3. Multi-stage Builds
    You're copying files from a previous stage into the final image. If the files already exist and haven’t changed, --link helps save space.

  4. Node Modules / Vendor Folders
    If you COPY these folders for multiple microservices and they share a common dependency tree, --link reduces redundant data.


When Not to Use It

While COPY --link can save space and speed up builds, there are some cases where it's better not to use it:

1. You Plan to Modify the Files Later

Hard links point to the same underlying file data. If you modify a file after copying it with --link, the hard link is broken and a full copy is created anyway.

This defeats the purpose and can lead to unexpected behavior or inconsistent builds.

2. You're Working on a Filesystem That Doesn’t Support Hard Links

Some filesystems — especially remote or mounted volumes like:

  • NFS (Network File System)

  • SMB (Samba/Windows shares)

  • Some Docker volumes or virtual filesystems

do not support hard linking.

If --link is used in these environments, the build may fail or silently skip the optimization.

3. You Require Full File Isolation for Security or Compliance

In environments where each layer must have completely isolated copies of files (e.g., strict audit/compliance pipelines or sandboxed builds):

🔐 Using hard links might be seen as a risk, since files technically reference the same inode on disk.

4. You're Building Images for Distribution Across Different OSes or CI/CD Pipelines

Hard links might behave differently across:

  • Linux vs macOS

  • Local vs cloud build runners

  • CI environments with containerized build agents

💡 Inconsistent behavior might cause subtle bugs, so using the standard COPY is often safer for portability.

5.You're Building Small Front-End Applications

For lightweight front-end apps (like static Next.js, React, or Angular builds), the benefits of --link are often negligible.

⚙️ These builds are typically fast and have small file sets, so the complexity of managing hard links isn’t worth the minimal gain. In fact, it can add more confusion than value.


Example of using Copy --link in Angular Application

🧑‍💻 Git repo
Official Docker angular Sample repo written by me.

https://github.com/kristiyan-velkov/docker-angular-sample/tree/develop

Docker file:

# =========================================
# Stage 1: Build the Angular Application
# =========================================
ARG NODE_VERSION=22.14.0-alpine
ARG NGINX_VERSION=alpine3.21

# Use a lightweight Node.js image for building (customizable via ARG)
FROM node:${NODE_VERSION} AS builder

# Set the working directory inside the container
WORKDIR /app

# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json package-lock.json ./

# Install project dependencies using npm ci (ensures a clean, reproducible install)
RUN --mount=type=cache,target=/root/.npm npm ci

# Copy the rest of the application source code into the container
COPY . .

# Build the Angular application
RUN npm run build 

# =========================================
# Stage 2: Prepare Nginx to Serve Static Files
# =========================================

FROM nginxinc/nginx-unprivileged:${NGINX_VERSION} AS runner

# Use a built-in non-root user for security best practices
USER nginx

# Copy custom Nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy the static build output from the build stage to Nginx's default HTML serving directory

COPY --link --from=builder /app/dist/*/browser /usr/share/nginx/html

# Example without --link
# COPY --chown=nginx:nginx --from=builder /app/dist/*/browser /usr/share/nginx/html

# Expose port 8080 to allow HTTP traffic
# Note: The default NGINX container now listens on port 8080 instead of 80 
EXPOSE 8080

# Start Nginx directly with custom config
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
CMD ["-g", "daemon off;"]

What Will --link Do Here?

In your line:

COPY --link --chown=nginx:nginx --from=builder /app/dist/*/browser /usr/share/nginx/html

the --link flag tells Docker:

“Copy the Angular build files into the Nginx image, but treat this copy as its own independent layer. If these files haven’t changed, reuse this layer — even if earlier steps did change.”

So, if you later change:

  • The Nginx base image version

  • The nginx.conf file

  • Any config at the end of the runner stage

Docker will skip re-copying the Angular build into the runner image if the output of npm run build hasn’t changed.


What Problems Can Occur?

Using --link has some important restrictions:

  1. No access to previous layer files
    The destination directory (/usr/share/nginx/html) must not rely on files or symlinks from earlier steps. The --link layer is isolated — it can't read or depend on what was already there.

  2. Symlinks in target path won’t work
    If /usr/share/nginx/html had symlinks from previous steps, they won’t be followed or preserved.

  3. Cannot mix with layers expecting continuity
    If something in your image expects the html folder to already contain files or a specific structure from a previous step — it will break.

What About File Ownership with COPY --link?

By default, `COPY --link` preserves the original file ownership. In most Node.js builder stages, that means files are owned by `root`. Since the Nginx container runs as the `nginx` user, it only needs read access, which it still has.

Your Build Times

With --link

docker build -t angular-link . # Result: 2.419s total

Without --link (Uncomment the example without link and comment the —link one)

docker build -t angular-no-link . # Result: 1.518s total

🤨 Wait... Isn't --link Supposed to Be Faster?

Yes — but here’s why in your case the no-link version seems faster on a clean build:

On First-Time Builds:

  • Docker has no cache yet.

  • --link adds a small setup overhead to isolate the layer.

  • That extra logic makes the first build with --link look slightly slower.

Where --link Really Shines

Try this simple test to see the difference:

  1. Add this line after the COPY command in your Dockerfile to simulate a change:

# Simulate a later change
ENV TEST_REBUILD=1
  1. Rebuild both versions:

    With --link:

docker build -t angular-link . # Time: 0.32s
  • Without --link:

docker build -t angular-link . # Example: 0.33s

What Happens

  • With --link: Docker skips recopying the Angular build output — it uses the cached layer directly.

  • Without --link: Docker recopies everything from the builder stage, even though nothing changed in the output.

This is where --link shows its true power — smarter caching and faster rebuilds when only later layers are modified.

--link isn’t about speed on first builds — it’s about smart caching and avoiding unnecessary work later.


Final Thoughts

COPY --link is great when it fits the use case:

  • Really Heavy front-end builds (Angular, React, Vue)

  • Multi-stage Dockerfiles

  • CI systems with caching and long pipelines

But for small projects or local dev builds?
I personally suggest to stick with standard COPY.

Use --link intentionally. Measure the results.
And choose what gives your team the real speed-up.


Click here for the 30% discount

By on Amazon.com


❤️ If you like my work, please follow me and subscribe ❤️

Front-end World is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.

Share

Discussion about this episode

User's avatar