You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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 directoryWORKDIR /app
RUN npm install -g turbo
COPY . .
RUN turbo prune --scope=docs --docker
# Add lockfile and package.json's of isolated subworkspaceFROM 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 projectCOPY --from=builder /app/out/full/ .
RUN pnpm dlx turbo run build --filter=docs
FROM base AS runner
WORKDIR /app
# Don't run production as rootRUN 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-tracingCOPY --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
Reacted by Thomas Liu, Dương Tiến Vinh, Michitoshi Yano, Emanuel Avila, Eli, rustam, Ron Zano, Riku Rainio, Nimul Islam Mahin and Daniel Kouba
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 dependenciesCOPY --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 rootRUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodejs
USER nodejs
EXPOSE 8080
CMD ["node", "./dist/server.js"]
I think this issue should be elevated, since the only reason I've chosen pnpm is that the offical docs recommend using it:
So it's strange there aren't any examples.
FROM node:20 as base
# Update apt-get and install the necessary libraries# This is mainly so that the `canvas` package can be installedRUN 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 --activateFROM 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 dependenciesCOPY --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.
Reacted by João Vitor, Maxi, Keegan Gladstone, Maciej Kukiełka, Benjamin Chavez, Moises Marquez and Junpei
@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.
Reacted by Pawel Mioduszewski, Benjamin Chavez, James, Moises Marquez, Jennifer-JY-Lee, Craig Duckett, Thiago Moreira, Michał Janaszek, Vitor Markis, Eli and 17 more
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.
Reacted by Alex Chan, Anton Perezhogin and Anna Markova
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.4ARG 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#exampleRUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /workspace
# enable corepack for pnpmRUN corepack enable
FROM base as fetcher
# pnpm fetch only requires lockfile, but we'll need to build workspacesCOPY pnpm*.yaml ./
COPY patches ./patches
# mount pnpm store as cache & fetch dependenciesRUN --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 buildARG 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 appRUN --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 appRUN pnpm --filter ${APP_NAME} deploy --prod --ignore-scripts ./out
FROM base as runner
WORKDIR /workspace
# Don't run production as rootRUN addgroup --system --gid 1001 mygroup
RUN adduser --system --uid 1001 myuser
USER myuser
# copy files needed to run the appCOPY --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 appCMD 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/**
Reacted by Riley Tomasek, rexdavid, David, James, Luke Snape, Mark Velnik, yuji-taira and Manas Pratim Biswas
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
Reacted by SoonMin
Reacted by Anthony Cyrille, Finn Fahrenkrug, Vinay, James, Mark Arseneault, Benjamin Chavez, Vitaliy Snitovets, Danny, Anton Perezhogin, Sandu Morari and 6 more
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!
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:
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"]
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)
@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.
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"]
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"]
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"]
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
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
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
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...
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).
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
ARG NODE_VERSION=18.18.0
ARG PROJECT=user-app
# Alpine base with necessary dependenciesFROM node:${NODE_VERSION}-alpine AS alpine
RUN apk update
RUN apk add --no-cache libc6-compat
# Setup pnpm and turbo on the alpine baseFROM alpine as base
RUN npm install pnpm@10.11.0 turbo@2.5.3 --global
RUN pnpm config set store-dir ~/.pnpm-store
# Prune projectsFROM base AS pruner
ARG PROJECT
WORKDIR /app
COPY . .
RUN turbo prune --scope=${PROJECT} --docker
# Build the projectFROM base AS builder
ARG PROJECT
WORKDIR /app
# Copy lockfile and package.json's of isolated subworkspaceCOPY --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 subworkspaceCOPY --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 targetRUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm prune --prod --no-optional
FROM alpine AS runner
ARG PROJECT
WORKDIR /app
# Create non-root userRUN 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/
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
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/cacheRUN 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}
Activity
jvzaniolo commentedon Jul 5, 2023
Here's my working version with
pnpm. I just copied the example withyarnand replaced it withpnpm, but it doesn't usepnpm fetchedouardr commentedon Jul 7, 2023
Hi, here is my version. Please let me know if there is any way to improve it.
nicu-chiciuc commentedon Jul 12, 2023
I think this issue should be elevated, since the only reason I've chosen

