*docker and some questions

October 14, 2025

Modern software development requires collaboration, consistency, and reliability. Yet, building the same application across different machines — Windows, macOS, Linux — has always been painful.

Before Docker, developers fought with:

  • “It works on my machine, not on yours.”
  • Dependency conflicts
  • Different OS environments
  • Version mismatches (Node 16 vs 18, Python 3.8 vs 3.10)
  • Global package problems
  • Messy onboarding
  • Inconsistent staging/production environments

Docker solves all these by giving developers a portable, reproducible environment — called a container — that works exactly the same everywhere.

This blog covers:

  • Why Docker exists
  • How Docker works
  • Multi-stage builds
  • Reverse proxying with NGINX
  • SPA routing
  • Static caching
  • Full-stack docker-compose setup
  • Best practices
  • And 20+ DevOps questions with complete answers

This is the exact knowledge used in real companies.


1. The Root Problem Docker Solves

Software depends on the environment, not just the code.

Different machines = different:

  • OS
  • libraries
  • runtimes
  • versions
  • package managers
  • filesystem behavior

Example:

A project may require Node 18, but one dev has Node 16.

Another uses PostgreSQL 13, another uses 15.

One dev is on Linux, another on Windows.

This creates chaos.

Docker introduces standardized environments:

“If it works in the container, it will work everywhere.”

You package your app + dependencies + environment into a container.


2. Dockerfiles: Reproducible Build Environments

A Dockerfile defines how an app is built.

Example: Backend multi-stage Dockerfile

# Stage 1 — Builder
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2 — Runner
FROM node:18-alpine AS runner

WORKDIR /app
COPY package*.json ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 8080
CMD ["node", "dist/server.js"]

This ensures:

  • same Node version everywhere
  • same dependency versions
  • same build output

3. Why Multi-Stage Builds Matter

Multi-stage builds bring:

✓ Smaller final images

You remove build tools, compilers, TypeScript, etc.

✓ More secure images

No unnecessary binaries or source code.

✓ Faster CI/CD

Only runtime files are copied.

✓ Cleaner separation

Build stage ≠ runtime stage.


4. Docker Compose: Orchestrating Multi-Service Apps

Modern apps need multiple services:

  • Frontend (React)
  • Backend (Node)
  • Database (Postgres)
  • Reverse proxy (Nginx)

Docker Compose connects them together.

Example full-stack architecture:

version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    networks:
      - app-network

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: postgres://postgres:mysecretpassword@db:5432/multi-stage-app
      JWT_SECRET: supersecret
      PORT: 8080
    depends_on:
      - db
    networks:
      - app-network

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: mysecretpassword
      POSTGRES_DB: multi-stage-app
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./frontend/dist:/usr/share/nginx/html:ro
    depends_on:
      - frontend
      - backend
    networks:
      - app-network

networks:
  app-network:

volumes:
  db_data:


5. Docker Networking — Why We Use Service Names

Inside Docker:

proxy_pass http://backend:8080;

NOT:

proxy_pass http://localhost:8080;

Because localhost refers to the NGINX container itself, not the backend.

Docker auto-creates DNS records for service names.


6. NGINX: Serving Frontend + Proxying Backend

Production apps should never expose the Vite dev server (5173).

Instead, React/Vite builds static files → NGINX serves them.

nginx.conf

events {}

http {
    gzip on;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;
    gzip_min_length 1024;

    server {
        listen 80;
        root /usr/share/nginx/html;
        index index.html;

        # Cache static assets for 1 year
        location ~* \.(?:js|css|png|jpg|jpeg|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # Reverse proxy for backend API
        location /api/ {
            proxy_pass http://backend:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # SPA fallback
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}


7. React Frontend Multi-Stage Dockerfile

FROM node:18-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]


Questions

Q1 — Why did you use COPY ./package.* ./package.* instead of copying only package.json and package-lock.json explicitly?

What would be the downside of using this wildcard approach in a real enterprise pipeline?


Q2 — Why do we run npm ci instead of npm install in Docker builds?

Explain specifically in terms of:

  • reproducibility
  • caching
  • CI/CD environments

Q3 — How does the Docker layer cache behave with your current COPY order?

Describe which layers will rebuild when:

  1. You change package.json
  2. You change only source files like server.js

Q4 — Why is EXPOSE 8080 not actually exposing a port?

Explain what it really does.


Q5 — What security or optimization improvements could you add to this Dockerfile for production?

Suggest at least two improvements (e.g., non-root user, multi-stage build, etc.)

Q5 — Why does backend copy node_modules from builder?

Because:

  • Avoids reinstalling dependencies
  • Ensures the same dependency tree used during build
  • Faster builds
  • Smaller layers

Q6 — Why do we need absolute paths like /app/dist when copying from another stage?

Because:

  • Each Docker stage has its own filesystem
  • COPY from another stage requires absolute paths
  • WORKDIR of current stage doesn’t apply to previous stages

Q7 — Why shouldn’t we use npm prune --production in builder stage?

Because devDependencies (TypeScript, bundlers, etc.) are needed to build the app.

Pruning too early breaks builds.

Q8 — Why shouldn’t the backend expose 8080:8080 in production?

Because:

  • Directly exposes backend to the internet
  • Bypasses NGINX security
  • Increases attack surface
  • Makes private endpoints public

Backend should only be reachable internally.

Q9 — Why must SPA apps use:

try_files $uri $uri/ /index.html;

Because refreshing /dashboard should not 404.

SPAs rely on client-side routing.

Q10 — Why can static assets be cached for 1 year?

Because filenames are fingerprinted:

main.92af71.js
vendor.39df22.js

They never change.

New build → new filename.

So long caching is safe.