Docker containers package JavaScript applications with their exact runtime environment, using multi-stage builds to create minimal production images with optimized layer caching for fast, reproducible deployments.
Separate builder and runner stages keep production images small by excluding devDependencies and build tools — reducing a Next.js image from 1.5GB to under 150MB.
Copy package.json and lockfile before source code so dependency installation is cached unless dependencies actually change, saving minutes on each build.
Excluding node_modules, .git, and build artifacts from the build context prevents unnecessary files from slowing down builds and bloating images.
Run as non-root user, pin base image versions, scan for vulnerabilities, and never bake secrets into images — use runtime environment variables instead.
Docker allows you to package an application and its dependencies into a standardized unit called a container. For JavaScript applications, this means shipping your app with the exact Node.js version, system dependencies, and node_modules that were tested, eliminating "works on my machine" problems. Containers are lightweight compared to virtual machines because they share the host operating system kernel.
A Dockerfile is a set of instructions that Docker follows to build an image. Key instructions for JavaScript apps: FROM specifies the base image (e.g., node:20-alpine for a minimal Node.js image), WORKDIR sets the working directory, COPY brings files into the image, RUN executes commands (like npm install), EXPOSE documents which port the app listens on, and CMD defines the default command to run the container.
Multi-stage builds are essential for JavaScript applications. The first stage (builder) installs all dependencies including devDependencies, builds the application (TypeScript compilation, bundling), and generates production artifacts. The second stage (runner) starts from a clean base image, copies only the production dependencies and build output, and sets up the runtime. This dramatically reduces image size — a Next.js app can go from 1.5GB (with all devDependencies) to under 150MB with multi-stage builds.
Docker caches each instruction as a layer. If a layer's inputs have not changed, Docker reuses the cached version. The critical optimization for JavaScript apps is copying package.json and the lockfile before copying source code: COPY package.json pnpm-lock.yaml ./ then RUN pnpm install, then COPY . .. This way, dependency installation is cached unless package.json or the lockfile changes — saving minutes on every build when only source code changed.
Like .gitignore, a .dockerignore file prevents unnecessary files from being sent to the Docker build context. Essential entries include node_modules, .git, .next, dist, and any test or documentation files. Without .dockerignore, Docker sends everything to the build daemon, slowing down builds significantly — especially with a large node_modules directory.
node:20-alpine (around 50MB) is much smaller than node:20 (around 350MB) because Alpine Linux uses musl libc instead of glibc. However, some native npm packages (like sharp for image processing) require additional Alpine packages (apk add) or may not work at all. For production, Alpine is preferred for its small attack surface and size; for debugging, the Debian-based image provides more familiar tools.
Run the application as a non-root user (use the built-in node user in official Node.js images). Pin base image versions to specific digests for reproducibility. Scan images for vulnerabilities with tools like Docker Scout or Trivy. Avoid storing secrets in the image — use environment variables or secret management services at runtime.
An image is a read-only template built from a Dockerfile. A container is a running instance of an image. You build images, you run containers. Multi-stage builds produce small images by separating the build environment from the runtime environment — this is the most important Docker optimization for JavaScript applications.
Fun Fact
The name Docker comes from the analogy of shipping containers — just as standardized containers revolutionized global trade by making it irrelevant what is inside the box, Docker containers make it irrelevant what operating system or dependencies an application needs.