Deploying a Next.js App with Neo

Next.js's standalone output mode produces exactly the small, self-contained image Neo deploys well.

5 min read

Enable standalone output

// next.config.js
module.exports = { output: 'standalone' };

This makes next build emit a minimal server bundle with only the dependencies actually used, instead of requiring the full node_modules tree in the runtime image.

Dockerfile

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

.neo.yml

name: site
port: 3000
env:
  NODE_ENV: production

For a mostly-static marketing site with a handful of dynamic routes, this is the whole config — Neo doesn't need to know it's Next.js specifically, only that a container listens on 3000.

Environment variables at build time vs. runtime

Next.js bakes NEXT_PUBLIC_* variables into the client bundle at build time — setting them via neo env set after the fact has no effect on already-built pages, since they're compiled into static JS. Server-only env vars (no NEXT_PUBLIC_ prefix) are read at runtime and do respond to neo env set followed by a restart.

If a public variable needs to change per environment, pass it as a Docker build arg and reference it in the Dockerfile, or rebuild for each environment rather than trying to inject it post-build.

ISR and persistent caching

If you're using Incremental Static Regeneration with the filesystem cache, mount it as a volume so revalidated pages survive a redeploy instead of resetting on every deploy:

volumes:
  cache: /app/.next/cache

Try it yourself — Neo is free, with every feature unlocked.

Get started free