Search code examples
reactjstypescriptdockernext.jsdockerfile

Next.JS v13 in Docker does not respect path alias but works locally


I had a working docker build and when I put in absolute paths with a path alias, it suddenly started complaining module not found, can't resolve 'ComponentName'

It will fail at the pnpm run build step, but when I run it locally, it works fine.

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./src/*"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

The file causing the error:

import { Providers } from './providers';
import NavBar from '~/components/NavBar';
import { css } from '~/styles/css';

import './global.css';

export const metadata = {
  title: 'p-stack-fs',
  description: 'A boilerplate for TypeScript, Next.js, and PostgreSQL',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>
          <div className={css({ minH: '100vh' })}>
            <NavBar />
            {children}
          </div>
        </Providers>
      </body>
    </html>
  );

Error:

 > [builder 6/6] RUN pnpm run build:
#25 0.534
#25 0.534 > p-stack-fs@0.1.0 build /app
#25 0.534 > next build
#25 0.534
#25 0.940 - warn You have enabled experimental feature (instrumentationHook) in next.config.js.
#25 0.941 - warn Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.
#25 0.941
#25 1.010 - info Creating an optimized production build...
#25 10.94 Failed to compile.
#25 10.94
#25 10.95 ./src/app/layout.tsx
#25 10.95 Module not found: Can't resolve '~/components/NavBar'
#25 10.95
#25 10.95 https://nextjs.org/docs/messages/module-not-found
#25 10.95
#25 10.95 ./src/app/layout.tsx
#25 10.95 Module not found: Can't resolve '~/styles/css'
#25 10.95
#25 10.95 https://nextjs.org/docs/messages/module-not-found
#25 10.95
#25 10.95
#25 10.95 > Build failed because of webpack errors
#25 11.09  ELIFECYCLE  Command failed with exit code 1.
------
executor failed running [/bin/sh -c pnpm run build]: exit code: 1

You can see the files exist and are named properly here:

enter image description here

FWIW, here's my Dockerfile

ARG ALPINE_VERSION=3.17

FROM node:20-alpine AS base

# Install dependencies only when needed
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 package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY panda.config.ts ./

RUN npm i -g pnpm && pnpm i --prod --frozen-lockfile

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Panda is built in the `prepare` command, which runs after
# `install`, so we copy it over here for building
# COPY --from=deps /app/src/styles/ ./src/styles/

RUN ["chmod", "+x", "./docker-entrypoint.sh"]

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1

RUN npm i -g pnpm
RUN pnpm run build

# Production image, copy all the files and run next
FROM alpine:${ALPINE_VERSION} AS runner

WORKDIR /app

# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1

RUN apk add --no-cache --update nodejs
# Get AWS SSM Params using a Go binary, reduces image size
# Note: this might get a "8: not found" error if not using the USER nextjs, has
# to do with something with the ca-certificates
RUN apk update && apk add --no-cache ca-certificates && update-ca-certificates
RUN wget https://github.com/pthieu/go-aws-get-parameter/raw/master/ssm_get_parameter
RUN ["chmod", "+x", "./ssm_get_parameter"]

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

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

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

USER nextjs

EXPOSE 80

ENV PORT 80

ENTRYPOINT [ "sh", "docker-entrypoint.sh" ]
CMD ["node", "server.js"]
# For debugging, keeps container alive
# CMD ["tail", "-f", "/dev/null"]

Solution

  • Found the issue.

    The solution was to update next.config.js to set the path alias in webpack. This should be the same as your tsconfig.json.

    const nextConfig = {
      output: 'standalone',
      webpack: (config, { isServer }) => {
        config.resolve.alias['~'] = path.join(__dirname, 'src');
        return config;
      },
    };
    

    Here's my hypothesis: in Next.js 13, they moved to turbopack but still use Webpack as a bundler. Webpack runs at a different phase than the TS compilation phase, so it doesn't take into account tsconfig.json.