pnpmis that the offical docs recommend using it:So it's strange there aren't any examples.
One issue I'm having is this:
I assume the problem is the way in which internal packages are symlinked, but this is just an idea.
jvzaniolo commentedon Jul 12, 2023
That's exactly my point here. The Docker examples were made with
yarnbut the rest of the docs and other examples use and recommendpnpm.nicu-chiciuc commentedon Jul 12, 2023
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
to my
.dockerignorefile (I already hadnode_modules)Before adding
**/node_modulesI had issue on theCOPY --from=builder /app/out/full/ .step.And here's my current
Dockerfile:I know that this dockerfile is much less optimized than the versions in other comments.
I've used
node:20instead 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.
nicu-chiciuc commentedon Jul 15, 2023
I created #5536, I think it should solve the issue.
josefaidt commentedon Jul 18, 2023
I came across this issue today looking for some insights around a missing lockfile in the
out/jsondirectory wherepnpm-workspace.yamlis copied.I also noticed a few comments here show
patchesgetting 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 prunedoesn't ignore lifecycle scripts. be careful how you structure your npm scripts for when you're pruning the virtual store for the final build imagepnpm fetch, I originally did not think it supported--ignore-scriptsbut apparently it does! I'd recommend adding that. If any of the installed dependencies have apostinstallstep that calls a transient dependencies bin then it will fail without--ignore-scripts.pnpm dlxwhich doesn't seem to cache well (10s+ on each build)dlxpnpm will unfortunately not load turbo from the virtual store but rather the registry which is likely the cause for the delay on buildIn my experience pnpm and turbo seem to have conflicting approaches to building a docker image.
pnpm fetchis 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 ... --dockerwill generate a pruned lockfile, which we can then usepnpm fetchon (but we needturboinstalled first`)What I've landed on is to use
turbofor building only with a mix ofpnpm deploy(on 8.6.6):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.turbocache in the build effectively (with buildkit cache or otherwise -- afaict I'm not ignoring .turbo dirs)?pnpm deployis 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 symlinkedpackages/**leetmachine commentedon Sep 8, 2023
Had same problem on
COPY --from=builder /app/out/full/ .line as everyone else usingpnpm.Added
**/node_modulesto root.dockerignoreto 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: standaloneEnded 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.
25 remaining items
EddyVinck commentedon Apr 11, 2024
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"innext.config.js**/node_modulesin.dockerignoreMy Dockerfile skills aren't great so this helped a lot! Thank you!
joakim-roos commentedon May 27, 2024
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:
This is my Dockerfile:
When trying to run the container i'm getting:
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)
If anyone has any idea to share how to solve this it's greatly appreciated! <3
edouardr commentedon Jun 3, 2024
@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-cacheand--progress=plainto your docker build command. This will log every output of your multi staged build. From there you should see ifpnpm installruns as expected.joakim-roos commentedon Jun 4, 2024
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:
Pedro-phd commentedon Jul 1, 2024
Hello, could anyone help me? I am having this problem in the running
RUN turbo run build --filter=webmy dockerfile:
Security2431 commentedon Oct 26, 2024
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:
tadeumaia commentedon Dec 1, 2024
For anyone using the example above, the COPY command is incorrect. It should be:
The correct format is always
--chown=user:group.Security2431 commentedon Dec 2, 2024
@tadeumaia Thank you for pointing this out. I've fixed the previous example
blitzzstorm commentedon Jan 5, 2025
If Docker container is referenced using its internal hostname, such as:
Include these in the runner step.
amosbastian commentedon Jan 27, 2025
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...
nktnet1 commentedon Feb 22, 2025
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:
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).robinvriens commentedon Mar 23, 2025
rinshadibnabu commentedon May 24, 2025
I spent hours debugging this exact issue and finally found a complete solution for me . The problem isn't just the
binaryTargetsin schema.prisma - it's a combination of build order, path resolution.#Next.js standalone builds look for Prisma client in these locations
#prisma was creating in the these file
was packages/db/generated
1. Correct schema.prisma:
**2. Fixed Dockerfile **
Verify Success
Project structure this works with:
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
YKDZ commentedon Jun 3, 2025
My personal solution with
prisma,vike,turbo prune,pnpm pruneandpnpm deploy:Number16BusShelter commentedon Jun 25, 2025
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: falseinnext.config.js. Otherwise, even with a loader defined, image loader is ignored .Env vars during build behave weirdly:
ENValone won’t be picked up bynext build. You must create a.env.productionfile with your build env vars during the build stage—otherwise they'll silently not load at runtime.