Developer & Server Workflows

Moving Files Out of Docker Containers and Kubernetes Pods

By SRIFT maintainers · Published

Reviewed by SRIFT maintainers · last reviewed against the SRIFT 4.3.2 source · third-party facts checked 8 October 2026.

Short answer

For Docker, docker cp copies files out of a running or stopped container through the Docker daemon, so the image needs nothing installed. For Kubernetes, kubectl cp works by running tar inside the container, so it fails on distroless and other minimal images; the fallbacks are kubectl exec with cat, an ephemeral debug container, or a volume another pod can read.

When you have no Docker socket or Kubernetes API access at all (a CI-created pod, a customer's cluster), the container can push the file out itself through an outbound link that someone downloads in a browser.

docker cp needs in the image
Nothing; works on stopped containers
kubectl cp needs in the image
A tar binary
Distroless pod
Ephemeral debug container or a shared volume
No cluster access
Outbound link from inside the container

Docker: docker cp and its limits

docker cp asks the Docker daemon to read the container's filesystem directly, so it does not depend on a shell or any tool inside the image. The Docker documentation states that the container can be running or stopped, which makes it the standard way to recover files from a container that crashed on start-up.

Copying out of a container
# file or directory to the current directory
docker cp api:/var/log/app/error.log .
docker cp api:/srv/uploads ./uploads

# stream a directory as a tar archive to stdout
docker cp api:/srv/uploads - | gzip > uploads.tar.gz

# Compose services
docker compose cp api:/var/log/app/error.log .

A few behaviours trip people up. Files copied to the host are owned by the user who ran the command, and -a keeps the original UID and GID. Symbolic links in the source are copied as links unless you pass -L. Resources under /proc, /sys and /dev, tmpfs mounts and mounts created inside the container cannot be copied; Docker suggests running tar through docker exec for those.

The real constraint is access: docker cp needs the Docker daemon, which is root-equivalent on the host. If the container runs on a remote host you can reach over SSH, the Docker CLI can talk to that daemon directly (DOCKER_HOST=ssh://user@host). If you only have a shell inside the container, a redirect works for single files, as long as you do not allocate a TTY:

docker exec api cat /data/report.pdf > report.pdf   # no -t: a TTY can mangle binary output

Kubernetes: kubectl cp and kubectl exec

kubectl cp looks like docker cp but works differently. The API server cannot read a container's filesystem, so kubectl runs tar in the container through an exec session and unpacks the stream locally. The kubectl reference is explicit: it requires the tar binary in the image, and if tar is missing, kubectl cp fails.

kubectl cp and its manual equivalent
kubectl cp prod/api-7d9f8:/var/log/app/error.log ./error.log -c api --retries 5

# what it does under the hood, useful for symlinks or modes
kubectl exec -n prod api-7d9f8 -c api -- tar cf - -C /var/log app | tar xf - -C ./logs

Choose the container with -c in multi-container pods; otherwise kubectl uses the default-container annotation or the first container. --retries (default 0) re-attempts an interrupted copy, which matters for large files over a flaky API-server connection.

kubectl exec with cat

Images without tar sometimes still have cat. Redirecting its output gives you the file, with caveats: never pass -t (a TTY rewrites line endings and corrupts binary data), there is no resume if the stream drops, and everything flows through the API server and kubelet, so very large files are slow. Compare checksums on both sides when the image has sha256sum.

kubectl exec -n prod api-7d9f8 -c api -- cat /data/dump.sql.gz > dump.sql.gz
kubectl exec -n prod api-7d9f8 -c api -- sha256sum /data/dump.sql.gz
sha256sum dump.sql.gz

Distroless images: ephemeral debug containers

Distroless images ship no shell, no tar and no cat, so neither approach above works. Kubernetes' answer is the ephemeral container (stable since v1.25): kubectl debug adds a temporary container with your choice of image to a running pod. With --target, it joins the process namespace of the named container.

Reach a distroless container's files
kubectl debug -it -n prod api-7d9f8 --image=busybox:1.36 --target=api -- sh

# inside the debug container: find the app's PID, then read through /proc
ps
ls /proc/1/root/data/

The Kubernetes documentation notes that container filesystems are visible to other containers sharing the process namespace through /proc/$pid/root, protected only by filesystem permissions. Two practical limits apply. Access through /proc is permission-checked, so a debug container running as a different user than the application may be refused; kubectl debug --profile and your cluster's admission policies decide what the debug container may do. And an ephemeral container cannot be removed once added: it stays in the pod spec until the pod is replaced. The container runtime must also support --target.

From the debug shell you still need a way to get the bytes to your machine. If the debug image has tar, a kubectl exec pipe into the debug container works; another option is to use a debug image that can send the file out itself, shown below.

Volumes, bind mounts and sidecars

If you control how the workload is deployed, the cleanest fix is to write files somewhere outside the container's own filesystem. With Docker, a bind mount puts them straight on the host, and a named volume can be archived by a throwaway container:

docker run --rm --volumes-from db -v "$PWD":/backup alpine \
  tar czf /backup/dbdata.tar.gz /var/lib/postgresql/data

In Kubernetes, a PersistentVolumeClaim can be mounted by a short-lived helper pod that has the tools you need. Remember that a ReadWriteOnce volume can be mounted only by pods on one node at a time, so the helper must be scheduled alongside the existing pod or the workload must be scaled down first. For logs or dumps the application produces routinely, a sidecar that shares an emptyDir with the app and ships files somewhere is the durable solution, but changing a pod spec restarts the pod, so it is a deployment change, not an incident-time fix.

When you cannot kubectl into the cluster at all

Two situations break every method above. A CI-created pod (a Kubernetes executor or ephemeral runner) disappears when the job ends and you have no credentials for its namespace. A customer's cluster belongs to someone else: you can ask them to run a command, but you will never get a kubeconfig. In both cases, the process that can see the file has to push it out.

An outbound streamed link fits well because the container needs only outbound HTTPS. With SRIFT, the command prints a link and then serves the file from inside the container through a connection to srift.app on port 443; nothing is stored on the server, and the recipient downloads in a browser or with srift get.

Inside the container (image with Node 20 or later)
npx -y srift-transfer quick-share /data/dump.sql.gz --encrypt --wait
Inside the container (no Node: standalone binary, needs curl or wget)
curl -fsSL https://srift.app/install.sh | sh
~/.srift/bin/srift quick-share /data/dump.sql.gz --encrypt --wait --wait-timeout 1h

For a customer, a one-off Job that mounts the volume is easier to hand over than a sequence of commands. The link appears in the Job's log, which the customer reads and forwards to you:

export-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: srift-export
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 7200
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: export
          image: node:22-alpine
          command: ["npx", "-y", "srift-transfer", "quick-share", "/data/dump.sql.gz",
                    "--encrypt", "--once", "--wait", "--wait-timeout", "90m"]
          volumeMounts:
            - name: data
              mountPath: /data
              readOnly: true
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: app-data

Points that matter in containers:

  • Embedded mode needs no local port. Where a background daemon cannot start, the command serves the link from its own process using only outbound HTTPS and WSS on port 443.
  • Keep the process alive. A container stops when its main process exits, taking any daemon with it. --wait blocks until the file has been downloaded; with --wait-timeout, the command exits with code 3 if nobody downloaded in time, and the timeout never interrupts a download in progress.
  • npx needs the npm registry. In an egress-restricted namespace, either allow it or use the standalone binary (a musl build exists for Alpine; the installer verifies its SHA-256 checksum).
  • Logs are not private. An --encrypt link carries its key after #k=, so anyone who can read the pod or Job log can download the file. Use --once so the link dies after the first download.

NetworkPolicy, egress proxies and diagnosis

Any push-out method depends on egress. In clusters with default-deny NetworkPolicies, the pod needs an egress rule for port 443 to the destination and for DNS (port 53 to the cluster DNS service), or name resolution fails before anything connects. Standard NetworkPolicy selects destinations by pod, namespace or IP block, not by hostname, so allowing a SaaS endpoint usually means going through an egress proxy or using a network plugin with FQDN-based policies.

The SRIFT CLI honours HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY, including socks5:// and socks5h:// proxies. Behind a TLS-inspecting proxy, set NODE_EXTRA_CA_CERTS to the proxy's CA bundle. Running npx -y srift-transfer doctor in the pod checks DNS, HTTPS, WebSocket, proxy reachability, loopback binding and whether a background daemon can start, then prints which mode will work and how to fix what is blocked.

If the cluster forbids egress to anything outside an allow-list, the honest answer is that a push-out link needs an exception, and an in-cluster object store or a kubectl cp through an approved path may be the better route. The firewall and proxy guide explains the common blocking patterns.

Choosing a method

Methods for getting a file out of a container or pod.
MethodNeeds in the imageNeeds your access toDistrolessBest for
docker cpNothingDocker daemon on the hostYesAny local or SSH-reachable Docker host, including stopped containers
kubectl cptarKubernetes API (exec permission)NoEveryday copies from pods you can exec into
kubectl exec … cat >catKubernetes API (exec permission)NoSingle files when tar is missing
kubectl debug (ephemeral)Nothing (tools come from the debug image)Kubernetes API (ephemeral containers)YesDistroless pods during an incident
Volume / helper pod / sidecarNothing in the app imageDeployment config or PVCYesRecurring exports and planned changes
Outbound link (srift quick-share)Node 20+ or the standalone binary (or a debug image with them)Nothing: someone runs one commandVia a helper or debug imageCI pods and clusters you cannot access

For an AI coding agent working inside a container, the same outbound link is how it can hand you a file; see how AI coding agents deliver files. The CLI tools comparison covers general-purpose alternatives.

Frequently asked questions

Why does kubectl cp fail with an error about tar?

kubectl cp runs tar inside the container to package the files. If the image has no tar binary, as with distroless or scratch-based images, the copy fails. Use kubectl exec with cat if cat exists, or an ephemeral debug container that brings its own tools.

Can I copy files from a stopped Docker container?

Yes. docker cp works on running and stopped containers because the Docker daemon reads the container filesystem directly. It does not work once the container has been removed, and it cannot copy from tmpfs mounts or from /proc, /sys or /dev.

References

  1. docker container cp (Docker)
  2. kubectl cp (Kubernetes)
  3. Ephemeral Containers (Kubernetes)
  4. Debug Running Pods (Kubernetes)
  5. Share Process Namespace between Containers in a Pod (Kubernetes)
  6. Network Policies (Kubernetes)

Use SRIFT from a terminal

One command prints a download link that streams from your machine. Needs only outbound HTTPS; --encrypt keeps the key out of the relay.

npx -y srift-transfer quick-share ./build.tar.gz --encrypt