Node and docker - how to handle babel or typescript build?

22,133

Solution 1

One possible solution is to wrap your build procedure in a special docker image. It is often referred as Builder image. It should contain all your build dependencies: nodejs, npm, gulp, babel, tsc and etc. It encapsulates all your build process, removing the need to install these tools on the host.

First you run the builder image, mounting the source code directory as a volume. The same or a separate volume can be used as output directory. The first image takes your code and runs all build commands.

As a first step you take your built code and pack it into production docker image as you do now.

Here is an example of docker builder image for TypeScript: https://hub.docker.com/r/sandrokeil/typescript/

It is ok to have the same docker builder for several projects as it is typically designed to be general purpose wrapper around some common tools. But it is ok to build your own that describes more complicated procedure.

The good thing about builder image is that your host environment remains unpolluted and you are free to try newer versions of compiler/different tools/change order/do tasks in parallel just by modifing Dockerfile of your builder image. And at any time you can rollback your experiment with build procedure.

Solution 2

The modern recommendation for this sort of thing (as of Docker 17.05) is to use a multi-stage build. This way you can use all your dev/build dependencies in the one Dockerfile but have the end result optimised and free of unnecessary code.

I'm not so familiar with typescript, but here's an example implementation using yarn and babel. Using this Dockerfile, we can build a development image (with docker build --target development .) for running nodemon, tests etc locally; but with a straight docker build . we get a lean, optimised production image, which runs the app with pm2.

# common base image for development and production
FROM node:10.11.0-alpine AS base
WORKDIR /app


# dev image contains everything needed for testing, development and building
FROM base AS development
COPY package.json yarn.lock ./

# first set aside prod dependencies so we can copy in to the prod image
RUN yarn install --pure-lockfile --production
RUN cp -R node_modules /tmp/node_modules

# install all dependencies and add source code
RUN yarn install --pure-lockfile
COPY . .


# builder runs unit tests and linter, then builds production code 
FROM development as builder
RUN yarn lint
RUN yarn test:unit --colors
RUN yarn babel ./src --out-dir ./dist --copy-files


# release includes bare minimum required to run the app, copied from builder
FROM base AS release
COPY --from=builder /tmp/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
CMD ["yarn", "pm2-runtime", "dist/index.js"]

Solution 3

I personally prefer to just remove dev dependencies after running babel during build:

FROM node:7

# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install

# Copy app source
COPY src /usr/src/app/src

# Compile app sources
RUN npm run compile

# Remove dev dependencies
RUN npm prune --production

# Expose port and CMD
EXPOSE 8080
CMD [ "npm", "start" ]

Solution 4

Follow these steps:

Step 1: make sure you have your babel dependencies inside of dependencies not dev dependencies on package.json. Also Add a deploy script that is referencing to babel from the node_modules folder. you will be calling this script from within docker This is what my package.json file looks like

{
  "name": "tmeasy_api",
  "version": "1.0.0",
  "description": "Trade made easy Application",
  "main": "build/index.js",
  "scripts": {    
     "build": "babel -w src/ -d build/ -s inline",
    "deploy" : "node_modules/babel-cli/bin/babel.js src/ -d build/",
  },
  "devDependencies": {   
    "nodemon": "^1.9.2"
  },
  "dependencies": {    
    "babel-cli": "^6.10.1",
    "babel-polyfill": "^6.9.1",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-stage-0": "^6.5.0",
    "babel-preset-stage-3": "^6.22.0"
  }
}

build is for your development purposes on your local machine and deploy is to be called from within you dockerfile.

Step 2: since we want to do the babael transformation ourselves make sure to add .dockerignore with the build folder that you are using during development. This is what my .dockerignore file looks like.

    build
    node_modules    

Step 3. Construct your dockerfile. below is a sample of my docker file

FROM node:6

MAINTAINER stackoverflow

ENV NODE_ENV=production
ENV PORT=3000

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:

ADD package.json /tmp/package.json
RUN cd /tmp && npm install
RUN mkdir -p /var/www && cp -a /tmp/node_modules /var/www

# copy current working directory into docker; but it first checks for  
# .dockerignore so build will not be included.

COPY      . /var/www/
WORKDIR   /var/www/

# remove any previous builds and create a new build folder and then
# call our node script deploy

RUN rm -f build
RUN mkdir build
RUN chmod 777 /var/www/build
RUN npm run deploy

VOLUME    /var/www/uploads
EXPOSE $PORT


ENTRYPOINT ["node","build/index.js"]

Solution 5

I just released a great seed app for Typescript and Node.js using Docker.

You can find it on GitHub.

The project explains all of the commands that the Dockerfile uses and it combines tsc with gulp for some added benefits.

If you don't want to check out the repo, here's the details:

Dockerfile

FROM node:8

ENV USER=app

ENV SUBDIR=appDir

RUN useradd --user-group --create-home --shell /bin/false $USER &&\
    npm install --global tsc-watch npm ntypescript typescript gulp-cli

ENV HOME=/home/$USER

COPY package.json gulpfile.js $HOME/$SUBDIR/

RUN chown -R $USER:$USER $HOME/*

USER $USER

WORKDIR $HOME/$SUBDIR

RUN npm install

CMD ["node", "dist/index.js"]

docker-compose.yml

version: '3.1'

services:
  app:
    build: .
    command: npm run build
    environment:
      NODE_ENV: development
    ports:
      - '3000:3000'
    volumes:
      - .:/home/app/appDir
      - /home/app/appDir/node_modules

package.json

{
  "name": "docker-node-typescript",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "gulp copy; gulp watch & tsc-watch -p . --onSuccess \"node dist/index.js\"",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Stephen Gardner ([email protected])",
  "license": "ISC",
  "dependencies": {
    "express": "^4.10.2",
    "gulp": "^3.9.1",
    "socket.io": "^1.2.0"
  },
  "devDependencies": {
    "@types/express": "^4.11.0",
    "@types/node": "^8.5.8"
  }
}

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "declaration": false,
    "module": "commonjs",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "ES6"
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}

To get more towards the answer of your question -- the ts is being compiled from the docker-compose.yml file's calling of npm run build which then calls tsc. tsc then copies our files to the dist folder and a simple node dist/index.js command runs this file. Instead of using nodemon, we use tsc-watch and gulp.watch to watch for changes in the app and fire node dist/index.js again after every re-compilation.

Hope that helps :) If you have any questions, let me know!

Share:
22,133
Jørgen Tvedt
Author by

Jørgen Tvedt

BY DAY: Working on my Digital Value Network project, which is an organizational collaboration network. Using clever technology, help from Stack Overflow, and some fresh thinking - I hope to introduce a completely new business system approach. https://phil.network BY EVENING: Exercising, reading and obeying the commands of the family.

Updated on July 29, 2021

Comments

  • Jørgen Tvedt
    Jørgen Tvedt almost 3 years

    I have a node application that I want to host in a Docker container, which should be straight forward, as seen in this article:

    https://nodejs.org/en/docs/guides/nodejs-docker-webapp/

    In my project, however, the sources can not be run directly, they must be compiled from ES6 and/or Typescript. I use gulp to build with babel, browserify and tsify - with different setups for browser and server.

    What would be the best workflow for building and automating docker images in this case? Are there any resources on the web that describes such a workflow? Should the Dockerimage do the building after npm install or should I create a shell script to do all this and simply have the Dockerfile pack it all together?

    If the Dockerfile should do the build - the image would need to contain all the dev-dependencies, which are not ideal?

    Note: I have been able to set up a docker container, and run it - but this required all files to be installed and built beforehand.