Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Docker Copy Directory: docker cp, COPY, and the Trailing Slash

Sean

Platform Writer

Aug 04, 2026
8 min read

Use docker cp for a running container and the COPY instruction for an image build. The trailing slash on the source path decides whether you copy the directory or its contents.

Docker Copy Directory: docker cp, COPY, and the Trailing Slash

This question has two distinct answers and picking the wrong one wastes an afternoon. docker cp moves files in and out of a container that already exists. COPY bakes files into an image at build time. They are not alternatives — they operate at different points in the lifecycle.

The detail that trips up nearly everyone, in both cases, is the trailing slash. It is the difference between ending up with /app/config and /app/config/config.

Table of contents

docker cp: moving directories in and out of a running container

The command works in both directions and behaves like cp -a, copying recursively and preserving permissions where it can.

# Host directory -> container
docker cp ./config my-container:/app/config

# Container -> host
docker cp my-container:/app/logs ./logs

# Copy the contents rather than the directory itself
docker cp ./config/. my-container:/app/config

# Preserve the source UID/GID instead of remapping to root
docker cp -a ./config my-container:/app/config

The container does not need to be running. A stopped container’s filesystem is still accessible, which makes docker cp a good way to retrieve logs or artefacts from something that crashed.

Two limitations worth knowing. You cannot copy between two containers in one command — go via the host. And docker cp writes to the container’s writable layer, so the moment that container is removed the files are gone with it.

The trailing slash rule

This is the single most common source of confusion, and the behaviour follows Unix cp conventions precisely.

  • Source without a trailing slash, destination exists: the directory is copied into the destination, producing /app/config/config
  • Source ending in /.: the contents are copied into the destination, producing /app/config/settings.yml
  • Destination does not exist: it is created, and the source contents land directly inside it
# Assume /app/config already exists in the container

# Result: /app/config/config/settings.yml  (probably not what you wanted)
docker cp ./config my-container:/app/config

# Result: /app/config/settings.yml  (the usual intent)
docker cp ./config/. my-container:/app/config

When a copy silently produces a nested duplicate directory, this is why. Check with docker exec my-container ls -la /app/config immediately after copying rather than discovering it when the application cannot find its configuration.

COPY in a Dockerfile: the build-time answer

For files that should be part of the image, COPY is correct. The result is reproducible, versioned with the Dockerfile, and present in every container started from that image.

# Copies the CONTENTS of src into /app/src
COPY src/ /app/src/

# Multiple sources into one destination directory
COPY package.json package-lock.json /app/

# Set ownership during the copy -- avoids a separate chown layer
COPY --chown=node:node src/ /app/src/

# From an earlier build stage
COPY --from=builder /build/dist /app/dist

In a Dockerfile, always end the destination with a slash when it is a directory. Without one, Docker cannot tell whether you mean a file or a directory, and the inference is not always what you expect.

COPY cannot reach outside the build context — no ../ paths. The context is the directory passed to docker build, and everything copied must live inside it. This is a security boundary, not an oversight.

Prefer COPY over ADD. ADD also unpacks local tar archives and fetches URLs, behaviours that surprise readers of the Dockerfile. Use COPY for files and an explicit RUN curl when you genuinely need to download something.

Layer caching and the ordering that speeds up builds

Each COPY creates a layer, and any change invalidates the cache for that layer and everything after it. Copying your whole project before installing dependencies means every source edit reinstalls all dependencies.

# Slow: any source change busts the dependency cache
COPY . /app
RUN npm ci

# Fast: dependencies only reinstall when the manifests change
COPY package.json package-lock.json /app/
RUN npm ci
COPY . /app

That reordering is often the single largest build-time win available, turning a two-minute rebuild into a five-second one. It costs nothing but attention to ordering.

Pair it with a .dockerignore file. Without one, COPY . /app sends node_modules, .git, and every local artefact into the build context — slowing the build, bloating the image, and busting the cache on files that should not be there at all.

# .dockerignore
.git
node_modules
*.log
.env
dist
.venv
__pycache__

Volumes: when you should not be copying at all

If you are running docker cp repeatedly during development, you are working around the wrong tool. A bind mount makes the host directory visible inside the container live, with no copying at all.

# Host directory mounted into the container, changes visible immediately
docker run -v $(pwd)/src:/app/src my-image

# Read-only, which is the right default for configuration
docker run -v $(pwd)/config:/app/config:ro my-image

# A named volume for data that must outlive the container
docker run -v app-data:/var/lib/app my-image

The rule of thumb: COPY for anything that belongs to the image, bind mounts for source during development, named volumes for data that must survive the container, and docker cp for one-off retrieval and debugging.

Do not use docker cp as a deployment mechanism. Files copied into a running container vanish when it is replaced, which means the next restart quietly reverts your change and the resulting confusion is expensive.

How this fits the rest of the stack

Most of the friction here comes from managing container filesystems by hand — copying files in, wondering why they vanished on restart, rebuilding images to change one config value. RunxBuild takes the Dockerfile and handles the build, the registry, and the deploy, with environment variables and persistent storage as first-class settings rather than files you copy into a live container. The RunxBuild hosting calculator shows the service and its storage as separate line items so the persistent layer is priced before you need it.

Useful related references:

FAQ

Does the container need to be running for docker cp?

No. A stopped container’s filesystem remains accessible, which makes docker cp a practical way to retrieve logs or artefacts from a container that crashed.

Why did docker cp create a nested directory?

The trailing-slash rule. Copying ./config into an existing /app/config places the directory inside it, giving /app/config/config. Use ./config/. as the source to copy the contents instead.

What is the difference between COPY and ADD?

COPY copies files and directories, and nothing else. ADD additionally unpacks local tar archives and downloads URLs. Prefer COPY for predictability, and use an explicit RUN curl when a download is genuinely needed.

Can COPY use a path outside the build context?

No. COPY cannot reference ../ paths. Everything must live inside the directory passed to docker build, which is a deliberate security boundary.

Do files copied with docker cp survive a restart?

A restart, yes — they live in the container’s writable layer. Replacing or recreating the container, no. For anything that must persist, use a volume; for anything that belongs to the image, use COPY in the Dockerfile.

#Docker Copy Directory#Docker#docker cp#Dockerfile#Containers