Docker is slow writing to the host file system when using volumes. This makes tasks like npm install, in NodeJS, incredibly painful. How can I exclude the node_modules folder from the volume so the build is faster?
It's common knowledge that Docker's mounted volume support on macOS is pathetically slow (click here for more info). For us Node developers, this means starting up your app is incredibly slow because of the requisite node install command. Well, here's a quick lil' trick to get around that slowness.
First, a quick look at the project:
(source: fredlackey.com)
Long story short, I'm mapping everything in my project's root (./) to one of the container's volumes. This allows me to use widgets, like gulp.watch() and nodemon to automagically restart the project, or inject any new code, whenever I modifiy a file.
This is 50% of the actual problem!
Because the root of the project is being mapped to the working directory within the container, calling npm install causes node_modules to be created in the root... which is actually on the host file system. This is where Docker's incredibly slow mounted volumes kick the project in the nads. As is, you could spend as long as five minutes waiting for your project to come up once you issue docker-compose up.
"Your Docker setup must be wrong!"
As you'll see, Docker is quite vanilla for this lil' project.
First, ye 'ole Dockerfile:
FROM ubuntu:16.04
MAINTAINER "Fred Lackey" <fred.lackey#gmail.com>
RUN mkdir -p /var/www \
&& echo '{ "allow_root": true }' > /root/.bowerrc \
&& apt-get update \
&& apt-get install -y curl git \
&& curl -sL https://deb.nodesource.com/setup_6.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g bower gulp gulp-cli jshint nodemon npm-check-updates
VOLUME /var/www
EXPOSE 3000
And, of course, the beloved docker-compose.yml:
version: '2'
services:
uber-cool-microservice:
build:
context: .
container_name: uber-cool-microservice
command:
bash -c "npm install && nodemon"
volumes:
- .:/var/www
working_dir: /var/www
ports:
- "3000"
As you can see, as-is this test project is lean, mean, and works as expected.... except that the npm install is sloooooooooow.
At this point, calling npm install causes all of the project's dependencies to be installed to the volume which, as we all know, is the host filesystem. This is where the pain comes in.
"So what's the 'trick' you mentioned?"
If only we could benefit from having the root of the project mapped to the volume but somehow exclude node_modules and allow it to be written to Docker's union file system inside of the container.
According to Docker's docs, excluding a folder from the volume mount is not possible. Which, makes sense I guess.
However, it is actually possible!
The trick? Simple! An additional volume mount!
By adding one line to the Dockerfile:
FROM ubuntu:16.04
MAINTAINER "Fred Lackey"
RUN mkdir -p /var/www \
&& echo '{ "allow_root": true }' > /root/.bowerrc \
&& apt-get update \
&& apt-get install -y curl git \
&& curl -sL https://deb.nodesource.com/setup_6.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g bower gulp gulp-cli jshint nodemon npm-check-updates
VOLUME /var/www
VOLUME /var/www/node_modules
EXPOSE 3000
... and one line to the docker-compose.yml file ...
version: '2'
services:
uber-cool-microservice:
build:
context: .
container_name: uber-cool-microservice
command:
bash -c "npm install && nodemon"
volumes:
- .:/var/www
- /var/www/node_modules
working_dir: /var/www
ports:
- "3000"
That's it!
In case you missed it, we added:
VOLUME /var/www/node_modules
and
- /var/www/node_modules
Say what!?!?
In short, the additional volume causes Docker to create the internal hooks within the container (folder, etc.) and wait for it to be mounted. Since we are never mounting the folder, we basically trick Docker into just writing to the folder within the container.
The end result is we are able to mount the root of our project, take advantage of tools like gulp.watch() and nodemon, while writing the contents of node_modules to the much faster union file system.
Quick Note re: node_modules:
For some reason, while using this technique, Docker will still create the node_modules folder within the root of your project, on the host file system. It simply will not write to it.
The original article is on my blog.
Related
What causes yarn/npm install from not installing any packages in a volume?
I have a Dockerfile that has the instruction to RUN yarn install (tested with both NPM and YARN), but the packages node_modules directory inside the container is empty. If I exec -it service_name bash and run the install command manually, it installs the packages correctly.
I've noticed this after a refactor, where I had a Worker service that did the process of installation and a second that ran the development server. Decided to keep all in the same Docker-compose declaration instead but since the issue start happening, it persists. Tried a full reset etc without success (down, rm containers, prune, etc).
The Service in question declared in the Docker-compose file:
node_dev:
build:
context: .
dockerfile: ./.docker/dockerFiles/node.yml
image: foobar/node_dev:latest
container_name: node_dev
working_dir: /home/node/app
ports:
- 8000:8000
- 9000:9000
environment:
- NODE_ENV=development
- GATSBY_WEBPACK_PUBLICPATH=/
volumes:
- ./foobar-blog-ui/:/home/node/app
- ui_node_modules:/home/node/app/node_modules
- ui_gatsbycli_node_module:/usr/local/lib/node_modules/gatsby-cli
- ./.docker/scripts/wait-for-it.sh:/home/node/wait-for-it.sh
command: /bin/bash -c '/home/node/wait-for-it.sh wordpress-reverse-proxy:80 -t 10 -- yarn start'
depends_on:
- mysql
- wordpress
networks:
- foobar-wordpress-network
The related Volumes references in the service:
volumes:
ui_node_modules:
ui_gatsbycli_node_module:
Finally, the Dockerfile that generates the image:
FROM node:8.16.0-slim
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
WORKDIR /home/node/app
RUN apt-get update
RUN apt-get install -y rsync vim git libpng-dev libjpeg-dev libxi6 build-essential libgl1-mesa-glx
RUN yarn global add gatsby-cli
RUN yarn install
Also, tried to yarn install --force --no-lockfile and made sure that it was tested without any package or yarn lock files present in the project root, and vice-versa.
I'm finding this quite odd and definitely a typo somewhere but I haven't been able to spot yet.
The host system is macOS Mojave.
I'd like to mention that if exec -it service_name bash and execute the NPM/YARN install the node_modules is populated with the packages.
Before most tests I did, I've also tried to reset by:
docker-compose stop
docker-compose rm -f
docker volume prune -f
docker network prune -f
And now tested with:
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)
docker volume prune -f
docker volume rm $(docker volume ls -qf dangling=true)
docker network prune -f
docker system prune --all --force --volumes
rm -rf ./.docker/certs/ ./.docker/certs-data/ ./.docker/logs/nginx/ ./.docker/mysql/data
The logs for the particular image:
Building node_dev
Step 1/8 : FROM node:8.16.0-slim
8.16.0-slim: Pulling from library/node
9fc222b64b0a: Pull complete
7d73b1e8f94b: Pull complete
1059045652d5: Pull complete
08cd60b80e4e: Pull complete
b7d875c65da4: Pull complete
Digest: sha256:0ec7ac448d11fa1d162fb6fd503ec83747c80dcf74bdf937b507b189b610756a
Status: Downloaded newer image for node:8.16.0-slim
---> 67857c9b26e1
Step 2/8 : ARG NODE_ENV=development
---> Running in da99a137d733
Removing intermediate container da99a137d733
---> 0f9b718d3f66
Step 3/8 : ARG NPM_TOKEN=3ea44a41-9293-4569-a235-a622ae216d60
---> Running in e339a4939029
Removing intermediate container e339a4939029
---> e47b42008bc3
Step 4/8 : ENV NODE_ENV=${NODE_ENV}
---> Running in fdc09147e9da
Removing intermediate container fdc09147e9da
---> 3b28ab5539d3
Step 5/8 : WORKDIR /home/node/app
---> Running in 44eef1d9293d
Removing intermediate container 44eef1d9293d
---> 2d07ecf3de2e
Step 6/8 : RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc
---> Running in a47d5e22839b
Removing intermediate container a47d5e22839b
---> bd9f896846b7
Step 7/8 : RUN yarn global add gatsby-cli
---> Running in ca3e74d12df4
yarn global v1.15.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "gatsby-cli#2.7.47" with binaries:
- gatsby
Done in 15.51s.
Removing intermediate container ca3e74d12df4
---> bc8d15985ad0
Step 8/8 : RUN yarn install --force --no-lockfile
---> Running in 3f0e35e5487b
yarn install v1.15.2
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Rebuilding all packages...
Done in 0.04s.
Removing intermediate container 3f0e35e5487b
---> 485b9e9bccba
Successfully built 485b9e9bccba
Successfully tagged foobar/node_dev:latest
Changed the Service command to sleep 300s and exect -it and ls -la the /home/node/app/node_modules to find:
.yarn-integrity
And when cat .yarn-integrity I see:
{
"systemParams": "linux-x64-57",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}
You install node packages during build time and installed properly as you see from logs without any error. So the issue with something docker-compose or existing volume.
So one thing that can help you to debug is create volume and then try with docker run instead of docker-compose.
docker volume create my_node_modules
docker run -it --rm -v my_node_modules:/home/node/app foobar/node_dev:lates bash -c "cd /home/node/app;npm list"
So now check the volume by attaching any container that to verify the behaiour
docker run -it --rm -v my_node_modules:/home/test/ node:alpine ash -c "cd /home/test/;npm list"
Or another option is to install packages at run time and then the installed packages will be available new any volume.
command: /bin/bash -c '/home/node/wait-for-it.sh wordpress-reverse-proxy:80 -t 10 -- yarn install && yarn start'
To verify that the packages exit or not try to run without docker-compose.
docker run -it --rm foobar/node_dev:lates bash -c "cd /home/node/app;npm list"
Move yarn install to entrypoint or do not attached volume for node modules.
I found a solution by taking advantage of Docker's cache system through the COPY instruction, that I've set to COPY package.json, package-lock.json and yarn.lock; Which caches the build step and only updates if there is any difference in the files, skipping reinstalling the packages otherwise.
To summarize, the service in the Docker compose remains the same, with the volumes, following the best practices shared in the community for nodejs projects.
We can see in the volumes a bind-mount ./foobar-blog-ui/:/home/node/app that mounts the App source code on the host to the app directory in the container. This allows a fast development environment because the changes we make in the host are immediately populated in the container, not possible to do otherwise.
Finally the named volume for the node_modules that I've named ui_node_modules, which is quite tricky to understand as we started by bind mount the app source code that includes the root where node_modules sits. When npm install runs the node_modules directory is created in the container, correct? But the bind-mount we've declared hides it. So, the named node_modules volume called ui_node_modules solves it by persisting the content of the /home/node/app/node_modules directory into the container and bypassing the hidden bind-mount.
node_dev:
build:
context: .
dockerfile: ./.docker/dockerFiles/node.yml
image: foobar/node_dev:latest
container_name: node_dev
working_dir: /home/node/app
ports:
- 8000:8000
- 9000:9000
environment:
- NODE_ENV=development
- GATSBY_WEBPACK_PUBLICPATH=/
volumes:
- ./foobar-blog-ui/:/home/node/app
- ui_node_modules:/home/node/app/node_modules
- ui_gatsbycli_node_module:/usr/local/lib/node_modules/gatsby-cli
- ./.docker/scripts/wait-for-it.sh:/home/node/wait-for-it.sh
command: /bin/bash -c '/home/node/wait-for-it.sh wordpress-reverse-proxy:80 -t 10 -- yarn start'
depends_on:
- mysql
- wordpress
networks:
- foobar-wordpress-network
volumes:
ui_node_modules:
ui_gatsbycli_node_module:
But the Dockerfile includes the instruction to copy the package*.json files that helps determinate when the Docker cache for the instruction step or layer should be updated.
FROM node:8.16.0-slim
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
# The package files should use Docker cache system
COPY ./foobar-blog-ui/package*.json .
COPY ./foobar-blog-ui/yarn.lock .
RUN yarn global add gatsby-cli
RUN yarn install
RUN apt-get update
RUN apt-get install -y rsync vim git libpng-dev libjpeg-dev libxi6 build-essential libgl1-mesa-glx
EXPOSE 8000
Hope this helps someone else in the future!
I'm running a node.js application that uses the html-pdf module, which in turn relies on phantomjs, to generate PDF files from HTML. The app runs withing a Docker container.
Dockerfile:
FROM node:8-alpine
WORKDIR /mydirectory
# [omitted] git clone, npm install etc....
RUN npm install -g html-pdf --unsafe-perm
VOLUME /mydirectory
ENTRYPOINT ["node"]
Which builds an image just fine.
app.js
const witch = require('witch');
const pdf = require('html-pdf');
const phantomPath = witch('phantomjs-prebuilt', 'phantomjs');
function someFunction() {
pdf.create('some html content', { phantomPath: `${this._phantomPath}` });
}
// ... and then some other stuff that eventually calls someFunction()
And then call docker run <the image name> app.js
When someFunction gets called, the following error message is thrown:
Error: spawn /mydirectory/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs ENOENT
This happens both when deploying the container on a cloud linux server or locally on my machine.
I have tried adding RUN npm install -g phantomjs-prebuilt --unsafe-perms to the Dockerfile, to no avail (this makes docker build fail because the installation of html-pdf cannot validate the installation of phantomjs)
I'm also obviously not a fan of using the --unsafe-perms argument of npm install, so if anybody has a solution that allows bypassing that, it would be fantastic.
Any help is greatly appreciated!
This is what ended up working for me, in case this is helpful to anyone:
FROM node:8-alpine
WORKDIR /mydirectory
# [omitted] git clone, npm install etc....
ENV PHANTOMJS_VERSION=2.1.1
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
RUN apk update && apk add --no-cache fontconfig curl curl-dev && \
cd /tmp && curl -Ls https://github.com/dustinblackman/phantomized/releases/download/${PHANTOMJS_VERSION}/dockerized-phantomjs.tar.gz | tar xz && \
cp -R lib lib64 / && \
cp -R usr/lib/x86_64-linux-gnu /usr/lib && \
cp -R usr/share /usr/share && \
cp -R etc/fonts /etc && \
curl -k -Ls https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-${PHANTOMJS_VERSION}-linux-x86_64.tar.bz2 | tar -jxf - && \
cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs
USER node
RUN npm install -g html-pdf
VOLUME /mydirectory
ENTRYPOINT ["node"]
I had a similar problem, only workaround for me was to download and copy a phantom manualy. This is my example from docker file, it should by the last thing before EXPOSE comand. Btw I use a node:10.15.3 image.
RUN wget -O /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
RUN mkdir /tmp/phantomjs && mkdir -p /usr/local/lib/node_modules/phantomjs/lib/phantom/
RUN tar xvjf /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C /tmp/phantomjs
RUN mv /tmp/phantomjs/phantomjs-2.1.1-linux-x86_64/* /usr/local/lib/node_modules/phantomjs/lib/phantom/
RUN rm -rf /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz && rm -rf /tmp/phantomjs
Don't forget to update your paths. It's only workaround, I didn't have time to figure it out yet.
I came to this question in March 2021 and had the same issue dockerizing highcharts: it worked on my machine but failed on docker run (same spawn phantomjs error). In the end, the solution was to find a FROM node version that worked. This Dockerfile works using the latest Node docker image and almost latest highcharts npm version (always pick up specific npm versions):
FROM node:15.12.0
ENV ACCEPT_HIGHCHARTS_LICENSE YES
# see available versions of highcharts at https://www.npmjs.com/package/highcharts-export-server
RUN npm install highcharts-export-server#2.0.30 -g
EXPOSE 7801
# run the container using: docker run -p 7801:7801 -t CONTAINER_TAG
CMD [ "highcharts-export-server", "--enableServer", "1" ]
Edit/Solution
The below issue was taking place because I was mounting my local files as volumes with the volumes section of the docker-compose. This was a legacy piece of code that I didn't initially notice. Leaving this up in case someone else has a similar issue. Thanks for the help.
Original Question
My Dockerfile (below) utilizes Babel to transpile ES6+ code into NodeJS10 code before it's shipped for prod. When I run docker container export myService..., the file structure is exactly as I expect with my import blah from /elsewhere statements being transpiled to var blah = require('elsewhere').
However, when I run this service locally with docker-compose and exec in with something like docker exec -it d66e623c8695 /bin/bash, I see the ES6+ syntax rather than transpiled code. This forces me to start my service locally with babel-node src/index.js when I'd much prefer to run it post-transpile with node src/index.js.
I haven't been able to wrap my head around why these are different, when starting my container with docker-compose up begins by building my container anew.
Any clarification about how this works would be greatly appreciated.
Dockerfile
FROM node:10.14.1 AS base
LABEL maintainer Pat D "me#me.com"
RUN mkdir /home/app
WORKDIR /home/app
RUN mkdir /procedures
RUN mkdir ./src
COPY procedures ./procedures
COPY src ./src
COPY package.json yarn.lock .sequelizerc jest.integration.json deploy.sh Dockerrun.aws.json.template ./
# Install postgresql so we can run the migrations. -qq and piping is to keep apt-get from spamming with logs
# see https://peteris.rocks/blog/quiet-and-unattended-installation-with-apt-get/
RUN apt-get update &&\
apt-get install -y --no-install-recommends apt-utils
RUN apt-get -qq clean < /dev/null > /dev/null &&\
apt-get -qq update < /dev/null > /dev/null &&\
apt-get -qq install -y curl postgresql libpq-dev build-essential < /dev/null > /dev/null &&\
rm -rf /var/lib/apt/lists/* &&\
rm -rf /var/cache/*
# Install dependencies
FROM base AS dependencies
RUN yarn install --quiet &&\
yarn install --silent && \
cp -R node_modules prod_node_modules
# Build
FROM dependencies AS build
COPY . .
RUN npm run build
RUN rm -rf src
RUN ls
RUN mv dist src
RUN ls
RUN cat src/index.js
# this outputs transpiled code when i build in docker compose
EXPOSE 3030
CMD ["node", "src/index.js"]
docker-compose.yml
version: '3.7'
services:
pat-service:
build:
context: .
target: build
working_dir: /home/app
command: ls src
volumes:
- ./:/home/app
- /home/app/node_modules
ports:
- 3030:3000
environment:
NODE_ENV: development
PGUSER: user
PGHOST: db
PGPASSWORD: password
PGDATABASE: db
PGPORT: 5432
LOGGLY_TOKEN: logglyuuidv4
LOGGLY_SUBDOMAIN: lsd1
STACK_NAME: test
networks:
default:
external:
name: architecture_default
I have docker setup for a node.js web service which creates a data volume for node modules like this:
volumes:
- ./service:/usr/src/service
- /usr/src/service/node_modules
The solution with data volume works fine. But using a data volume, the node_modules are not accessible or visible in my host (not container) which creates other restrictions. For example: I can not run tests in web-storm as they expect node_modules to be present.
Is there any alternative solution other than using data volume to mount node modules which also allows to access them on host?
The docker file looks like:
FROM node:6.3.1-slim
ARG NPM_TOKEN
ARG service
ENV NODE_ENV development
RUN apt-get update && apt-get -y install build-essential git-core python-dev vim
# use nodemon for development
RUN npm install --global nodemon
COPY .npmrc /root/.npmrc
RUN mkdir -p /usr/src/$service
RUN mkdir -p /modules/$service/
COPY sources/$service/package.json /modules/$service/package.json
RUN cd /modules/$service/ && npm install && rm -f /root/.npmrc && mv /modules/$service/node_modules /usr/src/$service/node_modules
WORKDIR /usr/src/$service
COPY sources/$service /usr/src/$service
ENV service=${service}
COPY start_services.sh /root/start_services.sh
COPY .env /root/.env
COPY services.sh /root/services.sh
RUN ["chmod", "+x", "/root/start_services.sh"]
RUN ["chmod", "+x", "/root/services.sh"]
CMD /root/start_services.sh
Specify node_modules as follows:
volumes:
- .:/usr/src/service/
- /usr/src/service/node_modules
I'm developing a webapp and I need node for my development environment.
I don't want a docker production container, but a development one: I need to share files between docker container and local development machines. I don't want to run docker each time I change a source file.
Currently my dockerfile is:
#React development
FROM node:4.1.1-wheezy
MAINTAINER xxxxx
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get -y install sudo locales apt-utils
RUN locale-gen es_ES.UTF-8
RUN dpkg-reconfigure locales
RUN echo Europe/Madrid | sudo tee /etc/timezone && sudo dpkg-reconfigure --frontend noninteractive tzdata
ADD src/package.json /tmp/package.json
RUN cd /tmp && npm install
RUN mkdir -p /src && cp -a /tmp/node_modules /src/
WORKDIR /src
EXPOSE 3000
VOLUME /src
I need directory to put all my source files (share a directory via data volume). I also need to execute npm install in my dockerfile so I get my node_modules directory inside my sources directory (/src/node_modules).
However when I mount a host directory as a data volume, as /src dir already exists inside the container’s image, its contents will be replaced by the contents of /src directory on the host so I don't have my /src/node_modules directory anymore:
docker run -it --volumes-from data-container --name node-dev user/dev-node /bin/bash
My host directory doesn't have node_modules directory because I get it through github and is not sync because it's quite a heavy dir.
My solution is to copy node_modules directory using an ENTRYPOINT directive.
docker-entrypoint.sh:
#!/bin/bash
if ! [ -d node_modules ]; then
cp -a /tmp/node_modules /src/
fi
exec "$#"
2 lines added to dockerfile:
COPY docker-entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Disclaimer: the following has nothing to do with Docker specifically.
I use SSHFS. The Ubuntu Wiki has a pretty good description:
SSHFS is a tool that uses SSH to enable mounting of a remote
filesystem on a local machine; the network is (mostly) transparent to
the user.
Not sure if this is useful in your scenario, but all my hosts run a SSH server anyways so it was a no-brainer for me. The win-sshfs project doesn't seem to be actively developed anymore, but it still runs fine in win 8/10 (though the setup is a little weird). OS X and Linux both have better support for this through FUSE.