Tools: Docker for Solo Developers: The Only Guide You Need in 2026

Tools: Docker for Solo Developers: The Only Guide You Need in 2026

Why Docker Changes Everything

Docker in 5 Minutes

Installation

Your First Dockerfile

The Commands You'll Use 90% of the Time

Docker Compose: Run Multiple Services

Real-World Tips for Solo Devs

1. Use .dockerignore

2. Multi-stage Builds (Smaller Images)

3. Never Store Secrets in Images

4. Health Checks

Free Deployment Options in 2026

The Docker Mindset

Bottom Line I used to deploy my apps with scp and manual config files. Then I discovered Docker and realized I'd been wasting hours every week. Here's the practical Docker guide I wish I had when starting out. Before Docker, deploying meant: With Docker, you ship a container — a box with your app AND everything it needs. Same box everywhere. That's it. Your app runs in a container. Most real apps need a database, cache, and the app itself. Docker Compose orchestrates all of them. One command. App + database + cache, all running. Keeps your images small and build times fast. Result: 1.2GB → 180MB image. Docker restarts unhealthy containers automatically. With Docker, you can deploy anywhere for free or cheap: Think of containers as immutable units: Docker takes a few days to click. Once it does, you'll never go back to manual deployments. Start with a simple Dockerfile for your next project. Then add Docker Compose when you need a database. You'll wonder how you shipped apps without it. Building something solo? The Freelancer OS Notion Template at guittet.gumroad.com helps you manage your projects, clients, and finances in one place (€19). Check it out. 🚀 Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# macOS / Windows: Install Docker Desktop # Linux: -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # macOS / Windows: Install Docker Desktop # Linux: -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # macOS / Windows: Install Docker Desktop # Linux: -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # Start from an official base image FROM node:20-alpine # Set working directory WORKDIR /app # Copy dependency files first (for caching) COPY package*.json ./ # Install dependencies RUN -weight: 500;">npm ci --only=production # Copy source code COPY . . # Expose port EXPOSE 3000 # Start the app CMD ["node", "server.js"] # Start from an official base image FROM node:20-alpine # Set working directory WORKDIR /app # Copy dependency files first (for caching) COPY package*.json ./ # Install dependencies RUN -weight: 500;">npm ci --only=production # Copy source code COPY . . # Expose port EXPOSE 3000 # Start the app CMD ["node", "server.js"] # Start from an official base image FROM node:20-alpine # Set working directory WORKDIR /app # Copy dependency files first (for caching) COPY package*.json ./ # Install dependencies RUN -weight: 500;">npm ci --only=production # Copy source code COPY . . # Expose port EXPOSE 3000 # Start the app CMD ["node", "server.js"] -weight: 500;">docker build -t my-app . -weight: 500;">docker run -p 3000:3000 my-app -weight: 500;">docker build -t my-app . -weight: 500;">docker run -p 3000:3000 my-app -weight: 500;">docker build -t my-app . -weight: 500;">docker run -p 3000:3000 my-app # Build an image -weight: 500;">docker build -t app-name . # Run a container -weight: 500;">docker run -p 8080:3000 app-name # Run in background (detached) -weight: 500;">docker run -d -p 8080:3000 app-name # List running containers -weight: 500;">docker ps # Stop a container -weight: 500;">docker -weight: 500;">stop container-id # View logs -weight: 500;">docker logs container-id -f # Shell into a running container -weight: 500;">docker exec -it container-id sh # Clean up everything -weight: 500;">docker system prune -a # Build an image -weight: 500;">docker build -t app-name . # Run a container -weight: 500;">docker run -p 8080:3000 app-name # Run in background (detached) -weight: 500;">docker run -d -p 8080:3000 app-name # List running containers -weight: 500;">docker ps # Stop a container -weight: 500;">docker -weight: 500;">stop container-id # View logs -weight: 500;">docker logs container-id -f # Shell into a running container -weight: 500;">docker exec -it container-id sh # Clean up everything -weight: 500;">docker system prune -a # Build an image -weight: 500;">docker build -t app-name . # Run a container -weight: 500;">docker run -p 8080:3000 app-name # Run in background (detached) -weight: 500;">docker run -d -p 8080:3000 app-name # List running containers -weight: 500;">docker ps # Stop a container -weight: 500;">docker -weight: 500;">stop container-id # View logs -weight: 500;">docker logs container-id -f # Shell into a running container -weight: 500;">docker exec -it container-id sh # Clean up everything -weight: 500;">docker system prune -a # -weight: 500;">docker-compose.yml version: "3.9" services: app: build: . ports: - "3000:3000" environment: - DATABASE_URL=postgres://user:pass@db:5432/mydb depends_on: - db - redis db: image: postgres:16 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: postgres_data: # -weight: 500;">docker-compose.yml version: "3.9" services: app: build: . ports: - "3000:3000" environment: - DATABASE_URL=postgres://user:pass@db:5432/mydb depends_on: - db - redis db: image: postgres:16 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: postgres_data: # -weight: 500;">docker-compose.yml version: "3.9" services: app: build: . ports: - "3000:3000" environment: - DATABASE_URL=postgres://user:pass@db:5432/mydb depends_on: - db - redis db: image: postgres:16 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: postgres_data: -weight: 500;">docker compose up -d -weight: 500;">docker compose up -d -weight: 500;">docker compose up -d node_modules .-weight: 500;">git .env *.log dist node_modules .-weight: 500;">git .env *.log dist node_modules .-weight: 500;">git .env *.log dist # Stage 1: Build FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 2: Production (much smaller) FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/server.js"] # Stage 1: Build FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 2: Production (much smaller) FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/server.js"] # Stage 1: Build FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN -weight: 500;">npm ci COPY . . RUN -weight: 500;">npm run build # Stage 2: Production (much smaller) FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/server.js"] # Wrong ENV DATABASE_PASSWORD=supersecret # Right: use environment variables at runtime -weight: 500;">docker run -e DATABASE_PASSWORD=$DB_PASS my-app # Wrong ENV DATABASE_PASSWORD=supersecret # Right: use environment variables at runtime -weight: 500;">docker run -e DATABASE_PASSWORD=$DB_PASS my-app # Wrong ENV DATABASE_PASSWORD=supersecret # Right: use environment variables at runtime -weight: 500;">docker run -e DATABASE_PASSWORD=$DB_PASS my-app HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD -weight: 500;">curl -f http://localhost:3000/health || exit 1 HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD -weight: 500;">curl -f http://localhost:3000/health || exit 1 HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD -weight: 500;">curl -f http://localhost:3000/health || exit 1 - "Works on my machine" disasters - Manual dependency installation on every server - Environment differences between dev, staging, and production - Hours debugging "but it worked locally!" - Railway.app — push Dockerfile, it deploys (free tier available) - Render.com — Docker support, free tier - Fly.io — excellent free tier, global edge - GitHub Actions + VPS — $5/month VPS, full control - Never SSH into a production container to make changes - Fix → rebuild → redeploy - Logs go to stdout/stderr, not files - State goes in volumes or external databases