How to Copy Files Between a Docker Container and the Host
docker cp copies files and directories in both directions between a container and the local filesystem. It works on running and stopped containers, needs no tools inside the image, and does not interrupt the container.
Syntax
docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH # container to host
docker cp [OPTIONS] SRC_PATH CONTAINER:DEST_PATH # host to container
CONTAINER is a name or ID (a unique ID prefix is enough). Paths inside the container are absolute, or relative to /. Find the container with docker ps, or docker ps -a for stopped ones.
From a container to the host
# A single file into the current directory
docker cp my-app:/var/log/my-app/app.log .
# A single file under a new name
docker cp my-app:/etc/nginx/nginx.conf ./nginx.conf.from-container
# A whole directory
docker cp my-app:/app/reports ./reports
This also works on a container that has exited, which is the easiest way to rescue logs or a crash dump from a container that will not start again.
To get files out of an image without running it, create a container but never start it:
docker create --name tmp my-app:1.0
docker cp tmp:/app/config/default.yaml .
docker rm tmp
From the host into a container
docker cp ./nginx.conf web:/etc/nginx/nginx.conf
docker exec web nginx -t && docker exec web nginx -s reload
docker cp ./scripts web:/opt/scripts
The process inside does not notice the new file by itself. Reload or restart the application as it requires, as in the nginx example.
How paths are resolved
The rules follow cp -a and depend on what exists at the destination:
- File to a path that does not exist: the file is created at that path. The parent directory must exist.
- File to an existing file: the destination is overwritten.
- File to an existing directory: the file is copied into it under its original name.
- Directory to a path that does not exist: the destination directory is created with the source's contents.
- Directory to an existing directory: the source directory is copied into it, so
./reportsends up as./reports/reportsif you run the same command twice. - Directory ending in
/.: only the contents are copied into the destination directory.
docker cp my-app:/app/reports/. ./reports # contents only, no nested folder
Running the same directory copy twice and getting a nested directory the second time is the most common surprise.
Ownership, permissions and symlinks
Copied files belong to the user and group at the destination. Files copied to the host belong to the user running docker cp, files copied into a container belong to root. If the app in the container runs as a non-root user, fix ownership afterwards:
docker cp ./config.yaml my-app:/app/config.yaml
docker exec -u root my-app chown node:node /app/config.yaml
With -a (--archive), docker cp keeps the original UID and GID instead. Symbolic links are copied as links by default. Add -L to copy the files they point to.
What docker cp cannot do
- No wildcards.
docker cp my-app:/var/log/*.log .fails, because there is no shell to expand*. Copy the directory, or use tar throughdocker exec(below). - Some paths are off limits:
/proc,/sys,/dev, tmpfs mounts and mounts created inside the container by its own processes. - No container-to-container copy in one step. Pipe it through the host instead (below).
- Not for live databases. Copying the files of a running database gives you an inconsistent snapshot. Use the database's own dump tool, for example
docker exec my-db pg_dump -U app app > app.sql.
Streaming with tar
When either path is -, docker cp reads or writes a tar archive on stdin or stdout:
# Container directory to a compressed archive on the host
docker cp my-app:/app/data - | gzip > data.tar.gz
# Copy between two containers without a temporary file
docker cp old-app:/app/data - | docker cp - new-app:/app/
When copying into a container from stdin, the destination must be an existing directory and the input must be a tar archive.
For wildcards or filtering, run tar inside the container (the image needs tar, which BusyBox-based images also provide):
mkdir -p logs
docker exec my-app sh -c 'cd /var/log/my-app && tar -cf - *.log' | tar -xf - -C ./logs
Docker Compose and Kubernetes
Compose has the same command, addressed by service name:
docker compose cp web:/var/log/nginx ./nginx-logs
docker compose cp ./nginx.conf web:/etc/nginx/nginx.conf
In Kubernetes, kubectl cp my-pod:/app/data ./data does the same job, but it relies on tar inside the container, so it fails on distroless images.
When docker cp is the wrong tool
docker cp is for one-off transfers: grabbing a log, inspecting a generated file, testing a config change. Anything you copy into a container is lost when the container is recreated. For recurring needs, use the right mechanism:
Bind mount a host directory when files must be shared continuously, for example source code during development or logs collected by a host agent:
docker run -d --name my-app -v "$(pwd)"/config:/app/config:ro my-app:1.0
Named volumes for data the container produces and that must survive it. Read them from the host by mounting the volume into a short-lived container:
docker run --rm -v my-app-data:/data -v "$(pwd)":/backup alpine:3 \
tar -czf /backup/my-app-data.tar.gz -C /data .
COPY in the Dockerfile for files that are part of the application: code, default configuration, static assets. They are then in every container started from the image, versioned with it.
COPY config/default.yaml /app/config/default.yaml
A config change made with docker cp on a server and never committed is lost on the next deploy, and nobody can tell what was changed.
Quick reference
docker cp CONTAINER:/path/file . # container to host
docker cp ./file CONTAINER:/path/file # host to container
docker cp CONTAINER:/path/dir/. ./dir # directory contents only
docker cp -a ./dir CONTAINER:/path # keep UID and GID
docker cp CONTAINER:/path - | gzip > out.tar.gz # stream as tar
docker cp a:/data - | docker cp - b:/ # container to container
docker compose cp SERVICE:/path ./local # Compose
docker create --name tmp IMAGE && docker cp tmp:/path . && docker rm tmp # from an image
