Serverless Compose Path Resolution and Build Issues in Monorepo Setup

Environment

* **Serverless Framework Version:** v4.17.1

* **Tool:** Serverless Compose

* **Operating System:** Windows

* **Node.js Version:** v20.9.0

* **Project Structure:** Monorepo with `serverless-compose.yml` at project root

/

├── serverless-compose.yml

├── serverless_custom.yml

├── package.json

├── src/

│ ├── chat/

│ ├── user/

│ └── … (shared source code for all Lambdas)

└── services/

├── compose1/ 

│   └── serverless.yml 

├── compose2/ 

│   └── serverless.yml 

└── compose3/ 

    └── serverless.yml 

Goal

To deploy **three separate services** (`compose1`, `compose2`, `compose3`) using a **single** `serverless deploy` command, where all services share a **common `src/` directory** located at the project root.

* Each service’s `serverless.yml` uses function definitions split across multiple YAML files within the `src/` folder via `${file(../../src/…)}`

* The `src/` folder contains shared business logic and Lambda handlers.


Problem

When executing `serverless package` or `serverless deploy` from the **project root**, the process fails for all services due to **path resolution issues**. Specifically:

* Each individual service (`compose1`, `compose2`, etc.) **cannot resolve the shared root-level `src/` directory** correctly during the **build and packaging** phase.

-–

Specific Errors Encountered

Path Resolution Failure

Cannot resolve '${file(../../src/...)}' variable. No value is available for this variable... 

Compilation Failure (Most Common)

Compilation failed for function alias <function-name>.  
Please ensure you have an index file with ext .ts or .js,  
or have a path listed as main key in package.json 

serverless-esbuild Plugin Error

Invalid option in context() call: "srcDir"

Built-in esbuild Failure

ENOENT: no such file or directory, open 'F:\...\services\compose1\.serverless\build\package.json'


### **Troubleshooting Attempts**

We have explored multiple workarounds, but none fully resolved the issue:

1. Used both:

* `serverless-esbuild` plugin

* Built-in `build.esbuild` option

2. Explicitly set `srcDir` in `esbuild` config — results in plugin error.

3. Prepend the correct relative path (src/...) to all handler

4. Added dummy `package.json` files to each `services/*` directory.

5. Validated all `${file(…)}` paths relative to each `serverless.yml`.


Conclusion
It seems that Serverless Compose has difficulty handling a **shared `src/` folder** when each service’s build context is isolated. We’re looking for a **recommended, idiomatic approach** to:

* Use a **single shared codebase (`src/`)** for multiple services

* Enable **esbuild bundling** (plugin or built-in)

* Avoid duplication of code or `package.json` across services

* Maintain clean separation of service definitions in `services/`


Please advise on the **best practice** for making this **monorepo setup** work reliably with:

* **Serverless Compose**

* **Esbuild (plugin or built-in)**

Thanks for submitting the query about Serverless Compose. To better assist we would likely need to see the current configurations for serverless-compose.yml. However, if you feel like sharing that information here is too public, we can continue the conversation via support@serverless.com. Once we are able to work through your use case you would then be more than welcome to post the solution here anyone else looking (or give us permission to).

# serverless-compose.yml
services:
  compose1:
    path: services/compose1
  compose2:
    path: services/compose2
  compose3:
    path: services/compose3

services/compose1/serverless.yml

service: compose1
frameworkVersion: “4.17.1”

useDotenv: true

build:

esbuild:

bundle: true

minify: true

sourcemap:

  type: linked

  setNodeOptions: true

keepNames: true

buildConcurrency: 1

zipConcurrency: 3

watch:

  pattern: \[ "src/\*\*/\*.(js|ts)" \]

srcDir: ../../

package:

individually: true

excludeDevDependencies: true

include:

- templates/\*\*

exclude:

- .git/\*\*

- .gitignore

- README.md

- package.json

- package-lock.json

- node_modules/\*\*

- test/\*\*

- .dynamodb/\*\*

- backups/\*\*

- resources/\*\*

plugins:

  • serverless-offline

  • serverless-prune-plugin

  • serverless-step-functions

custom: ${file(../../serverless_custom.yml)}

provider:

name: aws

stages:

default:

params: ${ssm:/aws/reference/secretsmanager/appName/${sls:stage}}

functions:

  • ${file(../../src/user/setting/routes.yml)}

  • ${file(../../src/subscription/config.yml)}

  • ${file(../../src/subscription/routes.yml)}

  • ${file(../../src/shared/system/config.yml)}

  • ${file(../../src/shared/crm/config.yml)}

  • ${file(../../src/shared/content/routes.yml)}

  • ${file(../../src/shared/image/routes.yml)}

  • ${file(../../src/shared/event/routes.yml)}

  • ${file(../../src/shared/growthbook/routes.yaml)}

