← All posts
DevOps7 min readProject: Auth admin panel

Deploying Next.js under a sub-path: basePath gotchas we hit in production

Running a Next.js app at /tools/auth-admin instead of / looks like a one-line config change. In production, it broke the post-login redirect, links to static files and environment handling. Here is what basePath does and doesn't prefix, the bug that sent users to the wrong URL after login, and the configuration that finally made it boring.

#Next.js#basePath#Deployment#Docker#Nginx#Reverse Proxy

The problem

Behind a reverse proxy at a sub-path, the admin panel sent users to the wrong URL after login and broke links to static files.

The result

One environment flag drives basePath everywhere, redirects and static links include the prefix, and the app runs as a standalone Docker image next to other services.

Stack

Next.jsDockerNginxTypeScript

The problem

Our auth admin panel isn't deployed on its own domain. It lives on a shared host behind a reverse proxy, next to several other applications, each under its own path:

https://backend.company.com/tools/auth-api/      ← the auth backend
https://backend.company.com/tools/auth-admin/    ← this admin panel
https://backend.company.com/tools/…              ← other apps

Next.js supports this with one setting, basePath. Locally, everything ran at / and worked perfectly. In production, a few things went wrong right away:

  • after logging in, users landed on https://backend.company.com/applications, a page that doesn't exist, instead of …/tools/auth-admin/applications,
  • links to files in public/ (like downloadable guides) returned 404,
  • the app needed to know whether it was in production in the browser too, not just at build time.

None of these show up in development, because in development there's no prefix.

The details

What basePath does

// next.config.ts
const nextConfig: NextConfig = {
  basePath: '/tools/auth-admin',
};

With this, Next.js:

  • serves every page under the prefix (/applications becomes /tools/auth-admin/applications),
  • prefixes <Link href="/applications"> automatically,
  • prefixes router.push('/applications') automatically,
  • serves its own assets (/_next/…) under the prefix.

That covers most of an app, which is exactly why the remaining cases are so surprising.

What basePath doesn't do

basePath only rewrites URLs that go through Next.js's routing helpers. Anything you build yourself as a string stays exactly as you wrote it:

  • plain <a href="/guides/file.md"> to files in public/,
  • window.location.href = … assignments,
  • URLs you send to another service, like a returnTo parameter for a login redirect,
  • fetch('/something') to your own routes.

The challenges

The post-login redirect

Login isn't an API call: the browser is redirected to the auth backend, through the company's identity provider, and then sent back to a returnTo address that the panel provides:

const returnTo = window.location.origin + config.loginReturnPath;
window.location.href = `${config.apiUrl}/auth/login?returnTo=${encodeURIComponent(returnTo)}`;

loginReturnPath was "/applications". Next.js never sees that string. It's sent to another service, which redirects the browser to https://backend.company.com/applications: no prefix, wrong app, 404.

Static files

The integration guides are Markdown files in public/guides/, linked for download. A plain <a href="/guides/integration-nestjs.md"> points to the root of the host, not the app.

Environment in the browser

Whether the prefix applies depends on the environment (none locally, the sub-path in production). next.config.ts runs at build time on the server, but the browser code also needs to know the prefix to build correct URLs, and in Next.js only variables prefixed with NEXT_PUBLIC_ reach the browser.

A shared host

The app runs next to other services, so it can't assume it owns port 3000, and the proxy must forward the sub-path correctly.

The solution

1. One flag, read in both places

// next.config.ts
const appEnv = process.env.NEXT_PUBLIC_APP_ENV || 'production'; // no value → production

const nextConfig: NextConfig = {
  output: 'standalone',
  ...(appEnv === 'production' && { basePath: '/tools/auth-admin' }),
};
// lib/config.ts: the only file that reads process.env
// Mirrors APP_ENV from next.config.ts. This file runs in the browser,
// and only NEXT_PUBLIC_-prefixed variables are visible there.
const appEnv = process.env.NEXT_PUBLIC_APP_ENV || 'production';

export const config = {
  /** Mirrors basePath in next.config.ts. Next prefixes <Link> and the router,
   *  but not plain <a href> to files in public/, so those add it. */
  basePath: appEnv === 'production' ? '/tools/auth-admin' : '',

  /** Where the browser lands after a successful login. Sent to another
   *  service, so Next can't prefix it: it must include the basePath. */
  loginReturnPath: appEnv === 'production' ? '/tools/auth-admin/applications' : '/applications',
} as const;

