Search code examples
node.jsdockeryarnpkg

node monorepo with yarn workspaces and docker without dockerfiles


I'm trying to find a proper setup to create docker images for individual services in a yarn workspaces monorepo such as this one :

packages/
├─ svc1/
│  ├─ package.json
├─ svc2/
│  ├─ package.json
├─ .../
package.json
yarn.lock

There is an issue when trying to use a Dockerfile inside each service package: the lockfile is located at repo root and the packages may reference some other packages in the repo using workspace: protocol. That would make the install step fail.

Therefore I wanted to create a "build" image that preserves the monorepo layout

  • 1- copy package.jsons and yarn.lock
  • 1.1- optionally mount .yarn/cache directory (hard to do at build time with docker)
  • 2- run the install step
  • 3- commit the repo container

The I would be able to fetch a given package dependencies from this build image and copy appropriate build artifacts to run my services/apps.

Dockerfile is not an option here because because of the directory layout:

  • impossible to COPY **/package.json respecting directories
  • build context is huge (I know .dockerignore could help)
  • Projects can be added/removed I don't want to edit dockerfile each time, just follow the repo layout

I investigated creating a script at repo root that would

  • create a custom docker container (e.g. from node:alpine)
  • copy every package.json respecting directory layout and the lockfile (& mount .yarn/cache?)
  • run install cmd
  • commit the container

I could not manage to create a proper script to do that, I can't docker cp files in a container I just created using docker create but my docker skills may be low.

Would you know how to create/commit this image using a shell script ?

Thank you


Solution

  • After many trial and errors I managed to do this :

    #!/usr/bin/env sh
    
    set -eo pipefail
    
    echo "pulling node:alpine..."
    # find . -name node_modules -prune -false -o -name package.json
    
    workdir="/opt/myrepo"
    
    mountpath="$(pwd)/.yarn/cache"
    container=$(docker create --tty --workdir "${workdir}" --mount type=bind,source=${mountpath},target=${workdir}/.yarn/cache,readonly node:alpine sh)
    echo "container id=\"${container}\""
    echo "docker start \"${container}\""
    docker start "${container}"
    
    find . -name node_modules -prune -false -o -name package.json | while IFS= read file; do
      path=$(dirname "${file}")
      echo "docker exec \"${container}\" mkdir -p \"${path}\""
      docker exec "${container}" mkdir -p "${path}"
      echo "docker cp \"${file}\" \"${container}:${file}\""
      docker cp "${file}" "${container}:${workdir}/${file}"
    done
    docker cp .yarnrc.yml "${container}":${workdir}/
    docker cp yarn.lock "${container}":${workdir}/
    docker cp .yarn/plugins "${container}":${workdir}/.yarn/
    docker cp .yarn/releases "${container}":${workdir}/.yarn/
    docker exec --workdir "${workdir}" "${container}" yarn workspaces focus --all --production
    docker commit "${container}" "myrepo:latest"
    docker stop "${container}"
    
    echo "done"
    

    This seem quirky but that worked. Is there a more elegant solution ?