Docker for Web Developers: Containerizing Full-Stack Applications

Docker eliminates the "it works on my machine" problem by packaging your application with its entire runtime environment. For web developers shipping full-stack applications — a Next.js frontend, a Node.js API, and a PostgreSQL database — Docker provides reproducible environments from development through production. Here is how to containerize effectively.
The Multi-Stage Build Pattern
Multi-stage builds keep your production images lean by separating build dependencies from runtime artifacts. A Next.js application benefits greatly from this approach.
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
The final image contains only the compiled output and production dependencies — no TypeScript sources, no devDependencies, no build tools. Image size drops from 1.2 GB to under 200 MB.
Docker Compose for Local Development
Docker Compose orchestrates multi-service environments. Define your frontend, API, database, and any supporting services in a single file.
# docker-compose.yml
version: '3.8'
services:
frontend:
build:
context: ./frontend
target: deps
ports:
- '3000:3000'
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- NEXT_PUBLIC_API_URL=http://localhost:4000
depends_on:
- api
api:
build: ./api
ports:
- '4000:4000'
volumes:
- ./api:/app
- /app/node_modules
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
ports:
- '5432:5432'
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U user']
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
The volume mounts enable hot-reloading — code changes on your host are reflected instantly inside the container without rebuilds.
Production Optimization
Production images need different treatment. Use .dockerignore to exclude development artifacts, set Node.js to production mode, and configure health checks.
node_modules
.git
.gitignore
*.md
.env.local
.env.development
test/
tests/
cypress/
.vscode/
Add health checks to your production Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"
CI/CD Integration
Build and push images in your CI pipeline. A GitHub Actions example:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build \
--target runner \
--cache-from ${{ vars.REGISTRY }}/app:latest \
--tag ${{ vars.REGISTRY }}/app:${{ github.sha }} \
--tag ${{ vars.REGISTRY }}/app:latest \
.
- name: Push to registry
run: |
docker push ${{ vars.REGISTRY }}/app:${{ github.sha }}
docker push ${{ vars.REGISTRY }}/app:latest
Leverage Docker layer caching by ordering your Dockerfile so that infrequently changing layers (dependencies) come before frequently changing layers (application code).
Secrets Management
Never bake secrets into images. Use Docker secrets in Swarm mode or pass them through environment variables at runtime. For Compose environments, use a .env file excluded from version control.
services:
api:
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
Containerizing Without Overhead
Docker is a developer tool first. A well-configured docker-compose.yml replaces pages of setup documentation. New team members run two commands — docker compose build and docker compose up — and have a fully running environment.
At SoniNow, we containerize every project from day one. Our web development services include Docker-based development environments, CI/CD pipeline setup, and production deployment configuration.
Containerize without friction. Work with SoniNow to make Docker work for your development workflow.
Related Insights

CI/CD Pipeline Design: Automating Build, Test, and Deployment Workflows
A guide to designing CI/CD pipelines that automate build, test, and deployment including GitHub Actions, GitLab CI, environment strategies, and rollback patterns.

CI/CD Pipeline for Next.js: GitHub Actions to Vercel and Docker Deployments
A step-by-step guide to building CI/CD pipelines for Next.js applications using GitHub Actions including automated testing, preview deployments, Docker builds, and production rollouts.

Database Migration Strategies: Zero-Downtime Schema Changes
Learn zero-downtime database migration strategies including expand-contract patterns, online schema changes, backward-compatible migrations, and rollback planning.