Remote Work Tools

A slow internet connection exposes every inefficiency in your Docker workflow. A docker pull nginx:alpine on a 5 Mbps connection takes 30 seconds. A multi-stage build that re-downloads base images because the cache is cold takes minutes. Remote workers on hotel Wi-Fi, rural broadband, or international roaming need Docker to use the network as little as possible.

This guide covers every technique to minimize Docker’s network usage: layer reuse, local registry mirrors, BuildKit cache mounts, and pre-pulling strategies.

Understand What Docker Transfers

# See what each layer costs
docker pull node:20-alpine 2>&1 | grep -E "Pull complete|Downloading|already exists"

# Check local image sizes
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h

# Check total local disk usage
docker system df

# See which images share layers (layers only stored once)
docker history node:20-alpine --no-trunc

Layers are the unit of transfer. If you already have node:20-alpine locally and pull node:20, Docker only transfers the layers that differ — not the full image.

Enable BuildKit

BuildKit is Docker’s next-generation build engine. It handles caching far better than the legacy builder.

# Enable for a single build
DOCKER_BUILDKIT=1 docker build .

# Enable permanently (Docker Desktop: already default)
# For Docker Engine on Linux, add to /etc/docker/daemon.json
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "features": {
    "buildkit": true
  }
}
EOF

sudo systemctl restart docker

# Verify
docker buildx version

Use Cache Mounts in Dockerfile

Without cache mounts, every npm install re-downloads packages from the internet. With cache mounts, they come from a local persistent cache.

# Before — downloads all packages on every build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# After — npm cache persists between builds
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build
# Python (pip cache)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Go module cache
FROM golang:1.22-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o app .

# Rust (cargo cache)
FROM rust:1.77-alpine
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release

These cache mounts persist between builds on the same machine. The first build is slow; all subsequent builds skip the download.

Set Up a Local Pull-Through Registry Mirror

A pull-through cache sits between your Docker client and Docker Hub. First pull goes to Docker Hub; every subsequent pull comes from your local machine. On a team, one person’s pull benefits everyone.

# Run a local registry with pull-through caching
docker run -d \
  --name registry-mirror \
  --restart=unless-stopped \
  -p 5000:5000 \
  -v /opt/registry-mirror:/var/lib/registry \
  -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
  registry:2

# Configure Docker Engine to use the mirror
# /etc/docker/daemon.json
sudo tee /etc/docker/daemon.json << 'EOF'
{
  "registry-mirrors": ["http://localhost:5000"],
  "features": {"buildkit": true}
}
EOF

sudo systemctl restart docker

# Test — first pull fetches from Docker Hub and caches locally
docker pull alpine:3.19
# Second pull is instant — comes from local mirror
docker pull alpine:3.19

On a home lab, run the mirror on a local server so the whole household’s Docker traffic caches locally.

Pre-Pull Base Images Before Going Mobile

Before leaving for a conference, travel day, or a location with poor connectivity, pre-pull every image you need.

# pre-pull.sh — run before traveling
#!/bin/bash
set -e

IMAGES=(
  "node:20-alpine"
  "node:20"
  "python:3.12-slim"
  "golang:1.22-alpine"
  "postgres:16-alpine"
  "redis:7-alpine"
  "nginx:alpine"
  "alpine:3.19"
  "ubuntu:24.04"
)

echo "Pre-pulling ${#IMAGES[@]} images..."
for img in "${IMAGES[@]}"; do
  echo "Pulling $img..."
  docker pull "$img"
done

echo "Done. Total local image storage:"
docker system df
chmod +x pre-pull.sh
./pre-pull.sh

Reduce Image Sizes to Speed Up Pushes

Smaller images transfer faster when pushing to CI or a remote registry.

# Compare Alpine vs full Debian
docker pull node:20           # ~1.1GB
docker pull node:20-alpine    # ~180MB
docker pull node:20-slim      # ~230MB

# Use dive to see which layers are wasteful
brew install dive
dive node:20-alpine

# Clean up intermediate layers in Dockerfile
# BAD — each RUN creates a layer with the apt cache included
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# GOOD — single layer, cache removed before layer is committed
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

Limit Docker’s Bandwidth Usage

On metered connections, cap how much bandwidth Docker can consume:

# Throttle docker pull to 2 Mbps using tc (Linux)
# Get the network interface name
ip link show | grep -E "^[0-9]+" | awk '{print $2}' | tr -d ':'

# Throttle outbound on eth0 to 2 Mbit/s
sudo tc qdisc add dev eth0 root tbf rate 2mbit burst 32kbit latency 400ms

# Remove the throttle when done
sudo tc qdisc del dev eth0 root

On macOS, use the Network Link Conditioner (developer tools) to simulate and test slow connections.

Export/Import Images to Transfer via USB or Scp

When you need an image on a remote machine with no internet:

# Save image to a tar file
docker save nginx:alpine | gzip > nginx-alpine.tar.gz

# Transfer via scp
scp nginx-alpine.tar.gz user@remote-machine:~/

# Load on the remote machine
ssh user@remote-machine "gunzip -c ~/nginx-alpine.tar.gz | docker load"

# Transfer multiple images
docker save node:20-alpine postgres:16-alpine redis:7-alpine | gzip > dev-images.tar.gz

Use .dockerignore to Avoid Sending Large Build Contexts

# .dockerignore — prevent large directories from being sent to Docker daemon
node_modules
.git
.github
dist
build
*.log
.env*
coverage
.cache
__pycache__
*.pyc
*.pyo

# Check current build context size before building
du -sh . --exclude=.git

A large build context (anything above a few MB) is transferred to the Docker daemon over the Unix socket on every build, even locally. A .git directory in a 5-year-old project can be hundreds of MB.

GitHub Actions Cache for CI Builds

# .github/workflows/build.yml
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build with GHA cache
  uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max
    push: true
    tags: ghcr.io/youruser/myapp:latest

The type=gha cache stores Docker layer cache in GitHub Actions Cache storage (10GB free). Subsequent CI builds skip unchanged layers entirely.

Built by theluckystrike — More at zovo.one