Search code examples
dockernext.jsdocker-composedockerfile

Error during multistage Docker build: sh: next: not found


I have a NextJS project with the following lines in my package.json:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
  },

All three scripts run without any hiccups from my command line. Now I’m trying to dockerize my application. My definitions are in a Dockerfile.prod:

# Final stage
# --------------------------------------------------------
# Install Node base image with Alpine Ubuntu
FROM node:20.11-alpine AS builder
# Set the working directory inside the container
WORKDIR /app
# Install pnpm globally in the builder stage
RUN apk add --no-cache bash
RUN npm install -g pnpm
# Copy package.json and pnpm-lock.yaml
COPY pnpm-lock.yaml package.json ./
# Install dependencies using pnpm
RUN pnpm install
# Copy the rest of the files to the working directory
COPY . .
# Generate Prisma Client
RUN npx prisma db push
RUN npx prisma generate
RUN pnpm build

# Runner stage
# --------------------------------------------------------
# Install Node base image with Alpine Ubuntu
FROM node:20.11-alpine AS runner
# Set the working directory inside the container
WORKDIR /app
# Install pnpm globally in the builder stage
RUN apk add --no-cache bash
RUN npm install -g pnpm
# Copy only the necessary files from the builder stage
COPY --from=builder /app/next.config.js /app/.next /app/public /app/node_modules ./
# Expose port 3000 from the container
EXPOSE 3001
# Start the Next.js server
CMD ["pnpm", "start"]

Besides, I also have a docker-compose.yaml that looks like this:

version: "3.8"
services:
  poco-production:
    build:
      context: .
      dockerfile: Dockerfile.prod
    container_name: poco-web-prod
    ports:
      - "3001:3001"
    volumes:
      - .:/app
      - /app/node_modules
    env_file:
      - .env

Now when I build this image using docker-compose up --build, I get the following error:

Attaching to poco-web-prod poco-web-prod | poco-web-prod | > poco@0.1.0 start /app poco-web-prod | > next start poco-web-prod | poco-web-prod | sh: next: not found poco-web-prod |  ELIFECYCLE  Command failed.

What am I missing here? ChatGPT says this:

The error message you’re encountering—sh: next: not found—indicates that the next command is not being found within your container.

But that can’t be true because this error only comes at next start while next build runs just fine. I am also copying my node_modules from the build stage into the runner stage. What else could be missing?

UPDATE

As suggested by Ahmed Abdelbaset in his answer below, I updated my Dockerfile to this:

# Install Node base image with Alpine Ubuntu
FROM node:20.11-alpine AS base

FROM base AS deps
# 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
WORKDIR /app
# Copy dependency lists
COPY package.json pnpm-lock.yaml* ./
# Enable corepack for pnpm
RUN corepack enable pnpm
# Install dependencies using pnpm
RUN pnpm i --frozen-lockfile



# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma Client
RUN npx prisma db push
RUN npx prisma generate
# Enable corepack for pnpm and install dependencies
RUN corepack enable pnpm && pnpm build


# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Disable NextJS telemetry during runtime
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

And added output: "standalone" to my next.config.js as follows:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.resolve.alias.canvas = false
    config.resolve.alias.encoding = false
    return config
  },
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "lh3.googleusercontent.com",
        pathname: "**",
      },
    ],
  },
  output: "standalone",
}

module.exports = nextConfig

This is now throwing a different error:

Cannot find module '/app/server.js'


Solution

  • The error message you’re encountering—sh: next: not found—indicates that the next command is not being found within your container.

    That's true, before the pnpm build (next build) you have pnpm install which generates node_modules. But before pnpm start (next start), You don't have node_modules.

    COPY --from=builder /app/next.config.js /app/.next /app/public /app/node_modules ./
    

    The previous line will copy the contents of the sources not the directories themselves. The output will not be:

    - next.config.js
    - .next/
      - static
      - server
      - ...
    - public
      - ...
    - node_modules
      - .pnpm
      - next
      - ...
    

    But it will be

    - next.config.js
    - static
    - server
    - .pnpm
    - ...
    

    so, there is no node_modules.

    Next.js provides a standalone output the collect all necessary files to run output in Docker in one place. To enable, configure next.config.js

    module.exports = {
      output: "standalone",
    }
    

    After running next build, you'll have .next/standalone dir that is stand alone in itself - no need to any other directory.

    # Runner stage
    # --------------------------------------------------------
    # Install Node base image with Alpine Ubuntu
    FROM node:20.11-alpine AS runner
    # Set the working directory inside the container
    WORKDIR /app
    
    # Copy only the necessary files from the builder stage
    COPY --from=builder /app/.next/standalone ./
    COPY --from=builder /app/public ./public
    COPY --from=builder /app/.next/static ./static
    
    # Expose port 3001 from the container
    EXPOSE 3001
    # Start the Next.js server
    CMD ["node", "server.js"]
    

    During running the server, Next.js will write/read .next this might conflict with the docker permissions. If that happened you will need

    # Runner stage
    # --------------------------------------------------------
    # Install Node base image with Alpine Ubuntu
    FROM node:20.11-alpine AS runner
    # Set the working directory inside the container
    WORKDIR /app
    
    
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    
    # Set the correct permission for prerender cache
    RUN mkdir .next
    RUN chown nextjs:nodejs .next
    
    # Copy only the necessary files from the builder stage
    COPY --from=builder /app/public ./public
    COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
    COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./static
    
    USER nextjs
    
    # Expose port 3001 from the container
    EXPOSE 3001
    # Start the Next.js server
    CMD ["node", "server.js"]
    

    Further Reading