Docker build times long with npm global install angular-cli

I have encountered a very strange scenario and was wondering if anyone was able to shed some light on the situation. In short, docker build with npm install -g angular-cli@1.0.0-beta.16 along with the rest of application steps takes forever to build. docker build with that alone in it’s own image, then docker build the rest of the app FROM that first image, together or individually they do not take that long. When I say long, I mean like 2 hours. The individuals take around 5 to 7 min to build.

So before I lay down all the code, a few notes. I have tried this on OS X 10.10.5, OS X 10.11.X, OS X 10.12.X, Arch Linux 4.5.1-1-ARCH, Ubuntu 14.04 LTS (in a vagrant box, and in AWS), as well as some friends have helped me with this on their machines. All same results. I am running Docker 1.12.1. and I’m building FROM alpine:3.4. The version of node is v6.2.0 and npm 3.8.9 (from the alpine packages). I have also tried this with a image that I built nodejs v5.11.1 and npm 3 from source.

  • docker run, docker exec and logs
  • Copying all files from one docker volume to another using wild card characters?
  • How to determine why sigterm was sent to process running inside docker container on mesos?
  • java.net.InetAddress java class doesn't resolve IP on Alpine Docker container
  • How to solve `Building CXX object src/CMakeFiles/qpidcommon.dir/qpid/sys/posix/Condition.cpp.o` while compiling qpid-cpp within Docker Alpine?
  • Custom fonts in rrdgraph in a Docker container running Alpine Linux
  • FROM alpine:3.4
    MAINTAINER First Lastname <user@example.com>
    
    ARG ENV
    
    #Set environment vars
    ENV HOME=/home \
        APP_DIR=/root/app \
        DIST_DIR=/var/www \
        ENV=${ENV} \
        AWS_REGION=us-east-1 \
        NPM_CONFIG_LOGLEVEL=info \
        LANG=en_US.UTF-8 \
        LC_ALL=C.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    #Install runtime packages
    RUN apk --no-cache add \
              ca-certificates \
              nodejs \
              nginx
    
    #Install build time packages
    RUN apk --no-cache add \
            --virtual build-dependencies \
              busybox \
              build-base \
              bzip2 \
              git \
              python-dev \
              libffi-dev
    
    RUN mkdir -p ${APP_DIR}
    WORKDIR ${APP_DIR}
    COPY . ${APP_DIR}
    
    #Bug https://github.com/npm/npm/issues/9863
    RUN cd $(npm root -g)/npm && \
        npm install fs-extra && \
        sed -i -e s/graceful-fs/fs-extra/ -e s/fs\.rename/fs.move/ ./lib/utils/rename.js && \
        rm -fr ${APP_DIR}/node_modules
    
    #install node packages
    RUN npm install -g angular-cli@1.0.0-beta.16 && \
        npm install -g typescript@2.0.3 && \
        npm install && npm install -g --save process-nextick-args && npm cache clean | tee /tmp/npm-install.log
    
    #build project
    RUN ng build  --environment=${ENV}
    RUN mv dist ${DIST_DIR}
    
    #clean up
    RUN rm -fr ${APP_DIR}
    RUN apk del build-dependencies
    
    #nginx config
    COPY ./nginx/nginx.conf /etc/nginx/
    RUN mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled/ && \
        chmod -R 755 /etc/nginx/sites-*
    RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
        ln -sf /dev/stderr /var/log/nginx/error.log
    COPY ./nginx/account-curation.conf /etc/nginx/sites-available/
    RUN ln -s /etc/nginx/sites-available/account-curation.conf /etc/nginx/sites-enabled/
    
    #app port
    EXPOSE 80 81
    
    #start nginx
    ENTRYPOINT [ "nginx" ]
    

    And package.json looks like

    {
      "name": "app-name",
      "version": "0.0.0",
      "license": "MIT",
      "angular-cli": {},
      "scripts": {
        "start": "ng serve",
        "lint": "tslint \"src/**/*.ts\"",
        "test": "ng test",
        "pree2e": "webdriver-manager update",
        "e2e": "protractor"
      },
      "private": true,
      "dependencies": {
        "@angular/common": "2.0.0",
        "@angular/compiler": "2.0.0",
        "@angular/core": "2.0.0",
        "@angular/forms": "2.0.0",
        "@angular/http": "2.0.0",
        "@angular/platform-browser": "2.0.0",
        "@angular/platform-browser-dynamic": "2.0.0",
        "@angular/router": "3.0.0",
        "@angular2-material/button": "2.0.0-alpha.8-2",
        "@angular2-material/card": "2.0.0-alpha.8-2",
        "@angular2-material/checkbox": "2.0.0-alpha.8-2",
        "@angular2-material/core": "2.0.0-alpha.8-2",
        "@angular2-material/grid-list": "2.0.0-alpha.8-2",
        "@angular2-material/icon": "2.0.0-alpha.8-2",
        "@angular2-material/input": "2.0.0-alpha.8-2",
        "@angular2-material/input": "2.0.0-alpha.8-2",
        "@angular2-material/list": "2.0.0-alpha.8-2",
        "@angular2-material/progress-circle": "2.0.0-alpha.8-2",
        "@angular2-material/tabs": "2.0.0-alpha.8-2",
        "@angular2-material/toolbar": "2.0.0-alpha.8-2",
        "angular2-modal": "2.0.0-beta.13",
        "core-js": "2.4.1",
        "es6-shim": "0.35.1",
        "hammerjs": "2.0.8",
        "jquery": "3.1.0",
        "jstree": "3.3.1",
        "localStorage": "1.0.3",
        "rxjs": "5.0.0-beta.12",
        "ts-helpers": "1.1.1",
        "zone.js": "0.6.25"
      },
      "devDependencies": {
        "@types/hammerjs": "^2.0.33",
        "@types/jasmine": "^2.2.30",
        "@types/jquery": "^2.0.32",
        "@types/jstree": "^3.3.32",
        "angular-cli": "1.0.0-beta.16",
        "codelyzer": "0.0.26",
        "jasmine-core": "2.4.1",
        "jasmine-spec-reporter": "2.5.0",
        "karma": "1.2.0",
        "karma-chrome-launcher": "2.0.0",
        "karma-cli": "1.0.1",
        "karma-jasmine": "1.0.2",
        "karma-remap-istanbul": "0.2.1",
        "protractor": "4.0.9",
        "ts-node": "1.2.1",
        "tslint": "3.13.0",
        "typescript": "2.0.3"
      }
    }
    

    I’m pointing this out because it already has all the packages I’m trying to install globally (same version) but when this installs it’s much quicker. So this takes around 2 hours to build. Insane right? So after fooling with this for days I can’t figure out why. I decided in light of time I’d build a base image with the global installs in there, then build the project image FROM that one. That way the CI build job would not take hours on every push to this repositories branch. But when I did this, it magically got faster. Around 7min for the base image, and 5 for the app image. It looked something like this.

    Base Image:

    FROM alpine:3.4
    MAINTAINER First Lastname <user@example.com>
    
    #Set environment vars
    ENV NPM_CONFIG_LOGLEVEL=info \
        LANG=en_US.UTF-8 \
        LC_ALL=C.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    #Install runtime packages
    RUN apk --no-cache add \
              ca-certificates \
              nodejs
    
    #Install build time packages
    RUN apk --no-cache add \
            --virtual build-dependencies \
              busybox \
              build-base \
              bzip2 \
              git \
              python-dev \
              libffi-dev
    
    #Bug https://github.com/npm/npm/issues/9863
    RUN cd $(npm root -g)/npm && \
        npm install fs-extra && \
        sed -i -e s/graceful-fs/fs-extra/ -e s/fs\.rename/fs.move/ ./lib/utils/rename.js
    
    #install node packages
    RUN npm install -g angular-cli@1.0.0-beta.16 && \
        npm install -g typescript@2.0.3 && \
        npm install -g --save process-nextick-args && npm cache clean | tee /tmp/npm-global-install.log
    
    RUN apk del build-dependencies
    
    ENTRYPOINT [ "/bin/ash" ]
    

    Application Image

    FROM company/application_base:latest
    MAINTAINER First Lastname <user@example.com>
    
    ARG ENV
    
    #Set environment vars
    ENV APP_DIR=/root/app \
        DIST_DIR=/var/www \
        ENV=${ENV} \
        AWS_REGION=us-east-1 \
        NPM_CONFIG_LOGLEVEL=info \
        LANG=en_US.UTF-8 \
        LC_ALL=C.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    #Install runtime packages
    RUN apk --no-cache add \
              ca-certificates \
              nginx
    
    #Install build time packages
    RUN apk --no-cache add \
            --virtual build-dependencies \
              busybox \
              build-base \
              bzip2 \
              git \
              python-dev \
              libffi-dev
    
    RUN mkdir -p ${APP_DIR}
    COPY . ${APP_DIR}
    WORKDIR ${APP_DIR}
    
    #install node packages
    RUN npm install && npm cache clean | tee /tmp/npm-install.log
    
    #build project
    RUN ng build && mv dist ${DIST_DIR}
    
    #clean up
    RUN rm -fr ${APP_DIR} && apk del build-dependencies
    
    #nginx config
    COPY ./nginx/nginx.conf /etc/nginx/
    RUN mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled/ && \
        chmod -R 755 /etc/nginx/sites-* && \
        ln -sf /dev/stdout /var/log/nginx/access.log && \
        ln -sf /dev/stderr /var/log/nginx/error.log
    COPY ./nginx/account-curation.conf /etc/nginx/sites-available/
    RUN ln -s /etc/nginx/sites-available/account-curation.conf /etc/nginx/sites-enabled/
    
    #app port
    EXPOSE 80 81
    
    #start nginx
    ENTRYPOINT [ "nginx" ]
    

    I do not know nodejs that well so something I tried to avoid installing angular-cli twice, was npm install the one from the package.json then mv it from the /path/to/app/node_modules/.bin/ng to the /usr/bin also just adding the /path/to/app/node_modules/.bin to my path, both resulted in a ng not found.

    Not sure it’s related but I will call it anyone, I keep getting this annoying npm gyp permissions issue only for globally installed packages. gyp WARN EACCES user "nobody" does not have permission to access the dev dir They where just warnings but I thought I would google it and see what it was about. Just to rule out possibilities I tried suggested approaches to fix that from posts like https://docs.npmjs.com/getting-started/fixing-npm-permissions
    and
    https://github.com/nodejs/node-gyp/issues/454
    Nothing I could find worked.

    Something else I tried is messing with the amount of ram and cpu’s that docker can utilize. such as docker build --build-arg ENV=dev --cpuset-cpus "0-5" --no-cache -t company/app_name:0.1.0 ..

    I really do not want to maintain two Dockerfile‘s, especially when I feel like there is something really dumb I am missing. Because I know they work much better when apart. What in the heck could cause this to increase the build time by so much when it’s all together.

  • Why do my volumes sometime not get mounted in my Docker container when using fig?
  • How to access lower layer files in docker?
  • Allowing Docker container to access Postgres running on localhost
  • How can I make a host directory mount with the container directory's contents?
  • how to use sed command with docker run to replace text on a .conf file for nginx docker image?
  • libcontainer - system programmers perspective
  • 2 Solutions collect form web for “Docker build times long with npm global install angular-cli”

    The “all” image build takes a long time due to issues with execSync and node-zopfli building their native modules very slowly. Both are optional dependencies so the install survives them not working but they take a looooong time to fail.

    The divided image build makes both those two packages fail to build quickly. I’m really not sure how that’s happened though as there appear to be a number of changes that can cause them to fail to build quickly.

    execAsync

    The execSync module shouldn’t even exist anymore, it’s for node 0.10.
    If I remove the npm bug fix from the Dockerfile, the execAsync build fails immediately rather than taking a long time to fail.

    node-zopfli

    To remove the permission issues use npm install --unsafe-perm to allow the build to run as the root user in the container.

    devDependencies

    Use either the devDependencies or the global installs for your ng build, not both. An npm install --production will remove the devDependencies from the app install which means angular-cli is not duplicated any more. If you need more of the devDependencies to complete the app build then you might want to go the other way, and not do the global installs but rely just on the devDepenencies (./node_modules/.bin/ng)

    Other Notes

    Use FROM mhart/alpine-node:6 to get a recent version of node/npm and don’t install nodejs via apk.

    Set the ARG ENV and ENV ENV as late as possible (i.e. just before the ng command that uses it) so that an environment change doesn’t trigger a complete rebuild of the image.

    There is no image size to gain (or lose really) when running clean up steps in a later Dockerfile command like RUN apk del build-dependencies. The files are already committed to a previous image layer by this time.

    When doing repeated docker image builds, use something like verdacio/npm-register for npm and/or apt-cacher-ng for generic os packages. They will remove most of the network overhead for repeated docker builds.

    Dockerfile

    So you end up with something like this:

    FROM mhart/alpine-node:6.7
    MAINTAINER First Lastname <user@example.com>
    
    # Set environment vars
    ENV HOME=/root \
        APP_DIR=/root/app \
        DIST_DIR=/var/www \
        AWS_REGION=us-east-1 \
        NPM_CONFIG_LOGLEVEL=info \
        LANG=en_US.UTF-8 \
        LC_ALL=C.UTF-8 \
        LANGUAGE=en_US.UTF-8
    
    # Install packages
    RUN apk --no-cache add \
            --virtual build-dependencies \
            ca-certificates \
            nginx \
            build-base \
            bzip2 \
            git \
            python-dev \
            libffi-dev
    
    RUN mkdir -p ${APP_DIR}
    WORKDIR ${APP_DIR}
    COPY . ${APP_DIR}
    
    # Install node packages
    RUN set -uex ;\
        npm install -g angular-cli@1.0.0-beta.16 typescript@2.0.3 process-nextick-args ;\
        npm install --production --unsafe-perm ;\
        npm cache clean
    
    ARG ENV
    ENV ENV=${ENV}
    
    # Build project
    RUN set -uex ;\
        ng build  --environment=${ENV} ;\
        mv dist ${DIST_DIR} ;\
        rm -fr ${APP_DIR}
    
    # nginx config
    COPY ./nginx/nginx.conf /etc/nginx/
    RUN set -uex ;\
        mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled/ ;\
        chmod -R 755 /etc/nginx/sites-* ;\
        ln -sf /dev/stdout /var/log/nginx/access.log ;\
        ln -sf /dev/stderr /var/log/nginx/error.log
    COPY ./nginx/account-curation.conf /etc/nginx/sites-available/
    RUN ln -s /etc/nginx/sites-available/account-curation.conf /etc/nginx/sites-enabled/
    
    # App port
    EXPOSE 80 81
    
    # Start nginx
    ENTRYPOINT [ "nginx" ]
    

    There’s no problem with having two Dockerfiles either. As each should do their own thing and not duplicate the others actions. You normally only need to do this when you have multiple images to build from the one base image though.

    Worth pointing out that may be related is to add this as well before the npm install steps:

    RUN npm config set maxsockets 10 
    

    On older versions of npm it is infinity, on newer ones it is 50.
    I got a huge speed boost when setting it explicitly to 10 myself (vaguely remember someone who said 25 was good for him).

    Docker will be the best open platform for developers and sysadmins to build, ship, and run distributed applications.