
How to manage node modules when you're developing in Docker containers
When I started learning Docker, all the sources I read recommended that I develop my applications natively to avoid unexpected behavior that would disrupt my progress. But at a certain point in my journey, the benefits of developing in containers became too attractive to ignore. Not only would it help with dependency management and maintaining a consistent environment, but without developing in containers, I never truly believed that Docker eliminated the “works on my machine” problem. The transition was surprisingly easy, bar one major headache, which I’ll tackle in this post.
My dream setup is for the application to run in the container while supporting live reloading of persisted application code. Ideally, this code would be stored somewhere on my local machine and somehow be picked up and run by the container. Docker provides decent support for this; I can build and run an image with the app in dev mode, then bind mount the source code files on my local machine. The Dockerfile and docker-compose look something like this:
|
|
|
|
But this fails! The problem is that the bind mounted volume is copied after the build completes, so the node_modules
directory that RUN npm install
generates is overridden before npm run dev
is invoked. Figuring this out took ages—the only information I had was that my dependencies were missing, and I investigated every other possibility before copy pasting all my code into ChatGPT which identified this issue as the culprit. It recommended using an anonymous volume for the node_modules
directory to isolate it from the /usr/src/app
files:
|
|
This gets everything deployed and it manages live reloads, but you need to remember to remove the volume every time you use the down command. If you don’t do this, you’ll leave a bunch of orphaned anonymous volumes on your machine. A potential fix is to name the volume, but this breaks if you need to update your dependencies since it will persist the old ones. I don’t want to deal with all this volume management stuff. Another solution I found is to target just the files you plan to modify, rather than the entire /app
directory:
|
|
I like this solution because it avoids dealing with Docker managed volumes entirely. However, I would have to enumerate all the important files which is another headache. Another option is to move npm install
to the CMD
part of the Dockerfile, but that kills all the performance gained from pre-installing dependencies in the image. The holy grail is something that I can set once and forget forever while I modify my application as if it were running locally. After going through a bunch of posts and articles suggesting the options I already tried, I found this solution that met all my criteria:
|
|
It’s really simple—rather than installing node_modules
in the same directory as the application source code, we install it in its own directory and set the NODE_PATH
environment variable. Then, when we copy all the source files via bind mounting, the node_modules
directory is safe and doesn’t get overridden. Now, in the course of regular development, I can change dependencies and source code however I want and it will be reflected in the application.
If you visit StackExchange, the first solution that ChatGPT provided is still the recommended approach. But even those guys ran into issues related to volume persisted dependencies. At the end of the day, any of these methods will get you where you want to go, as long as you do everything correctly. For Docker beginners like myself, the NODE_PATH
method seems quite ideal since it lets you develop in the way you’re used to developing and it doesn’t introduce new flags and ways to screw yourself for you to remember. I was really scared of developing in containers, but it’s been great so far, and stay posted because I’ll writing about tips and tricks I find to make this easier. ■