node.jsdockernext.jsdocker-composeturborepo

Error: Cannot find module '/app/apps/landing/server.js'


Goal: Being able to start an app via a global docker-compose

Problem: the build works correctly, it is the start part of the app next via

node ./apps/landing/server.js

which cannot find the server.js file

node: internal/modules/cjs/loader: 1137
throw err;

Error: Cannot find module '/app/apps/landing/server.js'
  at Module._resolveFilename (node: internal/modules/cjs/loader: 
  1134:15)
  at Module._load (node: internal/modules/cjs/loader: 975:27)
  at Function. executeUserEntryPoint [as runMain] (node: 
  internal/modules/run_main: 128:12)
  at node: internal/main/run_main_module:28:49 {
 code: ' MODULE. _NOT_FOUND',
 requireStack: []
}

Node. ]s v18.19.0

while the file via inspect docker desktop exists

docker desktop

Architecture

├── apps
│   └── landing
│       ├── Dockerfile
│       └── next.config.js
└── docker-compose.yml

apps/landing/Dockerfile

FROM node:18-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
 
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN pnpm add -g turbo
COPY . .
RUN turbo prune landing --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 --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
 
# Build the project
COPY --from=builder /app/out/full/ .
RUN pnpm turbo run build --filter=landing...

ENV NODE_ENV=production
ENV PORT=3000
 
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/landing/next.config.js .
COPY --from=installer /app/apps/landing/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/landing/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/landing/.next/static ./apps/landing/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/landing/public ./apps/landing/public

EXPOSE 3000

CMD node ./apps/landing/server.js

docker-compose.yml

version: '3.8'
services:
  landing:
    container_name: landing
    image: landing
    build:
      context: .
      dockerfile: ./apps/landing/Dockerfile
    volumes:
      - .:/app
      - pnpm-store:/pnpm/store
    ports:
      - "3000:3000" # Map the port to the host (adjust if your app uses a different port)
    environment:
      - PNPM_HOME=/pnpm
      - PATH=$PNPM_HOME:$PATH
    user: "1001" # Run the container as the non-root user created in the Dockerfile
    networks:
      - landing

networks:
  landing:
    name: landing
    driver: bridge

volumes:
  pnpm-store:
    name: pnpm-store

*apps/landing/next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
  productionBrowserSourceMaps: false,
  transpilePackages: [
    "@fizzy/ui",
    "lucide-react"
  ]
}

module.exports = nextConfig

Testing

When I manually force the import of the server.js file that I would have generated locally from Docker Desktop, it works.

/app/apps/landing/server.js

const path = require('path')

const dir = path.join(__dirname)

process.env.NODE_ENV = 'production'
process.chdir(__dirname)

// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGTERM', () => process.exit(0))
  process.on('SIGINT', () => process.exit(0))
}

const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || '0.0.0.0'

let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = {"env":{},"eslint":{"ignoreDuringBuilds":false},"typescript":{"ignoreBuildErrors":false,"tsconfigPath":"tsconfig.json"},"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","configOrigin":"next.config.js","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"analyticsId":"","images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[16,32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":60,"formats":["image/webp"],"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"inline","remotePatterns":[],"unoptimized":false},"devIndicators":{"buildActivity":true,"buildActivityPosition":"bottom-right"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"amp":{"canonicalBase":""},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"optimizeFonts":true,"excludeDefaultMomentLocales":true,"serverRuntimeConfig":{},"publicRuntimeConfig":{},"reactProductionProfiling":false,"reactStrictMode":null,"httpAgentOptions":{"keepAlive":true},"outputFileTracing":true,"staticPageGenerationTimeout":60,"swcMinify":true,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"},"next/server":{"transform":"next/dist/server/web/exports/{{ kebabCase member }}"}},"experimental":{"windowHistorySupport":false,"serverMinification":true,"serverSourceMaps":false,"caseSensitiveRoutes":false,"useDeploymentId":false,"useDeploymentIdServerActions":false,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","middlewarePrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":11,"memoryBasedWorkersCount":false,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"isrMemoryCacheSize":52428800,"fullySpecified":false,"outputFileTracingRoot":"/Users/fabio/Development/fizzy/pay/landing-pay","swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"adjustFontFallbacks":false,"adjustFontFallbacksWithSizeAdjust":false,"typedRoutes":false,"instrumentationHook":false,"bundlePagesExternals":false,"ppr":false,"webpackBuildWorker":false,"optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"configFileName":"next.config.js","transpilePackages":["@fizzy/ui","lucide-react"]}

process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)

require('next')
const { startServer } = require('next/dist/server/lib/start-server')

if (
  Number.isNaN(keepAliveTimeout) ||
  !Number.isFinite(keepAliveTimeout) ||
  keepAliveTimeout < 0
) {
  keepAliveTimeout = undefined
}

startServer({
  dir,
  isDev: false,
  config: nextConfig,
  hostname,
  port: currentPort,
  allowRetry: false,
  keepAliveTimeout,
}).catch((err) => {
  console.error(err);
  process.exit(1);
});

Solution

  • This captures the essence of what you are trying to do.

    ├── apps
    │   └── landing
    │       ├── Dockerfile
    │       ├── next.config.js
    │       ├── package.json
    │       ├── pages
    │       │   └── index.js
    │       └── public
    ├── docker-compose.yml
    ├── package.json
    ├── pnpm-lock.yaml
    ├── pnpm-workspace.yaml
    └── turbo.json
    

    As specified there is no server.js since this will be built on the image.

    🗎 docker-compose.yml

    version: '3.8'
    services:
      landing:
        container_name: landing
        build:
          context: .
          dockerfile: ./apps/landing/Dockerfile
        ports:
          - "3000:3000"
        environment:
          - NODE_ENV=production
    

    🗎 Dockefile

    FROM node:18.17.0-alpine as base
    ENV PNPM_HOME="/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    RUN corepack enable
    
    RUN apk add --no-cache libc6-compat
    RUN apk update
    
    RUN pnpm add -g turbo
    
    # BUILDER ---------------------------------------------------------------------
    
    FROM base AS builder
    
    WORKDIR /app
    
    COPY . .
    RUN turbo prune landing --docker
    
    COPY ./package.json ./pnpm-lock.yaml ./pnpm-workspace.yaml /app/
    COPY ./apps/landing /app/apps/landing
    
    RUN pnpm install --frozen-lockfile
    
    WORKDIR /app/apps/landing
    RUN pnpm turbo run build
    
    # RUNNER ----------------------------------------------------------------------
    
    FROM base AS runner
    
    WORKDIR /app
    
    RUN addgroup --system --gid 1001 nodejs
    RUN adduser --system --uid 1001 nextjs
    USER nextjs
    
    COPY --from=builder /app/apps/landing/.next/standalone /app
    
    CMD node ./apps/landing/server.js
    

    I have omitted the installer stage since I'm not 100% sure that it's necessary.

    enter image description here

    Further details can be found in this blog post.