← Back to list

Leaner, Smarter, Production-Ready: A Practical Guide to Multi-Stage Docker Builds and Container…

Multi-stage builds, docker run flags decoded, and the three commands every engineer needs to debug a live container — all in one place.

Gunjan Kapoor · 2026-05-23 18:35 · 10 claps · 6.3 min read paywalled
#devops #docker #multisatge #containers
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🥊 · Combat Sports

Leaner, Smarter, Production-Ready: A Practical Guide to Multi-Stage Docker Builds and Container Debugging

Multi-stage builds, docker run flags decoded, and the three commands every engineer needs to debug a live container — all in one place.

Docker Containers DevOps

The Problem With Single-Stage Builds

A standard Dockerfile does everything in one go — install build tools, compile the app, copy source files, set the entrypoint. It works. But the image it produces carries the dead weight of every tool used during the build: compilers, test runners, dev dependencies, intermediate files. None of that belongs in production.

A React app built with a standard single-stage Dockerfile can easily produce an image over 1GB. The actual output you need to serve — the compiled static files — is a few megabytes at most.

A build tool that weighs 400MB has no business riding into production inside your image.

This is the problem multi-stage builds solve.

Multi-Stage Builds: What Changes

Multi-stage builds let you use multiple FROM statements in a single Dockerfile. Each FROM starts a fresh stage with its own isolated filesystem. You compile and build in an early stage, then copy only the finished output into a clean, minimal final stage. The intermediate stages — with all their build tools and temporary files — never make it into the final image.

The result is a production image that contains only what is needed to run the app. Nothing more.

The Dockerfile

# ── Stage 1: build the app ───────────────────────────
FROM node:18-alpine AS installer
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ── Stage 2: serve with nginx ────────────────────────
FROM nginx:latest AS deployer
COPY --from=installer /app/build /usr/share/nginx/html

What each part does

Stage 1 — installer

The AS installer label names this stage so it can be referenced later. This stage uses the full node:18-alpine image to install dependencies and run the build. Everything produced by npm run build lands in /app/build. This stage is heavy — and deliberately so, because it's doing real work.

Stage 2 — deployer

This stage starts completely fresh from nginx:latest — a lightweight web server image. The single COPY --from=installer instruction copies only the compiled output from Stage 1. No node_modules, no source files, no build toolchain. The final image contains nginx plus your static files, and nothing else.

The size difference in practice:

Approach What’s included Typical image size Single-stage (node base) Full Node.js runtime + all deps + source 800MB — 1.2GB Multi-stage (nginx final) nginx + compiled static output only 20MB — 50MB

docker run — Every Flag Decoded

Once the image is built, you launch it with docker run. The flags you pass determine how the container behaves. Let's break down a common command:

docker run -it -dp 3000:3000 multi-stage

docker run

The base command. Tells Docker to create a new container from the specified image and start it. If the image isn’t found locally, Docker automatically pulls it from Docker Hub.

-i — Interactive (keep stdin open)

Keeps the standard input stream open so you can type commands into the container. Without -i, the container cannot receive keyboard input — useful when running a shell session.

-t — Allocate a TTY terminal

Allocates a pseudo-terminal (TTY) inside the container. This gives you a proper shell experience with a visible prompt, line editing, and colour output — similar to SSH-ing into a remote server.

*-i and -t are almost always used together as -it. One opens the pipe; the other makes it feel like a real terminal.*

-d — Detached mode (background)

Runs the container in the background and immediately returns your terminal. Docker prints the container ID and you’re back at your prompt — the container continues running silently.

-p 3000:3000 — Port mapping

Maps a port on your host machine to a port inside the container. The format is host_port:container_port.

Without -p, your application runs inside the container but is completely unreachable from the outside. With -p 3000:3000, any request to localhost:3000 on your machine is forwarded into port 3000 inside the container.

You can map different ports if needed:

docker run -dp 8080:3000 multi-stage
# host port 8080 → container port 3000
# access at localhost:8080

multi-stage — The image name

The name (or name:tag) of the image to run. Docker checks its local image store first. If not found, it pulls from Docker Hub. You can also use the full image path for private registries:

docker run -dp 3000:3000 your-username/multi-stage:v1.0

-it vs -d — Pick one

These flags serve different purposes and are rarely combined intentionally:

The Three Debugging Commands

Once a container is running, these three commands are your primary tools for understanding what’s happening inside.

docker logs — Read the container's output

