A secure & efficient Node/npm in Docker setup for frontend development

Posted by ryansouthgate on 22 Sep 2025

Intro

I’ve been searching for a secure and efficient Node in Docker setup for a little while now. And I think I’ve found it!

A couple of years ago, I’d install the latest LTS version of Node/npm on my Windows machine and be done with it. In my day job, I could be working on one of 4 different front end projects, and have my own front end projects I’d work on in my spare time.

If they’re not all up-to-date and able to run on the same version of Node/npm, you start to encounter version problems. Installed packages require different versions of the Node runtime, and builds break with ease.

To fix this problem, tools like NVM (Node Version Manager) allow you to install multiple differing versions of Node/npm on the same machine, and scope each project to a specific version. Excellent! Problem solved!

Least Privilege

After switching from Windows to Linux Mint, I became more security-conscious. Instead of installing Node.js globally, I began isolating it using Docker. I didn’t want a possibly malicious package on my machine, that had immediate access to my entire filesystem.

Docker uses containers to isolate your program from the host machine. Meaning if a malicious dependency goes rogue in your container it’s unlikely (if you’ve configured it correctly), to run rampant on your host machine and do damage.

Recent vulnerabilities

More recently, the number of vulnerable/malicious packages in npm seems to be increasing. All the below links are less than 2 weeks old from the date of this post:

While earlier attacks often targeted users of your web app, these newer threats are after your machine and any exposed secrets. Crypto wallets, API keys, and cloud access credentials are all up for grabs if a rogue package gets loose with enough permissions.

My first attempts

My first attempts of creating dockerfiles and compose files were half-baked at best. I’d have to keep chopping and changing commands in the dockerfile, to perform npm install and npm run start:myapp for example. I didn’t have the best grasp on how to get it working for a smooth local development experience.

I’d have node_modules folders on my host machine that I wouldn’t be able to access, and after updating packages in package.json I wouldn’t see an updated package-lock.json (due to the update happening in the container and not being mapped back to the mounted file on the host), which then caused more version mismatch problems.

The Workflow

Here’s the setup I arrived at after some trial and error:

First off, I’ve got a simple Dockerfile:

FROM node:15.12 # A Node image (v15.12 on DockerHub)

WORKDIR /app # The directory where all our application files will live
EXPOSE 4200 6677 # Front end application hosts on the container's port 6677, so we map that to our host machine's port of 4200

ENTRYPOINT [ ]

Here we use the Node:15.12 image. You can swap this out to whatever version of Node you need to use for your application. We set a working directory, and map our ports. Nothing out of the ordinary here.

Now, our docker-compose.yml file is where most of the configuration happens.

Here’s the key to making it work smoothly:

  • Use volumes to mount your package.json, package-lock.json, and node_modules so both your host and your container share the same project files.
  • Set the npm cache location to a mounted folder, so logs and cache data are always accessible and don’t vanish when the container stops.
# run: docker compose run ui start

services:
  ui:
    environment:
      - NPM_CONFIG_CACHE=/app/.npm-cache-docker/ # Store these logs (and cache information) in a mounted location the host machine has access to
    tty: true
    build:
      context: .
      dockerfile: Dockerfile
    network_mode: host
    entrypoint: ["npm"]
    volumes:
      - ./package.json:/app/package.json
      - ./package-lock.json:/app/package-lock.json
      - ./node_modules:/app/node_modules
      - ./:/app

In the file above, the line NPM_CONFIG_CACHE is a great addition. When commands fail for npm (install, audit fix etc), they get output to the console. However, more information gets added to the logs. If we didn’t set this variable, they get sent to a default location, that we haven’t mapped in our volumes. As we’ve set it to somewhere we have mounted, we now get easy access to the logs!

(if you’re doing this, you will want to add the following to your repo’s .gitignore file, to stop this folder being checked in):

# A folder from the Node/npm container, which we don't want in source control
.npm-cache-docker

Usage

It’s really easy.

On a freshly pulled repo, you’ll first need to install the packages (which populates the node_modules folder).

docker compose run --rm ui install

Then to run your app…

docker compose run --rm ui start:myapp 

--rm removes the container after use (if we don’t, we’ll end up with a bunch of orphaned containers). And, ui is the name of the Service in the Docker Compose File (above). And start:myapp is one of the scripts in the package.json file. Usually something like this:

{
    "scripts": {
        "start:myapp": "ng serve --host 0.0.0.0",
        "build": "ng build",
        "build:dev": "ng build --configuration=dev",
        "build:stg": "ng build --configuration=stg",
        "build:prd": "ng build --configuration=prd",
        "lint": "ng lint",
    }
}

And now you’re running your frontend project in Docker!

Safe in the knowledge that any malicious packages haven’t got access to data on the host machine

Benefits of Node/npm in Docker

Some of the benefits of running your projects in Docker:

  1. Environment Consistency - Docker containers ensure that your Node/npm environment is identical across all development, testing, and production systems, minimizing “it works on my machine” issues.
  2. Isolation - Each container is isolated from the host system and other containers, preventing dependency and version conflicts between projects. Also, allowing you to easily use different versions of Node/npm (without using node version managers)
  3. Ease of Deployment - Docker images can be built once and deployed anywhere Docker is available, streamlining app deployment and scaling.
  4. Simplified Onboarding - New developers can get up and running quickly with just Docker installed; no need to manually install Node/npm and dependencies.
  5. Security - Containers can be run with limited privileges and isolated from the host, reducing risk from potentially vulnerable Node modules (as referenced above)

Security Caveats

While containerizing Node.js will significantly reduce local risks, it’s not magic bullet. Remember:

  • CI/CD is still a target. Use minimal permissions/secrets in your containers, and don’t include secrets in build stages where they aren’t needed.
  • Pair with best practices. Use multi-stage builds, firewall rules (use allow lists for known, good, domains) on CI runners, and never expose secrets to steps that don’t need them.

Conclusion

This setup may not fit every project, but it’s made my workflow faster, safer, and almost stress-free. If you use Docker with Node.js/npm in a different way, I’d love to hear about it!

Thanks for reading 🙂



comments powered by Disqus