Byron Mansfield
Byron Mansfield

Reputation: 663

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 [email protected] 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.

FROM alpine:3.4
MAINTAINER First Lastname <[email protected]>

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 [email protected] && \
    npm install -g [email protected] && \
    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 <[email protected]>

#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 [email protected] && \
    npm install -g [email protected] && \
    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 <[email protected]>

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.

Upvotes: 3

Views: 2818

Answers (2)

Magnus_P
Magnus_P

Reputation: 127

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).

Upvotes: 0

Matt
Matt

Reputation: 74899

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 <[email protected]>

# 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 [email protected] [email protected] 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.

Upvotes: 1

Related Questions