Docker + Node.js Best Practices (Combined Summary)¶
1️⃣ Use COPY Instead of ADD¶
Rule:
Default to using the COPY command for moving local files into your Docker image.
Why:
-
ADDhas magic side effects:-
Can download files from URLs
-
Automatically extracts
.tararchives
-
-
These behaviors can cause confusion, unpredictability, and potential security risks.
Best Practice:
-
Use
COPYfor predictable and explicit behavior. -
Only use
ADDwhen you specifically need its extra features like remote file fetching or archive extraction. -
Ignore outdated Docker blogs that recommend
ADDby default.
2️⃣ Don’t Install npm or yarn¶
Rule:
Official Node.js base images already include npm and yarn. Do not waste image space re-installing them.
Why: - Stick to the versions bundled with your specific Node image to ensure compatibility. Only update them if you need a specific bug fix or feature. - Reinstalling or upgrading often causes:
- Version mismatches
- Larger images
- Unexpected dependency issues
Maintenance Tip:
After installing dependencies, always clean up:
RUN npm install && npm cache clean --force
This keeps your image small and efficient, similar to cleaning apt-get caches.
3️⃣ Launch with node, Not npm¶
Rule:
Direct Execution: Use CMD ["node", "app.js"] instead of CMD ["npm", "start"]
Why:
-
Process management:
npmlaunches Node as a subprocess, preventing Node from receiving OS signals (e.g.,SIGTERM) directly. -
Cleaner shutdowns:
Using Node directly allows proper signal handling and graceful container termination. -
Transparency:
node app.jsclearly shows which file is executed, instead of hiding logic insidepackage.json.
Example:
CMD ["node", "server.js"]
4️⃣ Use WORKDIR for Directory Management¶
Rule:
Avoid RUN mkdir and RUN cd. Use the WORKDIR instruction instead.
Why:
-
WORKDIRis a 2-in-1 command:-
Creates the directory if it doesn’t exist
-
Sets it as the active working directory
-
-
Results in cleaner, more readable Dockerfiles
Exception:
Use mkdir manually only if you need specific Linux permissions that WORKDIR cannot provide.
5. Architectural Mindset (Cloud Native)¶
- Statelessness: Ensure your Node app offloads sessions and state to external stores like Redis.
- Small Footprint: Use multi-stage builds (e.g.,
node:20-alpine) to keep production images as small as possible. - Immutability: Treat your containers as disposable; never modify a running container's code manually.
✅ One-Line Takeaway¶
Use
COPY, rely on bundled npm/yarn, clean caches, run Node directly, and useWORKDIRfor clean, predictable, production-ready Node.js Docker images.
----------------¶
------------------¶
Docker + Node.js Base Image Best Practices¶
1️⃣ Use Official Node Images Only¶
Rule:
- Always use official Node images from Docker Hub.
Why:
-
They are stable, secure, and well-maintained.
-
Custom Node builds are rarely necessary unless you’re operating at massive scale (e.g., Google, Netflix).
-
Official images cover nearly all real-world Node.js use cases.
2️⃣ Always Use Even-Numbered (LTS) Node Versions¶
Rule¶
-
Use even-numbered versions (e.g.,
node:18,node:20,node:22). -
Avoid odd-numbered versions (e.g., 19, 21).
Why¶
-
Even versions = LTS (Long-Term Support) → stable and production-ready.
-
Odd versions are experimental and introduce breaking changes.
-
Docker’s
:latesttag may point to unstable or experimental builds.
Best Practices¶
-
❌ Never use
latestin production -
Always pin your Node version in the
FROMline:
FROM node:20
- For maximum predictability, pin down to the patch level:
FROM node:20.10.0
3️⃣ Containers Eliminate the Need for Node Version Managers¶
Benefit:
-
Running Node inside Docker allows you to:
-
Pin exact Node versions per application
-
Ignore host OS Node version differences
-
-
Tools like
nvmornbecome unnecessary.
Result:
-
Your Mac, Windows, or Linux host can run any Node version
-
Your application always runs the exact version defined in the container
4️⃣ Selecting the OS Base: Debian vs Alpine vs Slim¶
| Feature | Debian-based (Default) | Alpine-based (-alpine) | Slim (-slim) |
|---|---|---|---|
| Tag Example | FROM node:20 | FROM node:20-alpine | FROM node:20-slim |
| Image Size | Large (often ~800MB+) | Very small (usually <100MB) | Medium (smaller than Debian, larger than Alpine) |
| Package Manager | apt-get | apk | apt-get |
| Built-in Utilities | Bash, OpenSSL, and many common Linux tools | Very minimal; only essential tools | Very few utilities included |
| Compatibility | Highest compatibility with native npm modules and complex dependencies | Some native or C/C++ npm modules may require extra configuration | Better compatibility than Alpine, less than full Debian |
| Performance | Slower builds and deployments due to size | Faster builds and deployments | Moderate |
| Security Surface | Larger attack surface | Smaller attack surface | Smaller than Debian, larger than Alpine |
| Best For | Legacy apps, complex builds, teams familiar with Debian/Ubuntu | Modern apps, production workloads, fast and secure deployments | When Alpine is incompatible but Debian tooling is required |
| Ease of Use | Easiest and most familiar | Slightly steeper learning curve | Moderate setup effort |
| Industry Preference | Safe default | Preferred minimal image for production | Less common today |
Recommendations: - ✅ Use Debian when migrating legacy applications or dealing with complex dependencies. - ✅ Use Alpine for modern production apps when dependencies support it. - ⚠️ Use Slim only if Alpine doesn’t work and you still need apt-get.
Pro Tip:
Alpine has largely replaced -slim as the industry standard for minimal Node.js images, but always test your npm dependencies before committing to it.
5️⃣ Image Variants on Docker Hub¶
| Variant | Recommendation | Reason |
|---|---|---|
node:<version> | ✅ Safe default (Debian) | Full Debian environment with apt-get, high compatibility, and predictable behavior |
node:<version>-alpine | ✅ Preferred when compatible | Very small image, faster builds, smaller security surface |
node:<version>-slim | ⚠️ Only if Alpine won’t work | Smaller than Debian but still supports apt-get; fewer built-in tools |
node:<version>-onbuild | ❌ Avoid completely | Hidden build steps cause confusion, poor transparency, and debugging issues |
node:latest | ❌ Never use in production | Unpredictable version changes can introduce breaking changes |
3️⃣ Practical Takeaways¶
- Alpine Advantages: minimal size, smaller attack surface, faster builds.
- Alpine Limitations: some npm modules may fail, CVE scanners may not work well, edge cases like nodemon issues.
- Debian Advantages: good compatibility, CI vulnerability scanning works reliably, stable and predictable.
- Debian Limitations: slightly larger, but usually negligible on modern cloud servers.
- Enterprise Considerations: You can run Node binaries on any Linux base (Ubuntu, CentOS, Red Hat) if required.
4️⃣ Version Recommendations¶
- Use even-numbered Node versions (LTS) for production (e.g., 18, 20, 22).
- Avoid odd-numbered versions (experimental).
- Never use
latestin production; always pin your version. - Pin patch versions if you want maximum consistency (e.g.,
node:20.10.0).
7️⃣ Do NOT Use onbuild Images¶
What onbuild Does¶
- Automatically runs instructions (e.g.,
COPY,npm install) from an upstream Dockerfile.
Why to Avoid It¶
-
Hidden behavior makes builds:
-
Hard to understand
-
Hard to debug
-
Hard to customize
-
-
Causes confusion because logic runs outside your Dockerfile
Verdict¶
-
onbuildwas a clever idea but creates more problems than it solves. -
Always write your Dockerfile explicitly.
✅ Final Recommendation (One-Liner)¶
Use official Node images, pin an even LTS version (preferably to patch level), avoid
latestandonbuild, prefer Alpine for production when compatible, and keep your Dockerfiles explicit and predictable.
---------------¶
----------------¶
Here’s a concise, structured Markdown summary of running Node.js apps as non-root users in Docker containers based on the lecture:
Running Node.js in Docker as a Non-Root User¶
1️⃣ Default Behavior¶
- Official Node images run apps as root by default.
- Root in a container does not equal root on the host.
- Containers are isolated, but running as root inside the container is still a security risk.
2️⃣ Why Run as a Non-Root User?¶
- Least privilege principle: Limits the damage if an attacker compromises the container.
- Even if the app is breached, the attacker only gains Node user privileges, not root.
- Reduces risk in production environments.
3️⃣ Built-in Node User¶
- Official Node images include a
nodeuser, but it’s not enabled by default. - Some tasks require root:
- Installing OS packages (
apt-get,yum,apk) - Installing global npm packages (
npm install -g) - Enable the Node user after root-required setup.
4️⃣ Common Challenges¶
| Challenge | Reason | Solution |
|---|---|---|
WORKDIR permissions | Directories created after USER node may still be root-owned | Manually chown directories to node:node |
| Writing files in container | App may need write access to certain directories | Use chown/chmod to give Node user appropriate permissions |
| Global npm installs | Node user cannot install globally | Install as root before switching to Node user |
5️⃣ Dockerfile Example¶
# Use official Node image
FROM node:20-alpine
# Set up dependencies as root
RUN apk add --no-cache git \
&& npm install -g some-global-package
# Create app directory and set permissions
WORKDIR /usr/src/app
RUN chown -R node:node /usr/src/app
# Copy app files
COPY . .
# Switch to non-root Node user
USER node
# Run the app
CMD ["node", "server.js"]
Notes:
-
USER nodeaffectsRUN,CMD, andENTRYPOINTcommands after it is declared. -
For development or special tasks, you can temporarily override with -u flag
docker compose exec -u root app sh.
6️⃣Best Practices and Key Takeaways¶
- Always enable the Node user in production containers.
| Step | Action |
|---|---|
| System Tools | Run apt-get / apk as root. Complete root-required setup first (OS packages, global npm packages). |
| Directory Setup | Use mkdir and chown to give the Node user ownership of the app folder. |
| Dependencies | Run npm install as the Node user to ensure it owns node_modules. |
| Execution | Set USER node before the final CMD. |
-----------------¶
Here’s a concise Markdown summary of your lecture on using the Node user (least privilege) in Docker containers:
Using the Node User in Docker (Least Privilege)¶
1️⃣ Concept¶
-
By default, official Node images run as root inside the container.
-
Root in a container is not the same as root on the host, but running as root still poses unnecessary risk.
-
Best practice: run apps as a non-root user (
node) for security.
2️⃣ Node User in Official Images¶
-
Node images include a built-in
nodeuser not enabled by default. -
Enables least privilege, limiting access even if the app is compromised.
-
Some tasks require root, e.g.,
apt-get,apk,npm install -g.
3️⃣ Common Issues When Using Node User¶
Issue
Cause
Solution
Permission errors on app directories
Directories/files created as root
Use chown to set ownership to node:node
npm install fails
Node user can't write to node_modules
Create directories first and set permissions before switching user
Bind mounts in development (Mac/Windows)
Mounted files owned by root
Use Docker Compose -u node or adjust permissions on host
Workdir creation
WORKDIR creates directories as root
Manually mkdir + chown before switching user
4️⃣ Recommended Workflow¶
# 1. Install system dependencies as root
RUN apt-get update && apt-get install -y ...
# 2. Create app directories and set permissions
RUN mkdir -p /usr/src/app \
&& chown -R node:node /usr/src/app
# 3. Switch to Node user
USER node
# 4. Copy app files and set ownership
COPY --chown=node:node package.json /usr/src/app/
COPY --chown=node:node . /usr/src/app/
# 5. Install app dependencies as Node user
RUN npm install
# 6. Set workdir and run app
WORKDIR /usr/src/app
CMD ["node", "index.js"]
5️⃣ Key Points¶
-
Only
RUN,CMD, andENTRYPOINTare executed as the specifiedUSER. -
WORKDIRandCOPYcan create files as root — adjust ownership withchown. -
Use root temporarily for installing packages, then switch back to Node user.
-
Always test permissions, especially for apps that write to disk, upload files, or use bind mounts.
-
Using the Node user is highly recommended for security and best practices.
This approach ensures least privilege, predictable builds, and safer containers without giving up flexibility for development tasks.
------------------¶
Here’s a concise summary of the lecture on Dockerfile efficiency for Node.js:
Dockerfile Efficiency Tips for Node.js¶
1️⃣ Base Image¶
-
Pin the Node version explicitly (e.g.,
node:20-alpine). -
Alpine is preferred for small images, but may cause issues with legacy apps.
2️⃣ Line Ordering Matters¶
-
Docker builds top to bottom, creating a new layer for each line.
-
Keep Static Layers at the Top: Instructions like
EXPOSE,LABEL, or environment variables that rarely change should be placed near the top of the Dockerfile. - Put Volatile Layers at the Bottom: Your source code changes the most, so the command to copy it should be one of the last steps before your
CMD.
3️⃣ Efficient COPY Strategy¶
he most common mistake is copying your entire project before running npm install. This busts the cache every time you change a single line of code, forcing a slow reinstall of all dependencies.
- The Pro Way:
COPY package*.json ./— Copy only the package files first.RUN npm install— Install dependencies (this layer is now cached).COPY . .— Copy the rest of your source code.
- The Wildcard Trick: Use
COPY package*.json ./orCOPY package.json package-lock.json* ./. - Use
*on lock files to avoid build errors if missing (package-lock*.json). -
Recommended approach:
COPY package*.json ./ RUN npm install COPY . .
4️⃣ Package Manager Usage¶
-
If using system package managers (
apt,apk,yum):-
Run all installs in one line at the top of the Dockerfile.
-
Pin versions where possible.
-
Avoid separate install lines later — caching may cause outdated installs.
-
**`FROM`**: Pinned LTS version (e.g., Alpine).
**`EXPOSE` / Environment variables**: Static configuration.
**OS Packages**: `RUN apt-get update && apt-get install...`
**`WORKDIR`**: Set the app directory.
**`COPY` (Package files)**: The first part of the "Double Copy."
**`RUN npm install`**: Cached until your dependencies actually change.
**`COPY` (Source code)**: The second part of the "Double Copy."