Hey @garethmcc , I’ve shared the serverless-compose current config. As well as one of the serverless.yml files in compose1, which is siilar to those in compose2 and compose3

Thank you for prompting about the secret key, I will make sure to share any such details privately.

As an alternative, you can avoid this issue letting npm (or similar) handle the task of sharing code among different services. Each compose has its own package.json and you can use node workspaces to share common code among them.

What was the solution for this?

I have similar issue going on.

My use case is something simple:
Setup an image optimizer lambda function url and put it behind a CloudFront.

Initially, I tried deploying everything in a single serverless.yml configuration.

However, that doesn’t work because there is no way for me to reference the Function Url associated to the Lambda to set the origin for Cloudfront.

Then, I discovered about Serverless Compose and tried to split the deployment in a 2 step process

# Serverless Compose Configuration
# This file orchestrates the deployment of multiple serverless services in the correct order

version: '1'

services:
  # First service: Deploy Lambda function with URL
  lambda-function-url:
    path: lambda-image-optimizer

  # Second service: Deploy CloudFront distribution using the Lambda function URL
  cloudfront-distribution:
    path: cloudfront-image-optimizer
    dependsOn:
      - lambda-function-url
    params:
      # Pass the Lambda function URL from the first service
      LAMBDA_FUNCTION_URL: ${lambda-function-url:ImageOptimizerLambdaFunctionUrl}


But it keeps failing on the first service lambda-function-url, which serverless.yml file looks like

# Lambda Function URL Service Configuration
# This service deploys only the Lambda function with URL, without CloudFront

plugins:
  - serverless-plugin-utils
  - serverless-esbuild

build:
  esbuild: false

custom:
  esbuild:
    external:
      - sharp
    packagerOptions:
      scripts:
        - npm install --os=linux --cpu=x64 sharp

provider:
  name: aws
  runtime: nodejs20.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  timeout: 30
  memorySize: 1536
  logRetentionInDays: ${ternary( ${opt:stage, 'dev'}, prod, 14, 1 )}
  environment:
    WASABI_ENDPOINT: ${param:WASABI_ENDPOINT}
    WASABI_REGION: ${param:WASABI_REGION}
    WASABI_BUCKET_NAME: ${param:WASABI_BUCKET_NAME}
    WASABI_ACCESS_KEY_ID: ${param:WASABI_ACCESS_KEY_ID}
    WASABI_SECRET_ACCESS_KEY: ${param:WASABI_SECRET_ACCESS_KEY}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
            - s3:DeleteObject
          Resource: 
            - "arn:aws:s3:::${param:WASABI_BUCKET_NAME}/*"

functions:
  imageOptimizer:
    handler: src/handlers/cloudfrontImageOptimizer.handler
    url: true
    cors:
      origin: '*'
      headers:
        - Content-Type
        - Accept
        - Cache-Control
        - ETag
        - Last-Modified
      allowCredentials: false

# Outputs - values that will be passed to other services
resources:
  Outputs:
    ImageOptimizerLambdaFunctionUrl:
      Description: "Lambda Function URL for CloudFront Image Optimizer"
      Value: !GetAtt ImageOptimizerLambdaFunctionUrl.FunctionUrl
      Export:
        Name: ${self:service}-${self:provider.stage}-lambda-function-url

    ImageOptimizerLambdaFunctionArn:
      Description: "Lambda Function ARN for CloudFront Image Optimizer"
      Value: !GetAtt ImageOptimizerLambdaFunction.Arn
      Export:
        Name: ${self:service}-${self:provider.stage}-lambda-function-arn

And the error I always keep seeing is

✖ lambda-image-optimizer    Build failed with 1 error:
error: Could not resolve "../src/handlers/cloudfrontImageOptimizer.ts" Results: 0 services succeeded, 1 failed, 0 skipped, 1 total    Time: 9s

However, if I deploy directly the lamba-image-optimizer, (not via serverless compose) it works.

The project structure looks like this:
serverless
→ cloudfront-image-optimizer

  • serverless.yml

→ lambda-image-optimizer

  • serverless.yml
  • src
    • handlers
      • cloudfrontImageOptimizer.ts
  • package.json
  • eslint.config.json
  • tsconfig.json

→ serverless-compose.yml

I’ve tried also things such as:

  • moving the “src” folder to the root (didn’t work)
  • Updating the settings ofr the serverless.yml of the lambda function
functions:
  imageOptimizer:
    handler: ./src/handlers/cloudfrontImageOptimizer.handler

AND

functions:
  imageOptimizer:
    handler: ./lambda-image-optimizer/src/handlers/cloudfrontImageOptimizer.handler

But with no success.

What I’m going to do temporarily is to manually deploy each service, meaning I will copy and paste the function url from the lambda deployment prior to executing the cloudfront deployment. Though that is not ideal.

Any help is appreciated!