DockerDockerfileNode.jsDevOps

Dockerfile and .dockerignore for Node.js Apps

Build cleaner Node.js Docker images by controlling the build context, using cache-friendly COPY order, applying multi-stage builds, and keeping secrets out of image layers.

·Updated ·11 min read·Counting...
Dockerfile and .dockerignore for Node.js Apps

A good Dockerfile is only half of a good container build. The .dockerignore file decides what enters the build context, and the Dockerfile decides how that context becomes image layers. For Node.js apps, the most common mistakes are copying too much, invalidating dependency cache too often, and accidentally placing secrets or local build artifacts into the image.

The practical order is: shrink the context, install dependencies from lockfiles, copy source after dependencies, build in a temporary stage, and keep the final runtime image small.

Understand the build context

In this command, the final dot is the build context:

docker build -t my-app:latest .

Dockerfile instructions such as COPY and ADD can only access files inside that context. If the context includes node_modules, build caches, logs, screenshots, .git, or local environment files, Docker still has to consider those files even if the Dockerfile does not copy all of them.

That is why .dockerignore matters before Dockerfile optimization.

A useful .dockerignore

For a typical Node.js or Next.js project, start with:

node_modules
.next
dist
coverage
.git
.env*
*.log
Dockerfile*
docker-compose*.yml
README.md

Adjust this list for the project. If the build truly needs a template file, do not exclude it. Real credentials should not be copied into the image at all.

.dockerignore also supports negation:

*.md
!README.md

The last matching rule wins. For projects with multiple Dockerfiles, Docker also supports Dockerfile-specific ignore files, such as build.Dockerfile.dockerignore.

Cache-friendly dependency installation

Do not copy the whole source tree before installing dependencies. Copy the package manifest and lockfile first:

# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

When only application source changes, Docker can reuse the dependency layer. If you start with COPY . ., every source change can force dependency installation again.

For pnpm, copy pnpm-lock.yaml and use the package manager version declared by your project. For Yarn, copy the relevant lockfile and workspace files. The principle is the same: make dependency layers depend on dependency metadata, not every source file.

Multi-stage builds

Build stages let you keep compilers, source files, and development dependencies out of the final image.

For a Next.js standalone build, the final stage often looks like this:

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs \
  && adduser --system --uid 1001 nextjs

COPY --from=build --chown=nextjs:nodejs /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Do not paste this blindly into every framework. Verify the actual output directory and runtime command for your app.

CMD, ENTRYPOINT, and signal handling

Prefer exec form for the default command:

CMD ["node", "server.js"]

Exec form avoids an unnecessary shell wrapper and usually handles termination signals more clearly. Use ENTRYPOINT when the executable is fixed and the user mainly supplies arguments. Remember that only the last CMD in a Dockerfile takes effect.

Secrets do not belong in image layers

Neither ARG nor ENV is a safe place for real production secrets. Values may appear in build metadata, image history, logs, or the final runtime environment.

Safer patterns:

  • Inject runtime secrets through the deployment platform.
  • Use BuildKit secret mounts for build-time private credentials.
  • Commit .env.example, not real .env files.
  • Rotate credentials immediately if they reached Git history or an image registry.

Do not copy .env into an image and delete it later. Earlier image layers may still contain the file.

Build and inspect

Use these commands during review:

docker build --pull -t my-app:latest .
docker image ls my-app
docker history my-app:latest
docker run --rm -p 3000:3000 my-app:latest

--no-cache is useful for diagnostics, but it should not be your default. If disabling cache makes the build "work," the Dockerfile may still have ordering or dependency issues.

Common problems

Problem Likely cause
COPY cannot find a file The file is outside the build context or ignored by .dockerignore
Dependencies reinstall after every edit Source is copied before lockfiles are used
Final image is large Runtime stage copies full source or development dependencies
Container does not stop cleanly Command uses shell form or wraps the app incorrectly
Secret appears in image history Secret was passed through ARG, ENV, or copied files

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.