To share code between the apps of a monorepo on Light Cloud, put it in a workspace package and import it from each app. The static site keeps its own folder as Root directory and builds unchanged, because the whole repository is checked out and npm finds the workspace root above the folder. A container cannot see outside its folder, so it gets a Dockerfile at the repository root and the root as its Root directory. From then on a change to the shared package redeploys the container by itself; the static site rebuilds when you ask it to.
What you will build
The two apps from Part 1, now importing the same package:
-
packages/sharedexports the product name and a version format, -
apps/webshows them in the page, -
apps/apiuses them in its JSON answer.
Source code: github.com/light-cloud-com/tutorial-monorepo, tag part-2.
Before you start
-
Part 1 finished: the two apps deployed from
apps/webandapps/api. To start here, clone the repository at the tagpart-1and deploy both apps as Part 1 shows. - Node.js 22 or later; I used Node.js 22.22 with npm 10.9. You need it on your machine this time, to write the lock file.
- Git and a terminal. On Windows, use PowerShell 7.
Step 1: Create the shared package
npm workspaces let several packages in one repository install together and link to each other. Declare them in a package.json at the repository root:
{
"name": "tutorial-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build:web": "npm run build --workspace apps/web",
"start:api": "npm start --workspace apps/api"
}
}
Then the package itself. It is two files:
{
"name": "@tutorial/shared",
"version": "1.0.0",
"description": "Code shared by the web app and the api",
"type": "module",
"main": "index.js"
}
// Shared by apps/web and apps/api. Change it here, and both apps pick it up
// on their next build.
export const PRODUCT = "One repository, many apps";
export function formatVersion(number) {
return `v${number}.0`;
}
Step 2: Use it in both apps
Add the package to the dependencies of both apps. Its version is *: any version, which with workspaces means the folder in this repository.
"dependencies": {
"@tutorial/shared": "*",
"react": "^19.2.8",
"react-dom": "^19.2.8"
}
"dependencies": {
"@tutorial/shared": "*",
"cors": "^2.8.5",
"express": "^5.1.0"
}
Import it where each app shows its version. In the web app:
import { PRODUCT, formatVersion } from "@tutorial/shared";
<h1>{PRODUCT}</h1>
<p>This page is the web app, {formatVersion(2)}.</p>
In the API:
import { formatVersion } from "@tutorial/shared";
res.json({ service: "api", version: formatVersion(VERSION), time: new Date().toISOString() });
With workspaces there is one lock file, at the root. Delete the two the apps had and let npm write the new one:
$ rm apps/api/package-lock.json apps/web/package-lock.json
$ npm install
added 98 packages, and audited 102 packages in 712ms
found 0 vulnerabilities
PS> Remove-Item apps/api/package-lock.json, apps/web/package-lock.json
PS> npm install
added 98 packages, and audited 102 packages in 712ms
found 0 vulnerabilities
npm also prints a line about packages looking for funding; I left it out. The repository root now has node_modules with a link @tutorial/shared pointing at packages/shared, and package-lock.json covering all three packages. Try the API on your machine:
$ npm run start:api
> start:api
> npm start --workspace apps/api
> api@1.0.0 start
> node server.js
api listening on port 8080
PS> npm run start:api
> start:api
> npm start --workspace apps/api
> api@1.0.0 start
> node server.js
api listening on port 8080
In a second terminal, curl http://localhost:8080/ answers {"service":"api","version":"v2.0","time":"..."}: the version now comes through the shared package. Stop the API with Ctrl+C.
Step 3: Push, and see what builds
Commit everything and push:
$ git add -A
$ git commit -m "Shared package with npm workspaces"
$ git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
8c35173..f327c2a main -> main
PS> git add -A
PS> git commit -m "Shared package with npm workspaces"
PS> git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
8c35173..f327c2a main -> main
Both folders changed, so both apps redeploy. They end differently.
The web app builds and is live: 15 seconds in my run. Its Install command is now npm install, because apps/web has no lock file of its own, and that is fine: Light Cloud checks out the whole repository and runs the install inside apps/web, where npm notices the workspace root one level up and installs from there, shared package included. The page shows the product name and v2.0 from the package.
The API fails:
The Build output on the same page shows why. Light Cloud generated a Dockerfile for the API and built it from apps/api alone; trimmed to the lines that matter:
#10 [builder 3/5] COPY package.json ./
#11 [builder 4/5] RUN npm install
#11 4.308 npm error code E404
#11 4.309 npm error 404 Not Found - GET https://registry.npmjs.org/@tutorial%2fshared - Not found
#11 4.311 npm error 404 The requested resource '@tutorial/shared@*' could not be found or you do not have permission to access it.
#11 ERROR: process "/bin/sh -c npm install" did not complete successfully: exit code: 1
A container is built from its Root directory only: the Docker build context is apps/api, so packages/shared is not there, and npm asks the public registry for a package that exists only in your repository. The static site had the whole repository on disk; the container does not.
Step 4: Build the API from the repository root
The fix has two parts: a Dockerfile at the repository root that copies the shared package in, and the API's Root directory moved to the root so that Dockerfile is the one Light Cloud uses.
# Builds apps/api together with the shared package. The api app's Root
# directory is the repository root, so this file sees every workspace.
FROM node:22-alpine
WORKDIR /app
# The root manifest and lock file, the shared package, then the api itself.
COPY package.json package-lock.json ./
COPY packages ./packages
COPY apps/api ./apps/api
# Install only what apps/api needs; the shared package is linked in.
RUN npm ci --workspace apps/api --omit=dev
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "apps/api/server.js"]
npm ci --workspace apps/api reads the root lock file and installs the API's dependencies, with @tutorial/shared as a link to the copied folder. Commit and push the file:
$ git add Dockerfile
$ git commit -m "Dockerfile for the api, built from the repository root"
$ git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
f327c2a..717c635 main -> main
PS> git add Dockerfile
PS> git commit -m "Dockerfile for the api, built from the repository root"
PS> git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
f327c2a..717c635 main -> main
Nothing deploys on this push: the new file is outside both apps' folders. Now move the API: open the monorepo-api app, click Settings , clear the Root Directory field so that it shows / (repository root), and click Save Changes :
Then open the Production environment, its Deployments tab, and click Deploy (it reads Redeploy once an environment has deployed before). This time the build used the Dockerfile: Deployed in 59s , and the API answered {"service":"api","version":"v2.0",...} 68 seconds after the click. The web page shows both halves coming from the shared package:
A root Dockerfile always makes the root a container, which is why the web app keeps apps/web as its Root directory: a static site cannot be built from a root that has a Dockerfile.
Step 5: Change the shared package
This is where the two apps part ways. Change the version format in the shared package from .0 to .1:
return `v${number}.1`;
$ git commit -am "shared: version suffix .1"
$ git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
717c635..4a9c93a main -> main
PS> git commit -am "shared: version suffix .1"
PS> git push
To https://github.com/YOUR-USERNAME/tutorial-monorepo.git
717c635..4a9c93a main -> main
The API redeployed by itself (1 minute 5 seconds), because its Root directory is the whole repository and every push is inside it. Not the web app: packages/shared is outside apps/web, so the push did not touch its folder. Refresh the page and the two disagree:
To rebuild the web app with the new package, open its Production environment, the Deployments tab, and click Redeploy :
Mine was rebuilt in 14 seconds and served the new bundle 31 seconds after the click. Both sides now say v2.1:
Both Deployments tabs tell the story. The web app's latest deployment was started by hand; the API's by the push:
A push that changes a file inside apps/web rebuilds the web app with the current shared package too, so in day-to-day work the gap closes on its own; Redeploy is for the case where only the package changed.
pnpm, yarn and task runners
Everything above uses npm workspaces, and two facts decide what else works:
- A container built from your own
Dockerfileruns whatever the Dockerfile says, so pnpm or yarn workspaces work there the same way: copy the lock file and the packages, install, start. - Light Cloud's static site builds run npm:
npm ciwhen the folder has apackage-lock.json,npm installotherwise. A static site inside a pnpm or yarn workspace cannot rely onworkspace:links.
Task runners such as Turborepo sit on top of workspaces. Each Light Cloud app still builds from its own folder with its own Build command , so they are not needed for what this guide does.
Troubleshooting
Container build failed. Check your Dockerfile and application code for errors.
Open the deployment and read the Build output. If it ends with npm error 404 for a package that exists only in your repository, the container was built from a folder that does not contain the package. Build it from the repository root with a Dockerfile that copies the package in (Step 4).
npm error 404 Not Found - GET https://registry.npmjs.org/@tutorial%2fshared
npm did not find the workspace root, so it treated the shared package as a public one. In a container, that means the build context does not include the root package.json and packages/. On your machine, run npm install from the repository root, not from inside an app folder that has no package-lock.json.
The web app still shows the old shared code
A change in packages/shared does not redeploy an app whose Root directory is apps/web. Click Redeploy on its Production environment, or push a change inside apps/web.









Top comments (0)