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 /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 /app/public ./public
COPY /app/.next/standalone ./
COPY /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.envfiles. - 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 |
Related FreeMac guides
- For day-to-day Docker operations, read Docker CLI Commands: Containers, Images, Logs, and Cleanup.
- If your container port is unreachable, read localhost vs 0.0.0.0 in Docker.
- For project-level configuration files, see Front-End Config: Env, Node, Git, ESLint, and TypeScript.
Continue reading
Docker CLI Commands: Containers, Images, Logs, and Cleanup
A practical Docker command guide for checking containers, reading logs, inspecting configuration, entering running containers, managing volumes, and cleaning disk safely.
9 min readlocalhost vs 0.0.0.0 in Docker: What They Mean
Understand why a Dockerized app bound to localhost may be unreachable, when to bind to 0.0.0.0, and how port publishing changes access from the host and network.
9 min readLinux Commands for Files, Processes, Network, and Logs
A practical Linux command guide for developers: files, directories, logs, processes, network checks, SSH, curl, environment variables, and safer cleanup habits.
Subscribe to FreeMac
Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.