6

Consider following file structure of yarn workspaces:

.
├── docker-compose.yaml
├── package.json
├── packages
│   └── pkg-1
│       ├── dist
│       ├── package.json
│       ├── src
│       └── tsconfig.json
├── services
│   ├── api-1
│   │   ├── dist
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   ├── src
│   │   ├── tsconfig.json
│   │   └── yarn.lock
│   └── client-1
│       ├── package.json
│       ├── src
│       └── yarn.lock
├── tsconfig.json
└── yarn.lock

I have written Dockerfile to create image for api-1:

ARG APP_DIR=/usr/app

# Build stage
FROM node:16.2-alpine AS build

ARG APP_DIR

WORKDIR ${APP_DIR}
COPY package.json ./
COPY yarn.lock ./
COPY tsconfig.json ./

WORKDIR ${APP_DIR}/packages/pkg-1
COPY packages/pkg-1/package.json ./
RUN yarn --pure-lockfile --non-interactive
COPY packages/pkg-1/tsconfig.json ./
COPY packages/pkg-1/src/ ./src
RUN yarn build

WORKDIR ${APP_DIR}/services/api-1
COPY services/api-1/package.json ./
COPY services/api-1/yarn.lock ./
RUN yarn --pure-lockfile --non-interactive
COPY services/api-1/tsconfig.json ./
COPY services/api-1/src/ ./src
RUN yarn build

# Production stage
FROM node:16.2-alpine AS prod

ARG APP_DIR

WORKDIR ${APP_DIR}
COPY --from=build ${APP_DIR}/package.json ./
COPY --from=build ${APP_DIR}/yarn.lock ./

WORKDIR ${APP_DIR}/packages/pkg-1
COPY --from=build ${APP_DIR}/packages/pkg-1/package.json ./
RUN yarn --pure-lockfile --non-interactive --production
COPY --from=build ${APP_DIR}/packages/pkg-1/dist ./dist

WORKDIR ${APP_DIR}/services/api-1
COPY --from=build ${APP_DIR}/services/api-1/package.json ./
COPY --from=build ${APP_DIR}/services/api-1/yarn.lock ./
RUN yarn --pure-lockfile --non-interactive --production
COPY --from=build ${APP_DIR}/services/api-1/dist ./dist

CMD ["node", "dist"]

Build is running from root docker-compose.yaml to have proper context:

services:
  api-1:
    image: project/api-1
    container_name: api-1
    build:
      context: ./
      dockerfile: ./services/api-1/Dockerfile
      target: prod
    ports:
      - 3000:3000

It is working but this way there will be a lot of repetition while application grow. Problem is the way how packages are building.

Package can be for example normalized components collection used among client services or collection of normalized errors used among api services.

Whenever I will build some service I need to first build its depending packages which is unnecessarily repetitive task. Not mention that building steps of respective package are defined over and over again in Dockerfile of every single service that uses the package.

So my question is. Is there a way how to create for example image of package that will be used for building a service to avoid defining build steps of package in service Dockerfile?

MauriceNino
  • 6,214
  • 1
  • 23
  • 60

2 Answers2

1

A while ago I have posted an answer detailing how I structured a monorepo with multiple services and packages.

The "trick" is to copy all the packages that your service depends on, as well as the project root package.json. Then running yarn --pure-lockfile --non-interactive --production once will install the dependencies for the all the sub-packages since they are part of the workspace.

The example linked isn't using typescript, but I believe this could be easily achieved with a postinstall script in every package.json that would run yarn build.

Anthony Garcia-Labiad
  • 3,531
  • 1
  • 26
  • 30
  • What you describe is what I do but without typescript compilation. For you is sufficient just copy packages and install prod dependencies. I need in first stage for every package install all dependencies to get typescript I can then build package with. In second stage I can copy just build folder and install only prod dependencies. This is process I need to do for pkg-1. See my Dockerfile. I am looking for a way how to avoid to defining it over and over again in service-1, service-2, service-3... when I build docker image. – Tomáš Vavřinka Jun 08 '21 at 08:31
0

Seems like you are looking for something that gives you the option to have a "parent" package.json, so you only have to invoke "build" on one and with that build the whole dependency tree.

e.g:

- package.json // root package
  | - a
    | - package.json // module a package
  | - b
    | - package.json // module b package

You might want to look into the following:

Both support structures like the one mentioned, lerna has just a lot more features. To get a quick grasp on the differences, look here: Is Lerna needed anymore with NPM 7.0.0's workspaces?

MauriceNino
  • 6,214
  • 1
  • 23
  • 60
  • I use yarn workspaces with lerna and so I have parent package.json. I just accidentally stripped it out from my file structure graph, but you can read it in my Dockerfile. What I am looking for is something like parent build (packages build) for docker. – Tomáš Vavřinka Jun 08 '21 at 08:09
  • @TomášVavřinka Sorry seems like I missed that in your question. What do you mean by "parent build (packages build)"? Do you just want to build the relevant dependencies and not the whole graph, or what is the problem? – MauriceNino Jun 08 '21 at 09:34
  • No problem. When I build docker image for service-1 I need in Dockerfile define building steps for pkg-1. When I build docker image for service-2 I also need in Dockerfile define building steps for pkg-1 since this service also uses pkg-1. When I build docker image for service-3 I also need in Dockerfile define building steps for pkg-1 since this service uses pkg-1 too. So I want to avoid this repetitions in Dockerfile. I want one reusable docker build for pkg-1 which can be used id Dockerfile of any service. – Tomáš Vavřinka Jun 08 '21 at 10:47
  • @TomášVavřinka Sorry but I have a really hard time following you. Why do you not just build the complete tree and pick out the paths that you need in each of the Dockerfiles? – MauriceNino Jun 10 '21 at 10:24