Every container writes its standard output and standard error to a log stream. docker logs reads it.

# View all logs from a container
docker logs container-name
# Or using the container ID
docker logs container-id

Useful flags:

# Follow logs in real time (like tail -f)
docker logs -f container-name
# Show only the last 50 lines
docker logs --tail 50 container-name
# Show logs with timestamps
docker logs -t container-name
# Combine: follow the last 20 lines with timestamps
docker logs -ft --tail 20 container-name

This is always the first place to look when a container misbehaves. Crash on startup? Check the logs. App throwing errors? Check the logs. Port not responding? Check the logs.

docker exec — Run commands inside a running container

docker exec lets you execute any command inside a container that is already running — without stopping or restarting it.

# Open an interactive shell (most common use)
docker exec -it container-name sh
# Or with bash (if the image includes it)
docker exec -it container-name bash
# Run a single command without entering a shell
docker exec container-name ls /app
docker exec container-name env
docker exec container-name cat /etc/nginx/nginx.conf

Once inside with sh or bash, you can:

  • Inspect the file system to verify your code was copied correctly
  • Check environment variables with env or printenv
  • Test network connectivity with ping, curl, or wget
  • Examine running processes with ps aux
  • Read config files your app is using

Type exit to leave the container shell without stopping the container.

***exec vs run -it:* docker run -it image sh creates a new container and opens a shell in it. docker exec -it container sh enters a container that is already running. For debugging a live deployment, you almost always want exec.

docker inspect — Full container metadata

docker inspect returns a detailed JSON object describing every aspect of a container's configuration and current state.

docker inspect container-name
# or
docker inspect container-id

The output is verbose, but you can filter for exactly what you need using --format with Go template syntax:

# Get the container's IP address
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container-name
# Check port bindings
docker inspect --format '{{json .NetworkSettings.Ports}}' container-name
# See all environment variables
docker inspect --format '{{range .Config.Env}}{{.}}
{{end}}' container-name
# Check the container's restart policy
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' container-name
# See the mounted volumes
docker inspect --format '{{json .Mounts}}' container-name

docker inspect is particularly useful for:

  • Debugging networking issues (wrong IP, port not bound)
  • Verifying environment variables were passed correctly
  • Checking whether volumes are mounted where you expect
  • Diagnosing restart policy and exit code issues

Putting It All Together — A Debugging Workflow

Here is a typical sequence when something goes wrong in a running container:

# 1. Check if the container is running at all
docker ps
# 2. If it's not in the list, check stopped containers too
docker ps -a
# 3. Read the logs — most errors surface here first
docker logs -f my-container
# 4. If the app started but behaves unexpectedly, exec in to investigate
docker exec -it my-container sh
# 5. Inside the container: verify files, env vars, network
ls /app
env
cat /app/config.json
# 6. Exit the shell
exit
# 7. If you need low-level config details (ports, mounts, networking)
docker inspect my-container

Quick Reference

Multi-stage Dockerfile pattern

# Stage 1: build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: production
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80

Key commands

docker run -dp 3000:3000 image — Run container detached with port mapping docker run -it image sh — Start a new container with an interactive shell docker logs container — View stdout/stderr from the container docker logs -f container — Follow logs in real time docker exec -it container sh — Shell into a running container docker exec container env — Print environment variables docker inspect container — Full JSON metadata of a container docker inspect — format ‘…’ container — Extract a specific field docker ps — List running containers docker ps -a — List all containers including stopped ones

docker run flags

-i / — interactive — Keep stdin open -t / — tty — Allocate a pseudo-terminal -d / — detach — Run in the background -p / — publish — Map host port to container port — name — Assign a name to the container -e / — env — Set an environment variable -v / — volume — Mount a host directory into the container — rm — Automatically remove the container when it exits

Build lean. Debug fast. Ship with confidence.


메타데이터
post_id
0135fa976b2f
slug
leaner-smarter-production-ready-a-practical-guide-to-multi-stage-docker-builds-and-container-0135fa976b2f
url
https://medium.com/@gunjankapoor9999/leaner-smarter-production-ready-a-practical-guide-to-multi-stage-docker-builds-and-container-0135fa976b2f
canonical_url
https://medium.com/@gunjankapoor9999/leaner-smarter-production-ready-a-practical-guide-to-multi-stage-docker-builds-and-container-0135fa976b2f
author_url
https://medium.com/@gunjankapoor9999
status
ok
fetched_at
2026-06-09 15:37:30