hashgraph / guardian

The Guardian is an innovative open-source platform that streamlines the creation, management, and verification of digital environmental assets. It leverages a customizable Policy Workflow Engine and Web3 technology to ensure transparent and fraud-proof operations, making it a key tool for transforming sustainability practices and carbon markets.
Apache License 2.0
93 stars 120 forks source link

Make the build process of docker images faster #3556

Open Neurone opened 2 months ago

Neurone commented 2 months ago

Problem description

Building Guardian docker images can take time, even on a powerful machine. Most of the time is spent downloading and compiling npm packages.

Some operations are executed multiple times during the process. Theoretically, those operations could be executed once, and the following tasks can leverage their outcomes (i.e., building interface and common folders).

As a test, I was able to configure docker-compose and Docker files to compile interfaces and common source code just once, and leverage those elements in following tasks as a dependency container.

The solution works, and I post here as an example of the gain in speed you can achieve by removing duplication in the process. Still, I wouldn't say I like this approach because it can mislead developers into thinking the container used at build time is necessary also at runtime (that is not true). This configuration of the docker-compose file can also trick different client into thinking the project is not started correctly, because you have a stopped container in the list of containers.

A better approach can be to create the image as a separate step, i.e., guardian-base, and using that image as the base image for the rest of the building process (i.e., FROM:guardian-base). At the moment, I didn't find a way to do all via docker compose, unless I create a container and a dependency. So the only option I see for the moment for this is to use two commands: build the image, run docker compose.

-----------------------------------------------
# Dockerfile.common
# New file in the root folder of the project
-----------------------------------------------

# syntax=docker/dockerfile:1
# Stage 0: Use node image for base image for all stages
ARG NODE_VERSION=20.11.1-alpine
FROM node:${NODE_VERSION} as base
WORKDIR /usr/local/app
# Define an argument `YARN_CACHE_FOLDER` for the Yarn cache directory
ARG YARN_CACHE_FOLDER=/root/.yarn

# Stage 1: Build interfaces module
FROM base as interfaces
COPY --link interfaces/package.json interfaces/tsconfig*.json yarn.lock ./
COPY --link interfaces/src src/
# Here and after. Leverage a cache mount to /root/.yarn to speed up subsequent builds
RUN --mount=type=cache,target=/root/.yarn \
    yarn install --frozen-lockfile && yarn pack

# Stage 2: Build common module
FROM base as common
COPY --link --from=interfaces /usr/local/app/guardian-interfaces-*.tgz /tmp/interfaces.tgz
COPY --link common/package.json common/tsconfig*.json yarn.lock ./
COPY --link common/src src/
RUN node -e "const fs=require('fs'); const input=JSON.parse(fs.readFileSync('package.json')); input.dependencies['@guardian/interfaces']='file:/tmp/interfaces.tgz'; fs.writeFileSync('package.json', JSON.stringify(input));"
RUN --mount=type=cache,target=/root/.yarn \
    yarn install && yarn pack

# Stage 3: Installing production dependecies
FROM base as deps
COPY --link --from=interfaces /usr/local/app/guardian-interfaces-*.tgz /tmp/interfaces.tgz
COPY --link --from=common /usr/local/app/guardian-common-*.tgz /tmp/common.tgz

-----------------------------------------------
# Dockerfile in the guardian-service folder
# Leveraging the common image in the building of the guardian-service image
-----------------------------------------------

# syntax=docker/dockerfile:1
# Stage 0: Use node image for base image for all stages
ARG NODE_VERSION=20.11.1-alpine
FROM node:${NODE_VERSION} as base
WORKDIR /usr/local/app
# Define an argument `YARN_CACHE_FOLDER` for the Yarn cache directory
ARG YARN_CACHE_FOLDER=/root/.yarn

# Stage 3: Installing production dependecies
FROM guardian-commons as deps
COPY --link guardian-service/package.json guardian-service/tsconfig*.json yarn.lock ./
RUN node -e "const fs=require('fs'); const input=JSON.parse(fs.readFileSync('package.json')); input.dependencies['@guardian/interfaces']='file:/tmp/interfaces.tgz'; input.dependencies['@guardian/common']='file:/tmp/common.tgz'; fs.writeFileSync('package.json', JSON.stringify(input));"
RUN --mount=type=cache,target=/root/.yarn,sharing=private \
    yarn install --prod

# Stage 4: Build service
FROM base as build
COPY --link --from=deps /tmp/interfaces.tgz /tmp/interfaces.tgz
COPY --link --from=deps /tmp/common.tgz /tmp/common.tgz
COPY --link --from=deps /usr/local/app/package.json /usr/local/app/tsconfig*.json /usr/local/app/yarn.lock ./
COPY --link guardian-service/src src/
RUN --mount=type=cache,target=/root/.yarn \
    yarn install --frozen-lockfile && yarn run build:prod

# Stage 5: Create the final image
FROM base as image
ENV NODE_ENV production

COPY --link guardian-service/src/migrations/artifacts src/migrations/artifacts/
COPY --link guardian-service/system-schemas system-schemas/
COPY --link guardian-service/artifacts artifacts/

# Copy the production dependencies from the deps stage and the built application from the build stage into the image
COPY --link --from=deps /usr/local/app/node_modules node_modules/
COPY --link --from=deps /usr/local/app/package.json ./
COPY --link --from=build /usr/local/app/dist dist/

# Change the user to node
USER node

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

-----------------------------------------------
# docker-compose.yml
# Updated docker compose file to create the common image and create a dependency for the guardian-service
-----------------------------------------------
services:
  guardian-commons:
    env_file:
      - ./configs/.env.${GUARDIAN_ENV}.guardian.system
    build:
      context: .
      dockerfile: ./guardian-commons/Dockerfile
    image: guardian-commons
    environment:
      - GUARDIAN_ENV=${GUARDIAN_ENV}

  guardian-service:
    env_file:
      - ./configs/.env.${GUARDIAN_ENV}.guardian.system
    build:
      context: .
      dockerfile: ./guardian-service/Dockerfile
    depends_on:
      - guardian-commons
    init: true
    volumes:
      - ./guardian-service/tls:/usr/local/guardian-service/tls:ro
      - ./guardian-service/configs:/usr/local/guardian-service/configs:ro
    environment:
      - GUARDIAN_ENV=${GUARDIAN_ENV}

Requirements

Definition of done

mattsmithies commented 2 months ago

Anything around these kind of benefits/optimisations would be welcomed