Prerequisites:

  • node (used here v13.9.0)
  • docker (used here v19.03.5)
  • docker-compose (used here v1.25.4)

Aproximate time to complete this setup: 30 minutes.

Install the Angular CLI globally:

npm i -g @angular/cli

Create a new angular project using the CLI and cd into the project directory:

ng new angular-docker-demo
cd angular-docker-demo

Create the Dockerfile:

# base image
FROM node:13.8.0

# install chrome for protractor tests
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
RUN apt-get update && apt-get install -yq google-chrome-stable

# set working directory
WORKDIR /usr/src/app

# install and cache app dependencies
COPY package.json ./package.json
RUN npm install

# copy the source files to the docker image
COPY . .

# make the host 0.0.0.0 so that the app can
# be accesed outside of the container
CMD npm start -- --host 0.0.0.0

Create the .dockerignore file (so that these files are not copied to the image):

node_modules
.git
.gitignore

Create the docker image (with the name angular-docker-demo:latest) and run it for testing purposes:

docker build -t angular-docker-demo .
docker run --rm -it --mount type=bind,source="${PWD}",target=/usr/src/app --mount type=volume,target=/app/node_modules -p 4201:4200 angular-docker-demo:latest
  • --rm deletes the container after we are done running it
  • -it allows us to execute shell comands inside the container (behaving like the terminal on our computer)
  • --mount type=bind,source=”\${PWD}”,target=/usr/src/app mounts the Angular app directory as a bind volume in the docker container, allowing us to change the source code in the container on the fly
  • --mount type=volume,target=/app/node_modules mounts the node_modules directory in the container to a separate anonymous volume so that it is not overwritten by the node_modules directory binded in the previous mount from our computer
  • -p 4201:4200 publish the 4200 port on which the Angular app is served and bind it to the 4201 port on the computer

The app should now be accessible on the computer on the address http://localhost:4201.

Update the karma.conf.js (unit testing config) to use Chrome Headless:

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma'),
    ],
    client: {
      clearContext: false, // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, './coverage/angular-docker-demo'),
      reports: ['html', 'lcovonly', 'text-summary'],
      fixWebpackSourcePaths: true,
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    // updated <-- HERE
    browsers: ['ChromeHeadless'],
    // new <-- HERE
    customLaunchers: {
      ChromeHeadless: {
        base: 'Chrome',
        flags: ['--no-sandbox', '--headless', '--disable-gpu', '--remote-debugging-port=9222'],
      },
    },
    singleRun: false,
    restartOnFileChange: true,
  });
};

Check that the unit testing works (the docker container started earlier must still be running — if it’s not, run the previous docker run command again):

docker exec -it CONTAINER npm run test

where CONTAINER is the container name or id which you can find by running

docker ps

Update the protractor.conf.js (e2e testing config) to use Chrome Headless:

const { SpecReporter } = require('jasmine-spec-reporter');

/**
 * @type { import("protractor").Config }
 */
exports.config = {
  allScriptsTimeout: 11000,
  specs: ['./src/**/*.e2e-spec.ts'],
  capabilities: {
    browserName: 'chrome',
    // new <-- HERE
    chromeOptions: {
      args: ['--no-sandbox', '--headless', '--window-size=1024,768'],
    },
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {},
  },
  onPrepare() {
    require('ts-node').register({
      project: require('path').join(__dirname, './tsconfig.json'),
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  },
};

Check that the e2e testing works (the docker container started earlier must still be running — if it is not, run the previous docker run command again):

docker exec -it CONTAINER npm run e2e -- --port 4202

If everything works, we can now stop the container — either by pressing Ctrl-C in the container terminal or by running the following command:

docker stop CONTAINER

Create the docker-compose.yaml file in the Angular project’s root:

version: "3.7"
services:
  web:
    container_name: project-name-web
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - type: bind
        source: .
        target: /usr/src/app
      - type: volume
        target: /usr/src/app/node_modules
    ports:
      - 4201:4200

Start docker compose:

docker-compose up

Run tests with:

docker-compose exec web npm run test
docker-compose exec web npm run e2e -- --port 4202

And that concludes the setup. I hope that by now you have a good starting point for your next project.