// Engineering Log
Secure Development: Part 5 — The Proper Dockerfile
Published on 2026-09-22
// Fast route
This article belongs to the topic Deploy and reliability.
Dockerfile of three lines — FROM node, COPY . ., CMD npm start — works, but in a pipeline such an image builds slowly, is slightly different each time and stops poorly. This section is about proper building: reproducibility, cache, secrets and the process inside the container. Minimal base images, scanning and runtime restrictions are covered in the container security lifecycle part.
Base image: version and digest
The latest tag and tags without a version change without warning: today’s build and tomorrow’s may get different language versions. At minimum — specify the version. More reliably, pin the image by digest, as Docker documentation recommends: then the build will use exactly that image even if the publisher updates the tag.
FROM python:3.13-slim@sha256:<digest>You can find the digest with the command docker buildx imagetools inspect python:3.13-slim. To keep pinned images from getting outdated, they are updated via Dependabot (package-ecosystem: "docker") — it creates merge requests with new tags and digests on a schedule.
The version should be supported. As of September 2026:
- Node.js 18 stopped receiving security updates on April 30, 2025; Node.js 20 — on April 30, 2026; for new images use LTS branches 22 (security fixes until April 30, 2027) and 24.
- Python 3.9 is not supported since October 31, 2025, support for 3.10 ends on October 31, 2026; for new images — 3.12 or 3.13.
Build context and .dockerignore
All contents of the build directory are sent to the builder unless excluded. A .dockerignore file speeds up the build and prevents unnecessary files — including files with passwords — from getting into the image:
.git
node_modules
.env
*.log
distInstruction order and cache
Each instruction creates a layer. If a layer changes, all subsequent layers are rebuilt. Therefore copy dependency files and install them first, and copy the code afterwards:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .Now changing the code does not trigger reinstalling dependencies. npm ci installs exactly what’s recorded in the lockfile — this is reproducible build.
Install system packages in a single instruction with list update and cleanup, as Docker documentation recommends:
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*Multi-stage builds
Compilers, dev-dependencies and sources are needed for building but not for running. Multi-stage builds leave only what’s necessary in the final image:
# syntax=docker/dockerfile:1
FROM node:24-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]The first stage builds the application, the second takes only the ready result and the runtime dependencies. The USER node instruction runs the app as an unprivileged user, which already exists in the official Node.js images.
Build secrets
A token for a private package registry must not be passed via ARG or ENV: those values are stored in the image and visible in its history. For this there are BuildKit secrets — they are mounted for a single instruction and do not end up in layers:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm cidocker build --secret id=npmrc,src=$HOME/.npmrc .By default the secret is available as the file /run/secrets/<id>; the env= parameter allows passing it into an environment variable only for that command.
Process in the container: PID 1 and signals
The container stop command sends SIGTERM to the main process. If CMD is written in shell form (CMD node server.js), the shell becomes the main process and the application may not receive the signal — after a timeout the container is forcibly terminated and in-flight requests are lost.
- Use the exec form:
CMD ["node", "dist/server.js"]. - If a startup script is needed, it should end with
exec "$@"so the app becomes PID 1 and receives signals — this pattern is in Docker documentation. - If the app spawns child processes, run the container with
docker run --init(in Compose —init: true): Docker will add a small init process based on tini, which forwards signals and reaps finished processes.
Linting the Dockerfile
The static analyzer hadolint finds common Dockerfile mistakes — unpinned versions, shell-form commands, unnecessary layers — and is easy to integrate into a pipeline:
docker run --rm -i hadolint/hadolint < DockerfileCommon mistakes
COPY . .at the top of the file. Dependency cache is invalidated by any code change.- No
.dockerignore..git, localnode_modulesand.envget into the image. npm installinstead ofnpm ci. The build may get different package versions than those recorded in the lockfile.- Tokens in
ARGandENV. They can be extracted from the image history. - Shell-form
CMD. The app does not get SIGTERM and cannot shut down cleanly. - Outdated base images. Node.js 18 and Python 3.9 no longer receive security fixes.
// Similar task
If you are dealing with something similar
This article belongs to one of the main working topics. You can keep reading on the topic, go to the homepage to understand what I do, or open the service pages directly.
Article topic
Deploy and reliability
Docker, CI/CD, releases, monitoring, observability, and incident handling.
Typical tasks behind this topic
- Set up deployment without manual chaos
- Add monitoring, alerts, and baseline observability
- Investigate incidents and stabilize production
// Next step
If you need help with this topic, not just another article, it is better to go straight to the service page. The homepage and topic collection stay available as secondary routes.
Open services// Reviews
Related reviews
I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler, more affordable solution. In the end I saved time and money. Mikhail — a true expert who works for the client's result, not for the fee. I recommend him!
I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler and more cost-effective solution. In the end I saved budget and time. Mikhail — a true expert who …
VPS setup, server setup
2026-05-12 · ★ 5/5
Excellent work! Set up the server very quickly, installed the control panel, and configured the IP. Definitely recommend!
Excellent work! Very quickly set up the server, installed the panel, configured the IP I can definitely recommend it!
Everything was excellent; helped promptly and professionally. Thank you — I recommend them to the community.
Everything's great, helped promptly and professionally, thank you, I recommend it to the community
VPS setup, server setup
2026-04-16 · ★ 5/5
There were several issues concerning both the technical side and overall understanding. Mikhail responded quickly, resolved the technical problems, and helped me understand them — many thanks. I'm satisfied with the result.
There were several issues concerning both the technical side and overall understanding. Mikhail responded quickly to the request, helped sort things out and resolved the technical problems and helped clarify …
VPS setup, server setup
2026-02-18 · ★ 5/5
Everything was done quickly and efficiently. I recommend.
Everything was done quickly and efficiently. I recommend.
VPS setup, server setup
2026-01-17 · ★ 5/5
Everything went well; the contractor responded quickly to questions and helped resolve the issue. Thanks!
Everything went well, the contractor responded quickly to questions and helped resolve the issue. Thank you!
VPS setup, server setup
2025-12-16 · ★ 5/5
// Contact
Need help?
Get in touch with me and I'll help solve the problem
I reply within one business day (03:00-13:00 GMT)
Или оставьте заявку здесь:
// Related