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
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
Reduce the time for building all the Guardian docker images
Remove duplicated operations in the build process
Definition of done
We reduce the time of building all the docker images on the same machine
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.Requirements
Definition of done