Skip to content

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:

  • ADD has magic side effects:

    • Can download files from URLs

    • Automatically extracts .tar archives

  • These behaviors can cause confusion, unpredictability, and potential security risks.

Best Practice:

  • Use COPY for predictable and explicit behavior.

  • Only use ADD when you specifically need its extra features like remote file fetching or archive extraction.

  • Ignore outdated Docker blogs that recommend ADD by 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:
    npm launches 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.js clearly shows which file is executed, instead of hiding logic inside package.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:

  • WORKDIR is 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 use WORKDIR for 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 :latest tag may point to unstable or experimental builds.

Best Practices

  • Never use latest in production

  • Always pin your Node version in the FROM line:

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 nvm or n become 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 latest in 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

  • onbuild was 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 latest and onbuild, 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 node user, 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 node affects RUN, CMD, and ENTRYPOINT commands 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 node user 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


# 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, and ENTRYPOINT are executed as the specified USER.

  • WORKDIR and COPY can create files as root — adjust ownership with chown.

  • 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:
    1. COPY package*.json ./ — Copy only the package files first.
    2. RUN npm install — Install dependencies (this layer is now cached).
    3. COPY . . — Copy the rest of your source code.
  • The Wildcard Trick: Use COPY package*.json ./ or COPY 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."