Two choices worth noting:

  • The variable is NEXT_PUBLIC_, so the same flag is visible to the build config and to browser code. Two separate variables would eventually disagree.
  • The default is production. If someone forgets to set it on the server, the app still gets its production prefix. Local development sets it explicitly. Defaulting to "development" would silently break production.

2. Fix the redirect: include the prefix where Next can't

The redirect bug was fixed by putting the full path in loginReturnPath. Once the string included the prefix, the identity provider sent users back to the right page.

The general rule: any URL that leaves Next.js (sent to another service, assigned to window.location, put in an email) must be built with the base path explicitly.

3. Prefix static file links

Links to files in public/ use the configured base path:

export function guideUrl(guide: Guide): string {
  return `${config.basePath}/guides/${guide.file}`;
}

One helper means every download link is right, and nobody has to remember the rule at each call site.

4. Standalone output for Docker

output: 'standalone',

With standalone output, next build produces a minimal server with only the dependencies it needs. The Docker image is built in three stages (install dependencies, build, run): the final stage copies only the standalone folder, the static assets and public/, runs as a non-root user, and starts with node server.js. Small images, fast starts, no full node_modules in production.

One trap deserves its own line: NEXT_PUBLIC_ variables are baked into the JavaScript bundle at build time. Setting them when the container starts changes nothing in the browser code. So the API URL (and anything else the browser needs) is passed as a Docker build argument:

ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build

Changing it means rebuilding the image, not just restarting the container.

5. Play nicely on a shared host

  • The container publishes a dedicated host port instead of assuming 3000, because other apps already live on the same machine.
  • The reverse proxy must agree with the app about the prefix. With basePath, the app expects requests to include /tools/auth-admin/, so the proxy should forward the path as is. Stripping the prefix in the proxy and setting basePath is a classic way to get a redirect loop or 404s.

6. A checklist for sub-path deployments

What we now check whenever an app moves under a prefix:

  • basePath set from one environment flag, defaulting to production
  • the same flag readable in the browser (NEXT_PUBLIC_)
  • every returnTo / callback URL sent to other services includes the prefix
  • every window.location assignment includes the prefix
  • every link to public/ files uses a helper that adds the prefix
  • the proxy forwards the sub-path as is
  • test once in a production-like setup, behind the proxy, before release

Lessons learned

  • basePath only prefixes what Next.js routes. <Link> and the router: yes. Strings you build yourself: no.
  • URLs that leave your app are your responsibility. Redirect targets, callbacks and emails need the full path.
  • Read the same flag everywhere. One NEXT_PUBLIC_ variable for config and browser code avoids drift.
  • Default to the safe environment. If a variable goes missing on the server, production should still be correct.
  • Centralize URL building. A helper for static files removes a whole category of 404s.
  • Know who strips the prefix. Either the proxy or the app owns it, never both.
  • Development hides path bugs. Test behind the real proxy before you ship.

FAQ

What does basePath do in Next.js?

It serves the whole app under a URL prefix (like /docs) and automatically adds that prefix to next/link, next/router/next/navigation and Next.js's own assets. Plain anchors, manual window.location changes and URLs sent elsewhere are not prefixed.

Why do links to files in the public folder break with basePath?

Because a plain <a href="/file.pdf"> isn't processed by Next.js. Build those URLs with the base path yourself, for example through a small helper that prepends it.

Why does my login redirect go to the wrong page after adding basePath?

The redirect URL (returnTo or callback) is a plain string sent to another service, so Next.js can't prefix it. Include the base path in that URL explicitly.

Should my reverse proxy strip the path prefix for a Next.js app with basePath?

No. With basePath, the app expects requests to include the prefix. Forward the path unchanged; strip it only if you don't use basePath at all.

Facing a similar problem?

I help teams fix performance, backend and reliability issues in production.

Let's talk
Welcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have funWelcome to My WebSite please enjoy and have fun

Componentity

Thanks for visiting my portfolio. Every project here reflects my passion for building and learning. If you’re interested in working together, have a question, or just want to connect—don’t hesitate to reach out. I'm always open to meaningful conversations and new ideas.

© 2026 Zakariya Zouazou. All Rights Reserved.

Contact Us