What is the best way to test a Node app using a docker image across multiple jobs/stages?

Problem to solve

I have a Node app that I would like to check/test in CI. I have 3 jobs split across 2 stages:

  • One prepare_images stage that creates a Docker image that will be my CI environment to check/test my Node app
  • One tests stage that triggers 2 specific jobs: unit_tests to trigger unit testing of my Node app and integration_tests to launch integration tests with other components

The CI image created consists of a debian image with additional packages + the npm packages of my Node app.

This idea is to generate the CI image only upon specific changes (see the changes variable in gitlab-ci/yml below), so that I don’t have to re-install my dependencies if only the source code has changed for instance.

I would like to install my Node app’s dependencies globally in my CI image but I couldn’t achieve it.

So far, I’ve found a workaround which consists of making a symlink to the node_modules generated into my Node project’s folder like using ln -s /usr/src/app/node_modules node_modules.

I’m wondering if creating a CI image that will be used across multiple stages/jobs is a good practice in general ? Also, I’m wondering if there is a proper way to install npm dependencies globally in a docker image so that it can be used in further stages/jobs in the CI?

Configuration

This is the job that builds the CI image:

prepare:docker:api-node:
  extends: [.prepare-common]
  stage: prepare_images
  variables:
    IMAGE_NAME: api-node
  rules:
    - !reference [.prepare-common, rules]
    - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
      changes:
        - package-lock.json
        - api/package.json
        - api/docker/ci/*
        - api/scripts/install-deps.sh

This is the docker-compose used to build the CI image:

services:
  api-node:
    build:
      # context is at root since package-lock.json is global to multiple npm workspaces (including api)
      context: ../
      dockerfile: ./api/docker/ci/Dockerfile
      cache_from:
        - $CI_REGISTRY_IMAGE/ci-images/api-node:latest
    image: $CI_REGISTRY_IMAGE/ci-images/api-node:${BUILD_VERSION:-latest}
    container_name: api-node

This is the Dockerfile I’m using to build the CI image:

FROM library/node:20.10-bookworm-slim

# Set working directory to install node modules
WORKDIR /usr/src/app

# Update lists of packages and install required dependencies
COPY ./api/scripts/install-deps.sh ./install-deps.sh
RUN ./install-deps.sh

# Copy list of production dependencies
COPY ["api/package.json", "package-lock.json", "./"]

# Clean install dependencies (production and development)
RUN npm ci

# Clean up installation files and cache
RUN rm install-deps.sh package.json package-lock.json && npm cache clean --force

And finally, this is the unit_tests job of my Node app:

api:unit_tests:
  image: ${DOCKER_API_NODE_IMAGE}
  tags: [amd64-build]
  stage: build_and_test
  before_script:
    - ln -s /usr/src/app/node_modules node_modules
  script:
    - npm run lint
    - npm run test:unit:ci
    - npm run build
  coverage: '/Branches\s*: \d+(?:\.\d+)?/'
  artifacts:
    reports:
      junit:
        - api/coverage/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: api/coverage/cobertura-coverage.xml
    paths:
      - api/dist/*

Versions

Please select whether options apply, and add the version information.

  • Self-managed
  • GitLab.com SaaS
  • Self-hosted Runners

Versions