The problem
docker cp copies exactly one path and does not expand wildcards:
$ docker cp "web:/var/log/app/*.log" logs/
Error response from daemon: Could not find the file /var/log/app/*.log in container web
The usual workaround runs tar inside the container and unpacks the stream
on the host: docker exec CONTAINER sh -c 'tar -cf - /path/*.log' | tar -xf -.
This needs a running container that ships sh and tar, so it fails on
stopped containers and on distroless and FROM scratch images.
The request is old:
- moby/moby#7710, "support
wildcards and destination names in
docker cp", has been open since August 2014 and has 165 reactions. A maintainer wrote in 2017 that contributions are welcome but that correct wildcard matching is tricky. The thread offers only workarounds. - Two Stack Overflow questions, Copy folder with wildcard from docker container to host (score 54, about 50k views) and How to copy multiple files from container to host using docker cp (score 20, about 15k views).
- Related open requests ask for several sources in one call (moby#37961, moby#50463) and for excludes (moby#40925, 22 reactions).
How it works
docker-cpg uses the tar stream that
docker cp already offers. To copy out, it takes the fixed directory in front
of the first wildcard (/var/log/app in /var/log/app/*.log), streams it with
docker cp CONTAINER:/var/log/app/. - and writes only the tar members that
match. Nothing runs inside the container, so the container may be stopped and
the image needs no shell. To copy in, the local sources are packed into one tar
stream and sent with docker cp - CONTAINER:DIR. A single source without
wildcards, excludes or other options is passed to docker cp unchanged.
Patterns follow the shell: *, ? and [...] match within one path
component, ** matches any number of directories, and wildcards match names
that start with a dot only if the pattern does. --exclude skips files and
directories, --parents keeps the full source path below the destination, and
- as destination writes one tar stream to stdout. The tool is written in
Python without dependencies. Linked into ~/.docker/cli-plugins, it runs as
docker cpg and passes global flags such as --context on to the docker
calls it makes.
This example from the README copies from a stopped container into a tar stream:
$ docker stop web
$ docker cpg "web:/var/log/app/*.log" - | tar tv
docker-cpg: copied 3 match(es), 3 file(s) to -
-rw-r--r-- 0/0 7 2026-10-30 05:09 access.log
-rw-r--r-- 0/0 6 2026-10-30 05:09 debug.log
-rw-r--r-- 0/0 6 2026-10-30 05:09 error.log
Testing
The suite has 26 tests. The unit tests cover pattern matching (*, ?,
character classes, **, hidden names), exclude patterns, the parsing of
container arguments and usage errors. The integration tests build a small root
filesystem in Python, import it with docker import and create containers from
it that are never started. The image contains no shell and no tar. The cases
include symlinks, a read-only directory, a hard link, two matches with the same
name, an exit status of 1 when nothing matches, dry runs, tar output on stdout
and copying several sources into a container. A symlink that already exists in
the destination is replaced; the tool does not write through it.
CI runs the suite on GitHub's ubuntu-24.04 runners with Python 3.9, 3.12 and
3.13 against Docker 28.0. There the integration tests fail instead of being
skipped when no Docker daemon is reachable. A further step installs the tool as
a CLI plugin, checks that docker --help lists cpg, copies
web:/logs/**/*.log out of a busybox container and runs a dry run through
docker --context default cpg.
Limitations
- Copying out streams the whole fixed directory in front of the first wildcard
and filters on the host.
"web:/var/log/*.log"reads all of/var/logincluding subdirectories, and"web:/**/*.conf"reads the whole container filesystem. - With a wildcard the destination is always a directory. If two matches have
the same name, the first one is copied and the others are skipped with a
warning unless
--parentsis set. - When copying in, the target must be an existing directory (a rule of
docker cp -). - There is no copying between two containers, no brace expansion and no
.dockerignoreparsing. - It is tested on Linux with Docker 27.0 and 28.0.
--docker podmanis not tested.
The code and installation instructions are at github.com/Jwrede/docker-cpg.