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
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);
});
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.
Further details can be found in this blog post.