docker-cpg: docker cp with wildcards

A wrapper around docker cp that adds wildcards, several sources and excludes, and that also works on stopped containers and images without a shell.

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:

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/log including 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 --parents is 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 .dockerignore parsing.
  • It is tested on Linux with Docker 27.0 and 28.0. --docker podman is not tested.

The code and installation instructions are at github.com/Jwrede/docker-cpg.