Docker for Developers: Alternatives, Multi-Stage Builds, and Performance Tips
Docker for Developers: Alternatives, Multi-Stage Builds, and Performance Tips
Docker changed how developers think about environments. But "just use Docker" glosses over the practical details: Docker Desktop's licensing, the performance cost of running containers on macOS, multi-stage build patterns, and the newer alternatives that solve real pain points.
This guide covers what matters for developers using containers in their daily workflow.
Docker Desktop Alternatives
Docker Desktop is free for personal use and small companies (under 250 employees / $10M revenue), but requires a paid subscription for larger organizations. Even setting licensing aside, there are technical reasons to consider alternatives.
Colima (macOS/Linux)
Colima runs Docker containers on macOS using Lima (Linux virtual machines). It's a drop-in replacement for Docker Desktop.
brew install colima docker docker-compose
# Start with default settings (2 CPU, 2 GB RAM)
colima start
# Start with more resources
colima start --cpu 4 --memory 8 --disk 60
# Use Rosetta for x86 emulation on Apple Silicon
colima start --arch aarch64 --vm-type=vz --vz-rosetta
After starting Colima, docker and docker compose commands work exactly as they do with Docker Desktop. The VZ virtualization framework with Rosetta on Apple Silicon provides near-native performance for x86 images.
Verdict: The best Docker Desktop replacement on macOS. Free, fast, and compatible.
Podman (All Platforms)
Podman is Red Hat's Docker-compatible container engine. It runs containers without a daemon (daemonless) and can run rootless by default, which is more secure.
# Install
brew install podman # macOS
sudo dnf install podman # Fedora
sudo apt install podman # Ubuntu
# Start the Podman machine (macOS)
podman machine init
podman machine start
# Use like Docker
podman run -it ubuntu:22.04 bash
podman build -t myapp .
podman compose up # or docker-compose with podman socket
Podman is command-compatible with Docker -- you can alias docker=podman and most workflows work unchanged. It also supports pods (groups of containers that share a network namespace), which mirrors Kubernetes pod semantics.
Verdict: Best for Fedora/RHEL environments and developers who want rootless containers. On macOS, Colima is simpler.
OrbStack (macOS Only)
OrbStack is a commercial Docker Desktop replacement for macOS that emphasizes speed and low resource usage. It starts in about 2 seconds and uses significantly less memory than Docker Desktop.
brew install orbstack
Verdict: The fastest and most polished Docker experience on macOS, but it's a paid product ($8/month after trial). Worth it if Docker performance on macOS is a daily pain point.
Rancher Desktop (All Platforms)
Rancher Desktop provides Docker (via moby/dockerd) or containerd as the container runtime, plus a built-in Kubernetes cluster. It's free and open source.
Verdict: Only worth considering if you need local Kubernetes. For plain Docker, Colima or Podman are simpler.
Comparison Table
| Feature | Docker Desktop | Colima | Podman | OrbStack | Rancher Desktop |
|---|---|---|---|---|---|
| Platform | All | macOS, Linux | All | macOS | All |
| Price | Free / $5-21/mo | Free | Free | $8/mo | Free |
| Startup Time | ~20s | ~10s | ~5s | ~2s | ~30s |
| Memory Usage | High | Medium | Low | Low | High |
| Kubernetes | Yes | Yes (--kubernetes) | No (use kind) | Yes | Yes |
| Rootless | Optional | Optional | Default | N/A | Optional |
| GUI | Yes | No | No (Podman Desktop) | Yes | Yes |
| Docker Compatible | Native | Full | Full (alias) | Full | Full |
Multi-Stage Builds
Multi-stage builds are the most impactful Dockerfile optimization. They separate the build environment (with compilers, build tools, dev dependencies) from the runtime image, dramatically reducing final image size.
Node.js Example
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS runner
WORKDIR /app
# Don't run as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
Go Example (Extreme Size Reduction)
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Stage 2: Minimal runtime
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
The Go binary is statically compiled, so the final image is just the binary and CA certificates -- typically under 20 MB compared to 300+ MB for the full Go image.
Docker Compose for Local Development
Docker Compose defines multi-container environments in a single YAML file. It's the standard way to run databases, caches, and services locally.
# docker-compose.yml (or compose.yml)
services:
app:
build:
context: .
target: builder # Use the build stage for development
ports:
- "3000:3000"
volumes:
- .:/app # Mount source code for live reload
- /app/node_modules # Don't overwrite container's node_modules
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
Key patterns:
- Use
depends_onwithcondition: service_healthyto wait for databases to be ready - Mount source code as a volume for live reload during development
- Use a named volume for database data so it persists across
docker compose down - Use the
targetfield to reference a specific build stage
docker compose up -d # Start all services in background
docker compose logs -f app # Follow app logs
docker compose exec db psql # Connect to database
docker compose down # Stop all services
docker compose down -v # Stop and remove volumes (reset data)
Dev Containers
Dev containers standardize development environments using Docker. Every developer on the team gets the same tools, versions, and configuration regardless of their host OS.
Create .devcontainer/devcontainer.json:
{
"name": "Project Dev Environment",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
],
"settings": {
"editor.formatOnSave": true
}
}
},
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"postCreateCommand": "npm install",
"forwardPorts": [3000, 5432]
}
VS Code's Dev Containers extension opens this automatically. GitHub Codespaces also uses devcontainer.json, so the same config works for cloud development.
When dev containers make sense: Teams with complex environment setup (multiple services, specific tool versions, OS-dependent dependencies). If your project is git clone && npm install && npm run dev, dev containers add overhead without much benefit.
Performance Tips
Layer Caching
Docker builds layers top-down. If a layer changes, all subsequent layers are rebuilt. Order your Dockerfile to maximize cache hits:
# Good: Dependencies change less often than source code
COPY package*.json ./
RUN npm ci
COPY . .
# Bad: Every source change invalidates the npm ci cache
COPY . .
RUN npm ci
.dockerignore
Always include a .dockerignore to prevent unnecessary files from entering the build context:
node_modules
.git
.env
dist
coverage
*.md
.DS_Store
.vscode
Without this, COPY . . sends your entire node_modules and .git directory to the Docker daemon, slowing down every build even if those files aren't used.
Use Alpine Images (With Caveats)
Alpine-based images (node:20-alpine, python:3.12-alpine) are 5-10x smaller than Debian-based ones. But Alpine uses musl libc instead of glibc, which can cause compatibility issues with some native modules.
Rule of thumb: Use Alpine for production images. If you hit compatibility issues, switch to -slim variants (Debian-based but stripped down).
BuildKit
BuildKit is Docker's improved build backend. It enables parallel layer building, better caching, and build secrets.
# Enable BuildKit (default in Docker 23.0+)
export DOCKER_BUILDKIT=1
# Use build secrets (don't bake credentials into layers)
docker build --secret id=npmrc,src=$HOME/.npmrc .
In Dockerfile:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
This mounts the secret during the build step but doesn't persist it in any layer.
The Bottom Line
For Docker Desktop alternatives, use Colima on macOS (free, fast, compatible) or Podman on Linux (rootless, daemonless). Write multi-stage Dockerfiles from the start -- they're not an optimization, they're a best practice. Use Docker Compose for local multi-service development. Adopt dev containers only when environment complexity justifies it. And always write a .dockerignore before your first build. These habits compound into faster builds, smaller images, and fewer "works on my machine" problems.