Deployment process: review code, build, deploy and maintain

A self-managed Linux VPS can run a compatible Next.js application as a Node.js server or container. This walkthrough uses standalone output and a reverse proxy; it is different from a static export, which does not support every server feature.

Prerequisites and the example’s scope

Use a supported Linux VPS with Docker Engine, Compose and a domain you control. The reference uses Node 24 LTS and a locked Next.js dependency tree. Check your own project’s Node requirements, native dependencies and framework version. Plan capacity for the build as well as the running app; compiling on a separate build machine may be more appropriate than using a small production server.

The sample contains a server-rendered page, a health endpoint and a static asset. It does not supply your authentication, database or a managed application service.

Download the reference projects (ZIP) for static, Node.js and Next.js examples. Read the included README and choose the example matching your application.

Enable standalone output

In an existing Next.js project, merge this setting into its configuration rather than replacing unrelated options.

export default {output:'standalone',poweredByHeader:false};

Build the runtime image

The example installs locked dependencies, compiles the application, copies standalone output and static assets, then runs as a non-root user. Keep public and .next/static with the standalone server.

FROM node:24-alpine AS dependencies
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./
RUN npm ci

FROM node:24-alpine AS build
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s CMD node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]

From the example’s nextjs directory, build and test using a loopback-only host binding:

docker build -t my-next-app:release-1 .
docker run --rm -p 127.0.0.1:18080:3000 -e APP_GREETING="Local release check" my-next-app:release-1

In another terminal, open http://127.0.0.1:18080/ and /healthz. The local HTTP check does not request a public certificate.

Configure the domain and reverse proxy

The production Compose example connects Caddy to the application over Docker networking. Only Caddy publishes ports 80 and 443. Its reverse-proxy configuration is:

{$APP_DOMAIN:localhost} {
    encode gzip zstd
    reverse_proxy app:3000
    log
}

On the intended fresh server, copy .env.example to a private .env file, set APP_DOMAIN to the real hostname and choose a release label. Check DNS and available ports, then validate the configuration before starting it.

docker compose config --quiet
docker compose build
docker compose up -d
docker compose ps
docker compose logs --tail=100 app proxy

Caddy can obtain a certificate when the domain and challenge requirements are satisfied. Retain its certificate data volumes. This is software you operate, not a separate managed SSL service included in the VPS.

Handle environment variables correctly

Public NEXT_PUBLIC_ values are embedded during a build and are visible to users. Do not place secrets there. Runtime server configuration is separate; our dynamic sample reads APP_GREETING on the server. Review whether each variable in your app is needed during the build, at runtime or both.

Use a secret manager or restricted secret files for sensitive values, and ensure the application supports the chosen mechanism. A production build should not require copying a developer’s private .env into the image.

Account for cache, uploads and database state

The sample is stateless. If your app relies on persistent uploads, cached generated content or a database, design their storage and backups explicitly. Multiple application instances may require coordinated cache behaviour and consistent configuration; adding a second container is not a complete scaling strategy.

Diagnose startup and proxy failures

  • 502 response: confirm the app is healthy, listening on the configured interface and reachable on the proxy’s network.
  • Missing scripts or images: verify that static output and public assets were copied.
  • Build terminated: inspect memory and disk availability; use a separate builder or a larger measured resource budget.
  • Old public setting: rebuild when a build-time frontend variable changes.

Release, verify and recover

Deploy a specific reviewed commit and record its image version. Check application routes, login, permissions, an error path and the health check before sending visitors to it. Keep logs free of tokens and personal information. Test a restart so that a temporary interactive shell is not the only thing keeping the application running.

Keep recoverable copies of persistent data, required configuration and the previous application image outside the server. A container restart policy is not a backup. Rehearse a restore into a separate environment. Reverting an application image does not automatically reverse a database migration; design backward-compatible changes and keep a migration recovery plan.

Technical references