Skip to content

[Turborepo] Add a with-docker-pnpm example #5462

Closed

Description

@jvzaniolo

Which project is this feature idea for?

Turborepo

Describe the feature you'd like to request

pnpm is the package manager of the other examples so it's weird that the with-docker example uses yarn.
It would be nice to have a with-docker-pnpm example too.

Describe the solution you'd like

The solution should use pnpm fetch which is the recommended way to use pnpm with docker

Describe alternatives you've considered

I was able to create a version with pnpm but it doesn't use pnpm fetch so I don't think it's the best way to use it

Activity

  1. jvzaniolo commented on Jul 5, 2023

    @jvzaniolo
    Author

    Here's my working version with pnpm. I just copied the example with yarn and replaced it with pnpm, but it doesn't use pnpm fetch

    FROM node:18-alpine AS base
    
    FROM base AS builder
    RUN apk add --no-cache libc6-compat
    RUN apk update
    # Set working directory
    WORKDIR /app
    RUN npm install -g turbo
    COPY . .
    RUN turbo prune --scope=docs --docker
    
    # Add lockfile and package.json's of isolated subworkspace
    FROM base AS installer
    RUN apk add --no-cache libc6-compat
    RUN apk update
    WORKDIR /app
    
    # First install the dependencies (as they change less often)
    COPY --from=builder /app/out/json/ .
    COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    RUN corepack enable
    RUN pnpm install --frozen-lockfile
    
    # Build the project
    COPY --from=builder /app/out/full/ .
    RUN pnpm dlx turbo run build --filter=docs
    
    FROM base AS runner
    WORKDIR /app
    
    # Don't run production as root
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    COPY --from=installer /app/apps/docs/next.config.js .
    COPY --from=installer /app/apps/docs/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=installer --chown=nextjs:nodejs /app/apps/docs/.next/standalone ./
    COPY --from=installer --chown=nextjs:nodejs /app/apps/docs/.next/static ./apps/docs/.next/static
    COPY --from=installer --chown=nextjs:nodejs /app/apps/docs/public ./apps/docs/public
    
    CMD node apps/docs/server.js
  2. edouardr commented on Jul 7, 2023

    @edouardr

    Hi, here is my version. Please let me know if there is any way to improve it.

    FROM node:18.16.0-alpine3.17 AS base
    ARG PNPM_VERSION=8.6.2
    ENV HUSKY=0
    ENV CI=true
    ENV PNPM_HOME=/usr/local/bin
    
    RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
    WORKDIR /app
    
    FROM base AS setup
    RUN pnpm add -g turbo
    COPY . .
    RUN turbo prune --scope=@acme/docs --docker
    
    FROM base AS builder
    # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
    RUN apk add --update --no-cache libc6-compat && rm -rf /var/cache/apk/*
    
    COPY .gitignore .gitignore
    COPY --from=setup /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=setup /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=setup /app/out/full/patches ./patches
    RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch
     # ↑ By caching the content-addressable store we stop downloading the same packages again and again
    
    # First install dependencies (as they change less often)
    COPY --from=setup /app/out/json/ ./
    RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
     pnpm install --filter=@acme/docs... -r --workspace-root --frozen-lockfile \
     --unsafe-perm \
     # ↑ Docker runs pnpm as root and then pnpm won't run package scripts unless we pass this arg
     | grep -v "cross-device link not permitted\|Falling back to copying packages from store"
     # ↑ Unfortunately using Docker's 'cache' mount type causes Docker to place the pnpm content-addressable store
     # on a different virtual drive, which prohibits pnpm from symlinking its content to its virtual store
     # (in node_modules/.pnpm), and that causes pnpm to fall back on copying the files.
     # And that's fine!, except pnpm emits many warnings of this, so here we filter those out.
    
    # Build the project and its dependencies
    COPY --from=setup /app/out/full/ ./
    COPY turbo.json turbo.json
    RUN pnpm run build --filter=@acme/docs...
    
    FROM base AS dev
    COPY --from=builder /app/ ./
    WORKDIR /app/apps/docs
    
    FROM builder AS pruned
    RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
     pnpm --filter=@acme/docs --prod deploy pruned --config.ignore-scripts=true
    
    FROM node:18.16.0-alpine3.17 AS runner
    WORKDIR /app
    COPY --from=pruned /app/pruned/ ./
    
    # Don't run production as root
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nodejs
    USER nodejs
    EXPOSE 8080
    
    CMD ["node", "./dist/server.js"]
  3. nicu-chiciuc commented on Jul 12, 2023

    @nicu-chiciuc

    I think this issue should be elevated, since the only reason I've chosen pnpm is that the offical docs recommend using it:
    Screenshot 2023-07-12 at 11 27 20
    So it's strange there aren't any examples.

    One issue I'm having is this:

    Using config file: /Users/nicu/.config/dive/dive.config.yaml
    Building image...
    [+] Building 3.8s (16/16) FINISHED
     => [internal] load .dockerignore                                                                                                                                                                                                                                      0.0s
     => => transferring context: 66B                                                                                                                                                                                                                                       0.0s
     => [internal] load build definition from Dockerfile                                                                                                                                                                                                                   0.0s
     => => transferring dockerfile: 1.22kB                                                                                                                                                                                                                                 0.0s
     => [internal] load metadata for docker.io/library/node:20                                                                                                                                                                                                             0.8s
     => [base 1/2] FROM docker.io/library/node:20@sha256:b3ca7d32f0c12291df6e45a914d4ee60011a3fce4a978df5e609e356a4a2cb88                                                                                                                                                  0.0s
     => [internal] load build context                                                                                                                                                                                                                                      0.6s
     => => transferring context: 1.98MB                                                                                                                                                                                                                                    0.5s
     => CACHED [base 2/2] RUN apt-get update &&         apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev                                                                                                               0.0s
     => CACHED [installer 1/7] WORKDIR /app                                                                                                                                                                                                                                0.0s
     => CACHED [builder 2/4] RUN npm install -g turbo                                                                                                                                                                                                                      0.0s
     => [builder 3/4] COPY . .                                                                                                                                                                                                                                             2.0s
     => [builder 4/4] RUN turbo prune --scope=@robo/ai_server --docker                                                                                                                                                                                                     0.4s
     => CACHED [installer 2/7] COPY .gitignore .gitignore                                                                                                                                                                                                                  0.0s
     => CACHED [installer 3/7] COPY --from=builder /app/out/json/ .                                                                                                                                                                                                        0.0s
     => CACHED [installer 4/7] COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml                                                                                                                                                                                0.0s
     => CACHED [installer 5/7] RUN npm install -g pnpm                                                                                                                                                                                                                     0.0s
     => CACHED [installer 6/7] RUN pnpm install --frozen-lockfile                                                                                                                                                                                                          0.0s
     => ERROR [installer 7/7] COPY --from=builder /app/out/full/ .                                                                                                                                                                                                         0.0s
    ------
     > [installer 7/7] COPY --from=builder /app/out/full/ .:
    ------
    Dockerfile:34
    --------------------
      32 |
      33 |         # # Build the project and its dependencies
      34 | >>>     COPY --from=builder /app/out/full/ .
      35 |         # COPY turbo.json turbo.json
      36 |
    --------------------
    ERROR: failed to solve: cannot copy to non-directory: /var/lib/docker/overlay2/qc5w7vfvoom7fgbpnz4wboevb/merged/app/apps/ai_server/node_modules/eslint-config-custom-server
    cannot build image
    exit status 1
    
    FROM node:20 as base
        # Update apt-get and install the necessary libraries
        # This is mainly so that the `canvas` package can be installed
        RUN apt-get update && \
            apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
        
        # RUN corepack enable && corepack prepare pnpm@8.6.5 --activate
    
    
    FROM base AS builder
        WORKDIR /app
    
        RUN npm install -g turbo
    
        COPY . .
    
        RUN turbo prune --scope=@robo/ai_server --docker
    
    
    FROM base as installer
        WORKDIR /app
    
        # First install dependencies (as they change less often)
        COPY .gitignore .gitignore
        COPY --from=builder /app/out/json/ .
        COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
        
        RUN npm install -g pnpm
    
    
        RUN pnpm install --frozen-lockfile
        
        # # Build the project and its dependencies
        COPY --from=builder /app/out/full/ .
        # COPY turbo.json turbo.json
        
        # # Uncomment and use build args to enable remote caching
        # # ARG TURBO_TEAM
        # # ENV TURBO_TEAM=$TURBO_TEAM
    
        # # ARG TURBO_TOKEN
        # # ENV TURBO_TOKEN=$TURBO_TOKEN
    
        # RUN pnpm dlx turbo build --filter=@robo/ai_server...
    

    I assume the problem is the way in which internal packages are symlinked, but this is just an idea.

  4. jvzaniolo commented on Jul 12, 2023

    @jvzaniolo
    Author

    @nicu-chiciuc I think this issue should be elevated, since the only reason I've chosen pnpm is that the official docs recommend using it:

    That's exactly my point here. The Docker examples were made with yarn but the rest of the docs and other examples use and recommend pnpm.

  5. nicu-chiciuc commented on Jul 12, 2023

    @nicu-chiciuc

    For posterity sake, here's how I made it work for me:

    The most important part was this comment: #1997 (comment)

    Basically I had to also add

    **/node_modules
    

    to my .dockerignore file (I already had node_modules)

    Before adding **/node_modules I had issue on the COPY --from=builder /app/out/full/ . step.

    And here's my current Dockerfile:

    FROM node:20 as base
        # Update apt-get and install the necessary libraries
        # This is mainly so that the `canvas` package can be installed
        RUN apt-get update && \
            apt-get install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
    
    
    FROM base AS builder
        WORKDIR /app
        
        ENV APP_NAME=my_secret_project
        
        # This might be necessary when switching to alpine
        # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
        # RUN apk add --no-cache libc6-compat
    
        RUN npm install -g turbo
    
        COPY . .
    
        RUN turbo prune --scope=@my_secret_org/${APP_NAME} --docker
    
    
    FROM base as installer
        WORKDIR /app
        ENV APP_NAME=my_secret_project
        
        RUN npm install -g pnpm
        RUN npm install -g turbo
    
        # First install dependencies (as they change less often)
        COPY .gitignore .gitignore
        COPY --from=builder /app/out/json/ .
        COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
        RUN pnpm install
        
        # Build the project and its dependencies
        COPY --from=builder /app/out/full/ .
        COPY turbo.json turbo.json
    
        # Uncomment and use build args to enable remote caching
        # ARG TURBO_TEAM
        # ENV TURBO_TEAM=$TURBO_TEAM
    
        # ARG TURBO_TOKEN
        # ENV TURBO_TOKEN=$TURBO_TOKEN
    
        RUN turbo run build --filter=@my_secret_org/${APP_NAME}...
        
    FROM base AS runner
        WORKDIR /app
        ENV APP_NAME=my_secret_project
    
        
        RUN npm install -g pnpm
    
        # Don't run production as root
        # RUN addgroup --system --gid 1001 expressjs
        # RUN adduser --system --uid 1001 expressjs
    
        # USER expressjs
        COPY --from=installer /app .
    
        # TODO: Maybe use the npm script?
        CMD pnpm --filter "@my_secret_org/${APP_NAME}" run start
    
    

    I know that this dockerfile is much less optimized than the versions in other comments.

    I've used node:20 instead of an alpine version since my plan is to (probably) make it work in a dev-container and I assume I'll need a full-fledged version.
    And I also wanted to make it simple/clear enough so that I can change it if necessary.

  6. nicu-chiciuc commented on Jul 15, 2023

    @nicu-chiciuc

    I created #5536, I think it should solve the issue.

  7. josefaidt commented on Jul 18, 2023

    @josefaidt
    Contributor

    I came across this issue today looking for some insights around a missing lockfile in the out/json directory where pnpm-workspace.yaml is copied.

    ➜  tree out -L 3
    out
    ├── full
    │   ├── apps
    │   │   └── frontend
    │   ├── package.json
    │   ├── packages
    │   │   ├── constants
    │   │   ├── discord
    │   │   ├── features
    │   │   ├── github
    │   │   ├── prisma-client
    │   │   ├── scripts
    │   │   ├── support
    │   │   └── type-helpers
    │   ├── pnpm-workspace.yaml
    │   └── turbo.json
    ├── json
    │   ├── apps
    │   │   └── frontend
    │   ├── package.json
    │   ├── packages
    │   │   ├── constants
    │   │   ├── discord
    │   │   ├── features
    │   │   ├── github
    │   │   ├── prisma-client
    │   │   ├── scripts
    │   │   ├── support
    │   │   └── type-helpers
    │   └── pnpm-workspace.yaml
    ├── pnpm-lock.yaml
    └── pnpm-workspace.yaml
    

    I also noticed a few comments here show patches getting captured as well. Are y'all adding that directory as a workspace?

    I've been tinkering with a pnpm+turbo+docker setup for the past few days and here's what I've noticed:

    • pnpm prune doesn't ignore lifecycle scripts. be careful how you structure your npm scripts for when you're pruning the virtual store for the final build image
    • when using pnpm fetch, I originally did not think it supported --ignore-scripts but apparently it does! I'd recommend adding that. If any of the installed dependencies have a postinstall step that calls a transient dependencies bin then it will fail without --ignore-scripts.
    • if you do not install turbo globally with npm you will either need to set up the pnpm global scope, or run via pnpm dlx which doesn't seem to cache well (10s+ on each build)
      • when running dlx pnpm will unfortunately not load turbo from the virtual store but rather the registry which is likely the cause for the delay on build

    In my experience pnpm and turbo seem to have conflicting approaches to building a docker image.
    pnpm fetch is exclusive to pnpm and fetches all dependencies in the project, loading them into the virtual store. This caches well and has a negligible impact to build time.
    turbo prune ... --docker will generate a pruned lockfile, which we can then use pnpm fetch on (but we need turbo installed first`)

    What I've landed on is to use turbo for building only with a mix of pnpm deploy (on 8.6.6):

    #syntax=docker/dockerfile:1.4
    ARG NODE_VERSION="18.15.0"
    ARG ALPINE_VERSION="3.17"
    FROM --platform=linux/amd64 node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base
    # for turbo - https://turbo.build/repo/docs/handbook/deploying-with-docker#example
    RUN apk add --no-cache libc6-compat
    RUN apk update
    WORKDIR /workspace
    # enable corepack for pnpm
    RUN corepack enable
    
    FROM base as fetcher
    # pnpm fetch only requires lockfile, but we'll need to build workspaces
    COPY pnpm*.yaml ./
    COPY patches ./patches
    # mount pnpm store as cache & fetch dependencies
    RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm-store \
      pnpm fetch --ignore-scripts
    
    FROM fetcher as builder
    # specify the app in apps/ we want to build
    ARG APP_NAME="frontend"
    ENV APP_NAME=${APP_NAME}
    WORKDIR /workspace
    COPY . .
    RUN --mount=type=secret,id=env,required=true,target=/workspace/.env \
      pnpm install --frozen-lockfile --offline --silent
    # build app
    RUN --mount=type=secret,id=env,required=true,target=/workspace/.env \
      --mount=type=cache,target=/workspace/node_modules/.cache \
      pnpm turbo run build --filter="${APP_NAME}"
    
    FROM builder as deployer
    WORKDIR /workspace
    # deploy app
    RUN pnpm --filter ${APP_NAME} deploy --prod --ignore-scripts ./out
    
    FROM base as runner
    WORKDIR /workspace
    # Don't run production as root
    RUN addgroup --system --gid 1001 mygroup
    RUN adduser --system --uid 1001 myuser
    USER myuser
    # copy files needed to run the app
    COPY --chown=myuser:mygroup --from=deployer /workspace/out/package.json .
    COPY --chown=myuser:mygroup --from=deployer /workspace/out/node_modules/ ./node_modules
    COPY --chown=myuser:mygroup --from=deployer /workspace/out/build/ ./build
    # start the app
    CMD pnpm run start

    This results in a ~650MB build image in a relatively small monorepo and ~300s builds with no cache. Half of that docker build time is actually from turbo build. Have y'all figured out how to use the .turbo cache in the build effectively (with buildkit cache or otherwise -- afaict I'm not ignoring .turbo dirs)?

    pnpm deploy is particularly helpful in a monorepo where you have small, private packages as pnpm will load them into the virtual store and you only need to copy the node_modules dir from the deploy output rather than also copying the symlinked packages/**

  8. leetmachine commented on Sep 8, 2023

    @leetmachine

    Had same problem on COPY --from=builder /app/out/full/ . line as everyone else using pnpm.

    Added **/node_modules to root .dockerignore to resolve.

    Then ran into issues with the last 3 COPY lines. Needed to also copy the next.config.js from the with-docker example. tldr- I was missing output: standalone

    Ended up with this. I like it because pnpm and turbo are pulled to "base", allowing me to just use bare alpine in the end result.
    copied from the with-docker example and updated to use pnpm.

    # src Dockerfile: https://github.com/vercel/turbo/blob/main/examples/with-docker/apps/web/Dockerfile
    FROM node:18-alpine AS alpine
    
    # setup pnpm on the alpine base
    FROM alpine as base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    RUN corepack enable
    RUN pnpm install turbo --global
    
    FROM base AS builder
    # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
    RUN apk add --no-cache libc6-compat
    RUN apk update
    # Set working directory
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=web --docker
    
    # Add lockfile and package.json's of isolated subworkspace
    FROM base AS installer
    RUN apk add --no-cache libc6-compat
    RUN apk update
    WORKDIR /app
    
    # First install the dependencies (as they change less often)
    COPY .gitignore .gitignore
    COPY --from=builder /app/out/json/ .
    COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    RUN pnpm install
    
    # Build the project
    COPY --from=builder /app/out/full/ .
    COPY turbo.json turbo.json
    
    # Uncomment and use build args to enable remote caching
    # ARG TURBO_TEAM
    # ENV TURBO_TEAM=$TURBO_TEAM
    
    # ARG TURBO_TOKEN
    # ENV TURBO_TOKEN=$TURBO_TOKEN
    
    RUN turbo run build --filter=web
    
    # use alpine as the thinest image
    FROM alpine AS runner
    WORKDIR /app
    
    # Don't run production as root
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    COPY --from=installer /app/apps/web/next.config.js .
    COPY --from=installer /app/apps/web/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
    
    CMD node apps/web/server.js
    
  9. 25 remaining items

  10. EddyVinck commented on Apr 11, 2024

    @EddyVinck

    Had same problem on COPY --from=builder /app/out/full/ . line as everyone else using pnpm.

    Added **/node_modules to root .dockerignore to resolve.

    Then ran into issues with the last 3 COPY lines. Needed to also copy the next.config.js from the with-docker example. tldr- I was missing output: standalone

    Ended up with this. I like it because pnpm and turbo are pulled to "base", allowing me to just use bare alpine in the end result. copied from the with-docker example and updated to use pnpm.

    # src Dockerfile: https://github.com/vercel/turbo/blob/main/examples/with-docker/apps/web/Dockerfile
    FROM node:18-alpine AS alpine
    
    # setup pnpm on the alpine base
    FROM alpine as base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    RUN corepack enable
    RUN pnpm install turbo --global
    
    FROM base AS builder
    # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
    RUN apk add --no-cache libc6-compat
    RUN apk update
    # Set working directory
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=web --docker
    
    # Add lockfile and package.json's of isolated subworkspace
    FROM base AS installer
    RUN apk add --no-cache libc6-compat
    RUN apk update
    WORKDIR /app
    
    # First install the dependencies (as they change less often)
    COPY .gitignore .gitignore
    COPY --from=builder /app/out/json/ .
    COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    RUN pnpm install
    
    # Build the project
    COPY --from=builder /app/out/full/ .
    COPY turbo.json turbo.json
    
    # Uncomment and use build args to enable remote caching
    # ARG TURBO_TEAM
    # ENV TURBO_TEAM=$TURBO_TEAM
    
    # ARG TURBO_TOKEN
    # ENV TURBO_TOKEN=$TURBO_TOKEN
    
    RUN turbo run build --filter=web
    
    # use alpine as the thinest image
    FROM alpine AS runner
    WORKDIR /app
    
    # Don't run production as root
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    COPY --from=installer /app/apps/web/next.config.js .
    COPY --from=installer /app/apps/web/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
    
    CMD node apps/web/server.js
    

    This setup was much better than what I had before, thanks a lot! I went from 1.7GB to 185MB with this Dockerfile after making some small adjustments to make it fit my project! 🥳

    • output: "standalone" in next.config.js
    • **/node_modules in .dockerignore

    My Dockerfile skills aren't great so this helped a lot! Thank you!

  11. joakim-roos commented on May 27, 2024

    @joakim-roos

    I've been trying to get it to work for the last few days with pnpm and standalone output in next.js.

    The file-output with dive looks like this:

    Screenshot 2024-05-27 at 15 39 24

    This is my Dockerfile:

    # We run this file from the root directory (see docker:build:next command in package.json)
    
    ARG APP_DIRNAME=next
    ARG PROJECT=@foundation/next
    ARG NODE_VERSION=20.11
    
    # 1. Alpine image
    FROM node:${NODE_VERSION}-alpine AS alpine
    RUN apk update
    RUN apk add --no-cache libc6-compat
    
    # Setup pnpm and turbo on the alpine base
    FROM alpine as base
    RUN corepack enable
    RUN npm install turbo --global
    RUN pnpm config set store-dir ~/.pnpm-store
    
    # 2. Prune projects
    FROM base AS pruner
    ARG PROJECT
    
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=${PROJECT} --docker
    
    # 3. Build the project
    FROM base AS builder
    ARG PROJECT
    
    WORKDIR /app
    
    # Copy lockfile and package.json's of isolated subworkspace
    COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /app/out/json/ .
    
    # First install the dependencies (as they change less often)
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
    
    # Copy source code of isolated subworkspace
    COPY --from=pruner /app/out/full/ .
    
    RUN turbo build --filter=${PROJECT}
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm prune --prod --no-optional
    RUN rm -rf ./**/*/src
    
    # 4. Final image - runner stage to run the application
    FROM alpine AS runner
    ARG APP_DIRNAME
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 next
    USER next
    
    WORKDIR /app
    
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/next.config.mjs .
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/standalone ./
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/static ./.next/static
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/public ./public
    
    CMD ["node", "server.js"]
    

    When trying to run the container i'm getting:

    2024-05-27 15:37:27 node:internal/modules/cjs/loader:1145
    2024-05-27 15:37:27   const err = new Error(message);
    2024-05-27 15:37:27               ^
    2024-05-27 15:37:27 
    2024-05-27 15:37:27 Error: Cannot find module 'next'
    2024-05-27 15:37:27 Require stack:
    2024-05-27 15:37:27 - /app/server.js
    2024-05-27 15:37:27     at Module._resolveFilename (node:internal/modules/cjs/loader:1145:15)
    2024-05-27 15:37:27     at Module._load (node:internal/modules/cjs/loader:986:27)
    2024-05-27 15:37:27     at Module.require (node:internal/modules/cjs/loader:1233:19)
    2024-05-27 15:37:27     at require (node:internal/modules/helpers:179:18)
    2024-05-27 15:37:27     at file:///app/server.js:22:1
    2024-05-27 15:37:27     at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
    2024-05-27 15:37:27     at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    2024-05-27 15:37:27     at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5) {
    2024-05-27 15:37:27   code: 'MODULE_NOT_FOUND',
    2024-05-27 15:37:27   requireStack: [ '/app/server.js' ]
    2024-05-27 15:37:27 }
    2024-05-27 15:37:27 
    2024-05-27 15:37:27 Node.js v20.13.1
    

    I run the build with turbo from root like so:
    (I have a remix application in this monorepo that has a very similar structure and runs perfectly like this from root)

    // from root package.json
    "docker:build:next": "turbo docker:build --filter=@foundation/next"
    
    // in next-app package.json
    "docker:build": "cd ../.. && docker build -t foundation/next -f apps/next/Dockerfile ."
    

    If anyone has any idea to share how to solve this it's greatly appreciated! <3

  12. edouardr commented on Jun 3, 2024

    @edouardr

    @joakim-roos it looks like you are missing some dependencies and, therefore, I guess you would have some issues when running pnpm install.

    The way I debug this is to add --no-cache and --progress=plain to your docker build command. This will log every output of your multi staged build. From there you should see if pnpm install runs as expected.

  13. joakim-roos commented on Jun 4, 2024

    @joakim-roos

    Thanks @edouardr!

    I did find the issue. The problem was that I totally forgot to copy over the node_modules dependencies so I added an extra layer which installs these and then copy over them to the final build. Here's a working version:

    # We run this file from the root directory (see docker:build:next command in package.json)
    
    ARG APP_DIRNAME=next
    ARG PROJECT=@foundation/next
    ARG NODE_VERSION=20.11
    
    # 1. Alpine image
    FROM node:${NODE_VERSION}-alpine AS alpine
    RUN apk update
    RUN apk add --no-cache libc6-compat
    
    # Setup pnpm and turbo on the alpine base
    FROM alpine as base
    RUN corepack enable
    RUN npm install turbo --global
    RUN pnpm config set store-dir ~/.pnpm-store
    
    # 2. Prune projects
    FROM base AS pruner
    ARG PROJECT
    
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=${PROJECT} --docker
    
    # 3. Build the project
    FROM base AS builder
    ARG PROJECT
    
    WORKDIR /app
    
    # Copy lockfile and package.json's of isolated subworkspace
    COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /app/out/json/ .
    
    # First install the dependencies (as they change less often)
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
    
    # Copy source code of isolated subworkspace
    COPY --from=pruner /app/out/full/ .
    
    RUN turbo build --filter=${PROJECT}
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm prune --prod --no-optional
    RUN rm -rf ./**/*/src
    
    
    # 4. Production dependencies
    FROM builder AS dependencies
    WORKDIR /app
    RUN pnpm --filter=$PROJECT deploy --prod --ignore-scripts --no-optional /dependencies
    
    # 5. Final image - runner stage to run the application
    FROM alpine AS runner
    ARG APP_DIRNAME
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 next
    USER next
    
    WORKDIR /app
    
    ENV NODE_ENV production
    
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/next.config.mjs .
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/standalone ./
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/static ./.next/static
    COPY --from=dependencies --chown=remix:nodejs /dependencies/node_modules ./node_modules
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/public ./public
    
    CMD ["node", "/app/server.js"]
    
  14. Pedro-phd commented on Jul 1, 2024

    @Pedro-phd

    This setup was much better than what I had before, thanks a lot! I went from 1.7GB to 185MB with this Dockerfile after making some small adjustments to make it fit my project! 🥳

    Hello, could anyone help me? I am having this problem in the running RUN turbo run build --filter=web

    my dockerfile:

    # We run this file from the root directory (see docker:build:next command in package.json)
    
    ARG APP_DIRNAME=web
    ARG PROJECT=web
    ARG NODE_VERSION=20.11
    
    # 1. Alpine image
    FROM node:${NODE_VERSION}-alpine AS alpine
    RUN apk update
    RUN apk add --no-cache libc6-compat
    
    # Setup pnpm and turbo on the alpine base
    FROM alpine as base
    RUN corepack enable
    RUN npm install turbo --global
    RUN pnpm config set store-dir ~/.pnpm-store
    
    # 2. Prune projects
    FROM base AS pruner
    ARG PROJECT
    
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=${PROJECT} --docker
    
    # 3. Build the project
    FROM base AS builder
    ARG PROJECT
    
    WORKDIR /app
    
    # Copy lockfile and package.json's of isolated subworkspace
    COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /app/out/json/ .
    
    # First install the dependencies (as they change less often)
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install
    
    # Copy source code of isolated subworkspace
    COPY --from=pruner /app/out/full/ .
    
    RUN turbo build --filter=${PROJECT}
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm prune --prod --no-optional
    RUN rm -rf ./**/*/src
    
    
    # 4. Production dependencies
    FROM builder AS dependencies
    WORKDIR /app
    RUN pnpm --filter=$PROJECT deploy --prod --ignore-scripts --no-optional /dependencies
    
    # 5. Final image - runner stage to run the application
    FROM alpine AS runner
    ARG APP_DIRNAME
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 next
    USER next
    
    WORKDIR /app
    
    ENV NODE_ENV production
    
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/next.config.mjs .
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/package.json .
    
    # Automatically leverage output traces to reduce image size
    # https://nextjs.org/docs/advanced-features/output-file-tracing
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/standalone ./
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/.next/static ./.next/static
    COPY --from=dependencies --chown=remix:nodejs /dependencies/node_modules ./node_modules
    COPY --from=builder --chown=next:nodejs /app/apps/${APP_DIRNAME}/public ./public
    
    CMD ["node", "/app/server.js"]
    

    image

  15. Security2431 commented on Oct 26, 2024

    @Security2431

    For those who need it, I've created a workaround with nextjs and create-t3-turbo.

    You can find it on GitHub:
    https://github.com/Security2431/create-t3-turbo-with-prisma/blob/main/apps/nextjs/Dockerfile

    Feel free to modify it as needed to suit your own use case:

    ARG NODE_VERSION=20
    ARG APP_DIRNAME=nextjs
    ARG PROJECT=@acme/nextjs
    
    # 1. Alpine image
    FROM node:${NODE_VERSION}-alpine AS alpine
    RUN apk update
    RUN apk add --no-cache libc6-compat
    
    # Setup pnpm and turbo on the alpine base
    FROM alpine AS base
    RUN corepack enable
    # Replace <your-major-version> with the major version installed in your repository. For example:
    # RUN npm install turbo@2.1.3 --global
    RUN npm install turbo --global
    
    RUN pnpm config set store-dir ~/.pnpm-store
    
    # 2. Prune projects
    FROM base AS pruner
    # https://stackoverflow.com/questions/49681984/how-to-get-version-value-of-package-json-inside-of-dockerfile
    # RUN export VERSION=$(npm run version)
    
    ARG PROJECT
    
    # Set working directory
    WORKDIR /app
    
    # It might be the path to <ROOT> turborepo
    COPY . .
     
    # Generate a partial monorepo with a pruned lockfile for a target workspace.
    # Assuming "@acme/nextjs" is the name entered in the project's package.json: { name: "@acme/nextjs" }
    RUN turbo prune --scope=${PROJECT} --docker
     
    # 3. Build the project
    FROM base AS builder
    ARG PROJECT
    
    # Environment to skip .env validation on build
    ENV CI=true
    
    WORKDIR /app
    
    # Copy lockfile and package.json's of isolated subworkspace
    COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /app/out/json/ .
    
    # First install the dependencies (as they change less often)
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
     
    # Copy source code of isolated subworkspace
    COPY --from=pruner /app/out/full/ .
    
    RUN pnpm build --filter=${PROJECT}
    
    # 4. Final image - runner stage to run the application
    FROM base AS runner
    ARG APP_DIRNAME
     
    # Don't run production as root
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    WORKDIR /app
    
    ENV NODE_ENV=production
    
    COPY --from=builder --chown=nextjs:nodejs /app .
    WORKDIR /app/apps/${APP_DIRNAME}
    
    ARG PORT=3000
    ENV PORT=${PORT}
    EXPOSE ${PORT}
    
    CMD ["pnpm", "start"]
    
    
  16. tadeumaia commented on Dec 1, 2024

    @tadeumaia

    For anyone using the example above, the COPY command is incorrect. It should be:

    COPY --from=builder --chown=nextjs:nodejs /app .  
    

    The correct format is always --chown=user:group.

  17. Security2431 commented on Dec 2, 2024

    @Security2431

    @tadeumaia Thank you for pointing this out. I've fixed the previous example

  18. blitzzstorm commented on Jan 5, 2025

    @blitzzstorm

    If Docker container is referenced using its internal hostname, such as:

    Attaching to web
    web  |    ▲ Next.js 15.1.0
    web  |    - Local:        http://3c289193736b:3000
    web  |    - Network:      http://3c289193736b:3000
    web  |
    web  |  ✓ Starting...
    web  |  ✓ Ready in 571ms
    web  | redirecting to dashboard
    

    Include these in the runner step.

    EXPOSE 3000
    
    ENV HOSTNAME "0.0.0.0"
    ENV PORT 3000
    
  19. amosbastian commented on Jan 27, 2025

    @amosbastian

    I came across this issue today looking for some insights around a missing lockfile in the out/json directory where pnpm-workspace.yaml is copied.

    ➜  tree out -L 3
    out
    ├── full
    │   ├── apps
    │   │   └── frontend
    │   ├── package.json
    │   ├── packages
    │   │   ├── constants
    │   │   ├── discord
    │   │   ├── features
    │   │   ├── github
    │   │   ├── prisma-client
    │   │   ├── scripts
    │   │   ├── support
    │   │   └── type-helpers
    │   ├── pnpm-workspace.yaml
    │   └── turbo.json
    ├── json
    │   ├── apps
    │   │   └── frontend
    │   ├── package.json
    │   ├── packages
    │   │   ├── constants
    │   │   ├── discord
    │   │   ├── features
    │   │   ├── github
    │   │   ├── prisma-client
    │   │   ├── scripts
    │   │   ├── support
    │   │   └── type-helpers
    │   └── pnpm-workspace.yaml
    ├── pnpm-lock.yaml
    └── pnpm-workspace.yaml
    

    I also noticed a few comments here show patches getting captured as well. Are y'all adding that directory as a workspace?

    I've been tinkering with a pnpm+turbo+docker setup for the past few days and here's what I've noticed:

    • pnpm prune doesn't ignore lifecycle scripts. be careful how you structure your npm scripts for when you're pruning the virtual store for the final build image

    • when using pnpm fetch, I originally did not think it supported --ignore-scripts but apparently it does! I'd recommend adding that. If any of the installed dependencies have a postinstall step that calls a transient dependencies bin then it will fail without --ignore-scripts.

    • if you do not install turbo globally with npm you will either need to set up the pnpm global scope, or run via pnpm dlx which doesn't seem to cache well (10s+ on each build)

      • when running dlx pnpm will unfortunately not load turbo from the virtual store but rather the registry which is likely the cause for the delay on build

    In my experience pnpm and turbo seem to have conflicting approaches to building a docker image. pnpm fetch is exclusive to pnpm and fetches all dependencies in the project, loading them into the virtual store. This caches well and has a negligible impact to build time. turbo prune ... --docker will generate a pruned lockfile, which we can then use pnpm fetch on (but we need turbo installed first`)

    What I've landed on is to use turbo for building only with a mix of pnpm deploy (on 8.6.6):

    #syntax=docker/dockerfile:1.4
    ARG NODE_VERSION="18.15.0"
    ARG ALPINE_VERSION="3.17"
    FROM --platform=linux/amd64 node:${NODE_VERSION}-alpine${ALPINE_VERSION} as base

    for turbo - https://turbo.build/repo/docs/handbook/deploying-with-docker#example

    RUN apk add --no-cache libc6-compat
    RUN apk update
    WORKDIR /workspace

    enable corepack for pnpm

    RUN corepack enable

    FROM base as fetcher

    pnpm fetch only requires lockfile, but we'll need to build workspaces

    COPY pnpm*.yaml ./
    COPY patches ./patches

    mount pnpm store as cache & fetch dependencies

    RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm-store
    pnpm fetch --ignore-scripts

    FROM fetcher as builder

    specify the app in apps/ we want to build

    ARG APP_NAME="frontend"
    ENV APP_NAME=${APP_NAME}
    WORKDIR /workspace
    COPY . .
    RUN --mount=type=secret,id=env,required=true,target=/workspace/.env
    pnpm install --frozen-lockfile --offline --silent

    build app

    RUN --mount=type=secret,id=env,required=true,target=/workspace/.env
    --mount=type=cache,target=/workspace/node_modules/.cache
    pnpm turbo run build --filter="${APP_NAME}"

    FROM builder as deployer
    WORKDIR /workspace

    deploy app

    RUN pnpm --filter ${APP_NAME} deploy --prod --ignore-scripts ./out

    FROM base as runner
    WORKDIR /workspace

    Don't run production as root

    RUN addgroup --system --gid 1001 mygroup
    RUN adduser --system --uid 1001 myuser
    USER myuser

    copy files needed to run the app

    COPY --chown=myuser:mygroup --from=deployer /workspace/out/package.json .
    COPY --chown=myuser:mygroup --from=deployer /workspace/out/node_modules/ ./node_modules
    COPY --chown=myuser:mygroup --from=deployer /workspace/out/build/ ./build

    start the app

    CMD pnpm run start
    This results in a ~650MB build image in a relatively small monorepo and ~300s builds with no cache. Half of that docker build time is actually from turbo build. Have y'all figured out how to use the .turbo cache in the build effectively (with buildkit cache or otherwise -- afaict I'm not ignoring .turbo dirs)?

    pnpm deploy is particularly helpful in a monorepo where you have small, private packages as pnpm will load them into the virtual store and you only need to copy the node_modules dir from the deploy output rather than also copying the symlinked packages/**

    Is a ~650MB build image something we just have to live with or is this a result of using pnpm and turborepo together? To me it seems way too large...

  20. nktnet1 commented on Feb 22, 2025

    @nktnet1

    I've made a turborepo template with pnpm and docker (and docker-compose), in case anyone is looking for a concrete examples apart from NextJS:

    The containerised applications include:

    • a static web app (Vanilla React, Vite)
    • a NodeJS server (Hono)

    Currently not using pnpm fetch, but caching/performance doesn't appear to be an issue for the template which follows Turborepo's docker docs (after converting yarn -> pnpm).

  21. robinvriens commented on Mar 23, 2025

    @robinvriens
    FROM node:23-alpine AS base
    
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    
    RUN apk update
    RUN apk add --no-cache libc6-compat
    RUN corepack enable && corepack prepare pnpm
    
    FROM base AS builder
    WORKDIR /app
    
    RUN pnpm install --global turbo
    COPY . .
    RUN turbo prune --scope=web --docker
    
    FROM base AS installer
    WORKDIR /app
    
    COPY --from=builder /app/out/json/ .
    RUN pnpm install --frozen-lockfile
    COPY --from=builder /app/out/full/ .
    RUN pnpm turbo build
    
    FROM base AS runner
    WORKDIR /app
    
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
    COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
    
    CMD ["node", "apps/web/server.js"]
    
  22. rinshadibnabu commented on May 24, 2025

    @rinshadibnabu

    I spent hours debugging this exact issue and finally found a complete solution for me . The problem isn't just the binaryTargets in schema.prisma - it's a combination of build order, path resolution.

    #Next.js standalone builds look for Prisma client in these locations

    /app/next/standalone/apps/user-app/generated/prisma
    /app/next/standalone/apps/user-app/.next/server
    /home/rinshad/projects/repos/paisakaro/packages/db/generated/prisma  ← This path doesn't exist in container
    /app/next/standalone/apps/user-app/.prisma/client
    /tmp/prisma-engines
    

    #prisma was creating in the these file
    was packages/db/generated

    1. Correct schema.prisma:

    generator client {
      provider      = "prisma-client-js"
      output        = "../generated/prisma"
      binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
    }

    **2. Fixed Dockerfile **

    ARG NODE_VERSION=18.18.0
    ARG PROJECT=user-app
    # Alpine base with necessary dependencies
    FROM node:${NODE_VERSION}-alpine AS alpine
    RUN apk update
    RUN apk add --no-cache libc6-compat 
    
    # Setup pnpm and turbo on the alpine base
    FROM alpine as base
    RUN npm install pnpm@10.11.0 turbo@2.5.3 --global
    RUN pnpm config set store-dir ~/.pnpm-store
    
    # Prune projects
    FROM base AS pruner
    ARG PROJECT
    
    WORKDIR /app
    COPY . .
    RUN turbo prune --scope=${PROJECT} --docker
    
    # Build the project
    FROM base AS builder
    ARG PROJECT
    
    WORKDIR /app
    
    # Copy lockfile and package.json's of isolated subworkspace
    COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /app/out/json/ .
    
    # First install the dependencies (as they change less often)
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
    
    # Copy source code of isolated subworkspace
    COPY --from=pruner /app/out/full/ .
    #this is neccarry 
    RUN cd packages/db && \
      pnpm prisma generate --schema=./prisma/schema.prisma
    RUN turbo build --filter=${PROJECT}
    # Clean and regenerate with explicit binary target
    
    RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm prune --prod --no-optional
    
    FROM alpine AS runner
    ARG PROJECT
    
    WORKDIR /app
    # Create non-root user
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    
    COPY --from=builder --chown=nextjs:nodejs /app/apps/${PROJECT}/.next/standalone ./next/standalone
    COPY --from=builder --chown=nextjs:nodejs /app/apps/${PROJECT}/.next/static ./next/standalone/apps/app/.next/static
    COPY --from=builder --chown=nextjs:nodejs /app/apps/${PROJECT}/public ./next/standalone/apps/app/public
    ##prisma was looking for this and couldn't find and claude.ai helped me to find it 
    COPY --from=builder --chown=nextjs:nodejs /app/packages/db/generated ./next/standalone/apps/${PROJECT}/generated
    USER nextjs
    
    EXPOSE 3000
    ENV PORT=3000
    
    ENV HOSTNAME="0.0.0.0"
    CMD ["node", "next/standalone/apps/user-app/server.js"]

    Verify Success

    docker run --rm -it --entrypoint sh your-app:latest
    ls -la /app/next/standalone/packages/db/generated/prisma/

    Project structure this works with:

    ├── apps/user-app/          # Next.js app
    ├── packages/db/prisma/     # Prisma schema
    ├── docker/Dockerfile.user-app
    

    may be my problem was solved with this what about yours , may i misunderstood the problem , is there a better way to create prisma client is this optimal

  23. YKDZ commented on Jun 3, 2025

    @YKDZ

    My personal solution with prisma, vike, turbo prune, pnpm prune and pnpm deploy:

    # --- 1. Base
    FROM node:22-alpine AS base
    ENV PNPM_HOME="/pnpm" \
        PATH="/pnpm:$PATH"
    
    RUN corepack enable \
        && pnpm add -g turbo@latest
    
    # --- 2. Installer
    FROM base AS pruner
    WORKDIR /repo
    
    COPY . .
    
    RUN turbo prune @cat/app --docker --use-gitignore=false \
        # turbo prune remove injectWorkspacePackages from pnpm-lock.yaml
        # https://github.com/vercel/turborepo/discussions/10541
        && sed -i '/^settings:/a \  injectWorkspacePackages: true' ./out/pnpm-lock.yaml \
        && sed -i '/^settings:/a \  injectWorkspacePackages: true' ./out/json/pnpm-lock.yaml
    
    # --- 3. Builder
    FROM base AS builder
    WORKDIR /builder
    
    COPY --from=pruner /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml
    COPY --from=pruner /repo/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
    COPY --from=pruner /repo/out/json/ ./
    
    RUN pnpm install -r --frozen-lockfile
    
    COPY --from=pruner /repo/out/full/ ./
    
    RUN pnpm --filter=@cat/db db:generate \
        && turbo build --filter=@cat/app
    
    # --- 4. Deployer
    FROM builder AS deployer
    WORKDIR /builder
    
    RUN pnpm --filter=@cat/app deploy --prod --no-optional /deployer \
        && pnpm prune --prod --no-optional
    
    # --- 5. Runner
    FROM node:22-alpine AS runner
    ENV PNPM_HOME="/pnpm" \
        PATH="/pnpm:$PATH" \
        NODE_ENV=production
    
    WORKDIR /app
    
    RUN corepack enable
    
    # Prisma
    COPY --from=builder /builder/packages/db/prisma ./prisma
    COPY --from=builder /builder/packages/db/src/generated/prisma/libquery_engine* /tmp/prisma-engines/
    
    COPY --from=deployer /deployer/plugins ./plugins/
    COPY --from=deployer /deployer/package.json ./
    COPY --from=deployer /deployer/node_modules ./node_modules/
    COPY --from=deployer /deployer/dist ./dist/
    
    CMD ["pnpm", "run", "docker-entrypoint"]
  24. Number16BusShelter commented on Jun 25, 2025

    @Number16BusShelter

    It took me forever to make it work, and this discussion was the most helpful—so I want to share my Docker build for Nextjs with Turborepo + PNPM workspace (with workspace deps) using a multi-stage builder.

    Some hard-won lessons:

    • Custom image loader only works with unoptimized: false in next.config.js. Otherwise, even with a loader defined, image loader is ignored .

    • Env vars during build behave weirdly: ENV alone won’t be picked up by next build. You must create a .env.production file with your build env vars during the build stage—otherwise they'll silently not load at runtime.

    # -------------------------------------------------------- #
    # 0. Build Arguments
    # -------------------------------------------------------- #
    
    ARG NODE_VERSION=20
    ARG PNPM_VERSION=10
    ARG SERVICE=example-service
    ARG PNPM_HOME="/pnpm"
    ARG PATH="${PNPM_HOME}:${PATH}"
    ARG PNPM_STORE="/pnpm/store"
    ARG APP_PORT=3000
    
    ARG NODE_ENV
    ARG USE_CDN
    ARG UPLOAD_ENV
    
    # -------------------------------------------------------- #
    # 1. Base system environment
    # -------------------------------------------------------- #
    FROM node:${NODE_VERSION}-alpine AS core-os
    LABEL stage=core
    RUN apk update
    RUN apk add --no-cache dumb-init libc6-compat curl
    
    # -------------------------------------------------------- #
    # 2. Core tooling setup (Turbo, PNPM, etc.)
    # -------------------------------------------------------- #
    FROM core-os as base
    ARG PNPM_STORE
    ARG SERVICE
    LABEL builder=true
    
    RUN npm install -g corepack@latest
    RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
    RUN npm install -g turbo cross-env
    
    RUN --mount=type=cache,id=pnpm,target=${PNPM_STORE} pnpm config set store-dir ${PNPM_STORE}
    RUN turbo telemetry disable
    
    # -------------------------------------------------------- #
    # 3. Prune unused packages
    # -------------------------------------------------------- #
    FROM base AS pruner
    ARG SERVICE
    LABEL stage=builder
    
    WORKDIR /app
    COPY . .
    
    RUN turbo prune --scope=@your-org/${SERVICE} --docker
    
    # -------------------------------------------------------- #
    # 4. Build application
    # -------------------------------------------------------- #
    FROM base AS build
    ARG SERVICE
    LABEL stage=builder
    
    ARG NODE_ENV
    ARG USE_CDN
    ARG UPLOAD_ENV
    
    WORKDIR /app
    
    ENV CI=true
    ENV NEXT_TELEMETRY_DISABLED=1
    ENV SLS_TELEMETRY_DISABLED=1
    ENV SLS_NOTIFICATIONS_MODE=off
    
    COPY --from=pruner /app/tsconfig.base.json ./
    COPY --from=pruner /app/out/pnpm-lock.yaml ./
    COPY --from=pruner /app/out/pnpm-workspace.yaml ./
    COPY --from=pruner /app/out/json/ .
    
    RUN --mount=type=cache,id=pnpm,target=${PNPM_STORE} \
        pnpm install --frozen-lockfile
    
    COPY --from=pruner /app/out/full/ .
    
    RUN printf '%s\n' \
      "NODE_ENV=production" \
      "USE_CDN=${USE_CDN}" \
      "UPLOAD_ENV=${UPLOAD_ENV}" \
      > ./services/${SERVICE}/.env.production
    
    RUN pnpm run build --filter=./services/${SERVICE}
    RUN pnpm --filter=./services/${SERVICE} run upload-assets
    
    RUN rm ./services/${SERVICE}/.env.production
    
    # -------------------------------------------------------- #
    # 5. Extract prod dependencies
    # -------------------------------------------------------- #
    FROM build AS dependencies
    ARG SERVICE
    LABEL stage=builder
    WORKDIR /app
    
    RUN pnpm --filter=./services/${SERVICE} deploy --legacy --prod --ignore-scripts --no-optional /dependencies \
        && pnpm prune --prod --no-optional
    
    # -------------------------------------------------------- #
    # 6. Runtime container
    # -------------------------------------------------------- #
    FROM core-os AS runner
    ARG SERVICE
    ARG APP_PORT
    LABEL stage=runner
    
    ENV NODE_ENV=production
    ENV PORT=${APP_PORT:-3000}
    
    WORKDIR /app
    
    COPY --from=build --chown=nextjs:nodejs /app/services/${SERVICE}/.next/standalone/services/${SERVICE} ./
    COPY --from=build --chown=nextjs:nodejs /app/services/${SERVICE}/.next/static ./.next/static
    COPY --from=build --chown=nextjs:nodejs /app/services/${SERVICE}/public ./public
    COPY --from=dependencies --chown=remix:nodejs /dependencies/node_modules/ ./node_modules/
    
    # Ensure writable .next/cache
    RUN mkdir -p .next/cache && chown -R 1001:1001 .next
    RUN mkdir -p .next/cache/fetch-cache && chown -R 1001:1001 .next
    
    RUN apk del curl gcc make python3 g++ || true \
      && rm -rf /usr/lib/node_modules/npm /usr/local/lib/node_modules/npm ~/.npm
    
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    CMD ["node", "./server.js"]
    EXPOSE ${APP_PORT:-3000}
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: examplesImprovements or additions to examplesgood first issueGood for newcomers to the repository

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions