Streamline your container workflow with Codefresh!

Create lean Node.js image with Docker multi-stage build

Docker Tutorial | April 24, 2017

TL;DR

Starting from Docker 17.05+, you can create a single Dockerfile that can build multiple helper images with compilers, tools, and tests and use files from above images to produce the final Docker image. Read this simple tutorial and create a free Codefresh account  to build, test and deploy images instantly.

The “core principle” of Dockerfile

Docker can build images by reading the instructions from a Dockerfile. A Dockerfile is a text file that contains a list of all the commands needed to build a new Docker image. The syntax of Dockerfile is pretty simple and the Docker team tries to keep it intact between Docker engine releases.

The core principle is very simple: 1 Dockerfile -> 1 Docker Image.

This principle works just fine for basic use cases, where you just need to demonstrate Docker capabilities or put some “static” content into a Docker image.

Once you advance with Docker and would like to create secure and lean Docker images, a single Dockerfile is not enough.

People who insist on following the above principle find themselves with slow Docker builds, huge Docker images (several GB size images), slow deployment time and lots of CVE violations embedded into these images.

The Docker Build Container pattern

Docker Pattern: The Build Container

The basic idea behind Build Container pattern is simple:

Create additional Docker images with required tools (compilers, linters, testing tools) and use these images to produce lean, secure and production ready Docker image.


An example of the Build Container pattern for typical Node.js application:

  1. Derive FROM a Node base image (for example node:6.10-alpine) node and npm installed (Dockerfile.build)
  2. Add package.json
  3. Install all node modules from dependency and devDependency
  4. Copy application code
  5. Run compilers, code coverage, linters, code analysis and testing tools
  6. Create the production Docker image; derive FROM same or other Node base image
  7. install node modules required for runtime (npm install --only=production)
  8. expose PORT and define a default CMD (command to run your application)
  9. Push the production image to some Docker registry

This flow assumes that you are using two or more Dockerfiles and a shell script or flow tool to orchestrate all steps above.

Example

I use a fork of Let’s Chat node.js application. Here is the link to our fork.

Builder Docker image with eslint, mocha and gulp

Production Docker image with ‘production’ node modules only

What is Docker multi-stage build?

Docker 17.05 extends Dockerfile syntax to support new multi-stage build, by extending two commands: FROM and COPY.

The multi-stage build allows using multiple FROM commands in the same Dockerfile. The last FROM command produces the final Docker image, all other images are intermediate images (no final Docker image is produced, but all layers are cached).

The FROM syntax also supports AS keyword. Use AS keyword to give the current image a logical name and reference to it later by this name.

To copy files from intermediate images use COPY --from=<image_AS_name|image_number>, where number starts from 0 (but better to use logical name through AS keyword).

Creating a multi-stage Dockerfile for Node.js application

The Dockerfile below makes the Build Container pattern obsolete, allowing to achieve the same result with the single file.

 

The above Dockerfile creates 3 intermediate Docker images and single release Docker image (the final FROM).

  1. First image FROM alpine:3.5 AS bas – is a base Node image with: node, npm, tini (init app) and package.json
  2. Second image FROM base AS dependencies – contains all node modules from dependencies and devDependencies with additional copy of dependencies required for final image only
  3. Third image FROM dependencies AS test – runs linters, setup and tests (with mocha); if this run command fail not final image is produced
  4. The final image FROM base AS release – is a base Node image with application code and all node modules from dependencies

Try Docker multi-stage build today

In order to try Docker multi-stage build, you need to get Docker 17.05, which is going to be released in May and currently available on the beta channel.

So, you have two options:

  1. Use beta channel to get Docker 17.05
  2. Run dind container (docker-in-docker)

Running Docker-in-Docker 17.05 (beta)

Running Docker 17.05 (beta) in docker container (--privileged is required):

Try mult-stage build. Add --host=:23751 to every Docker command, or set DOCKER_HOST environment variable.

Summary

With Docker multi-stage build feature, it’s possible to implement an advanced Docker image build pipeline using a single Dockerfile .

Kudos to Docker team for such a useful feature!


Hope, you find this post useful. I look forward to your comments and any questions you have.

PS Codefresh just added multi-stage build support, Please go on and create a free Codefresh account to try this out.

 

 

Alexei Ledenev

About Alexei Ledenev

Alexei is an experienced software architect and HPE distinguished technologist. He currently works at Codefresh as the Chief Researcher, focusing lately on #docker, #golang and #aws. In his spare time, Alexei maintains a couple of Docker-centric open-source projects, writes tech blog posts, and enjoys traveling and playing with his kids. https://github.com/gaia-adm/pumba

Reader Interactions

Enjoy this article? Don't forget to share.

Comments

  1. Cheers, good explanation and clean Dockerfile!

  2. Thx for the blog. However, the build process failed…

    npm ERR! Linux 3.13.0-91-generic
    npm ERR! argv “/usr/bin/node” “/usr/bin/npm” “run” “lint”
    npm ERR! node v6.9.2
    npm ERR! npm v3.10.9

    npm ERR! missing script: lint
    npm ERR!
    npm ERR! If you need help, you may report this error at:
    npm ERR!

    npm ERR! Please include the following file with any support request:
    npm ERR! /root/chat/npm-debug.log

  3. I was thinking there was a way to build only one stage. But it looks like Docker will go through all stages, only the last stage in the Dockerfile is the what will be assigned as the image?

    This doesn’t help if I want i want to end on an earlier stage. Such as if I have a dev stage, is there anyway to start the container in that stage?

    • You can always create a new Docker container from any LAYER. Just run docker history command to see all image layers. Then select some layer, for example b3616e272dc1 and run it as a container:

      $ docker run -it --rm b3616e272dc1 sh

      if you like to keep specific layer for future use, tag it:

      $ docker tag b3616e272dc1 myrepo/myimage:master

  4. Interesting article. Would I be correct in saying this mirrors a CI build pipeline?

  5. Correction: The docker version required for this is 17.05+, not 17.0.5+. That erroneous extra decimal point makes a difference

  6. What is the difference between executing npm install in an intermediate container, then copy it in the final one vs. Just executing npm install in the final container?

    • Speed.
      Why downloading npm packages twice? For a small project it makes no difference, but for real project, it can take minutes (depends on network latency).

    • The basic idea is to have 2 folders in base image: one with production dependencies and other with dev.
      Then test intermediate image could use (copy) dev dependencies from base and release image will copy only production dependencies.
      If test intermediate image fails some test or lint rule, final release image won’t be built.

  7. Cool feature!

    If the dockerfile step for say linting fails, would it stop it from progressing to the next chain?

  8. Btw:

    RUN npm install –only=production

    copy production node_modules aside

    RUN cp -R node_modules prod_node_modules

    That’s smart!

  9. Hello,

    Is it possible to launch only a specific stage in the docker-compose file ?

    I want to run unit tests separately when I want to,

    for example:

    i want to report tests in jenkinsfile in a specific file, as report.xml and I want to launch

    junit report.xml

    thx

Reply This Comment: Dan Garfield Cancel reply

Your email address will not be published. Required fields are marked *

Follow me on Twitter