Deploy to Production
How to deploy Apex Dashboard to a static host, Vercel, a self-hosted server, or Docker.
What you deploy is the out/ directory
next.config.ts sets output: "export", so npm run build writes a complete static site to out/ — prerendered HTML for every route, the JSON fixtures, and _headers. There is no Node.js server in production, and no .next/standalone build. Anything that can serve files can host this template.
Static Hosts
Cloudflare Pages, Netlify, GitHub Pages, Amazon S3 + CloudFront, Azure Static Web Apps, nginx — all of them work the same way. Build, then upload the directory.
npm run build
# then upload the contents of out/One routing rule matters. The export writes sibling files — /users is out/users.html, not out/users/index.html. Cloudflare Pages, Netlify and Vercel try the .html extension automatically. A hand-configured nginx does not, so tell it to:
server {
root /var/www/apex/out;
# Serve /users from users.html. Without this every route except / is a 404.
try_files $uri $uri.html $uri/index.html /404.html;
error_page 404 /404.html;
}public/_headers carries the security and caching headers and is read automatically by Cloudflare Pages and Netlify. On any other host, translate it into that host's own configuration — the content-hashed files under /_next/static are safe to serve immutable, and nothing else should be cached that way.
Vercel (Recommended)
Vercel is the easiest way to deploy a Next.js application. It provides automatic builds, preview deployments, and a global edge network.
- Push your code to a GitHub, GitLab, or Bitbucket repository.
- Go to vercel.com/new and import your repository.
- Vercel auto-detects Next.js — accept the defaults and click "Deploy".
- Every push to the main branch triggers a production deployment. Pull requests get preview URLs automatically.
No build configuration is needed. Vercel handles npm run build and serves the output from their global CDN.
Previewing the Build Locally
To check the real exported output before you upload it, serve out/ with the zero-dependency server included in tools/serve-static.mjs. It resolves routes the way Cloudflare Pages and Netlify do, so what you see locally is what the host will serve.
# Build the static export
npm run build
# Serve out/ on http://localhost:3737
npm run start
# Use a different port:
PORT=8080 npm run startThis is a preview server, not a production one. It has no compression, no TLS and sends cache-control: no-store so you never see a stale chunk from an earlier build. Put a real static host or an nginx in front of out/ for production rather than running this behind PM2.
It is also what the Playwright suite runs against, which is why npm run test:e2e tests the artefact you actually ship instead of the dev server.
Docker
Create a Dockerfile at the project root for containerized deployments:
# --- Build stage ---
FROM node:20-alpine AS builder
WORKDIR /app
# Note: a plain `npm ci`, not `--omit=dev`. Tailwind and TypeScript are
# devDependencies and the build needs them.
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Runtime stage ---
# The export is just files, so the runtime layer is out/ plus the ~60-line
# server. No node_modules, no framework at runtime.
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/out ./out
COPY --from=builder /app/tools/serve-static.mjs ./tools/serve-static.mjs
ENV PORT=3000
EXPOSE 3000
CMD ["node", "tools/serve-static.mjs"]Build and run the container:
docker build -t apex-dashboard .
docker run -p 3000:3000 apex-dashboardFor a production container, swap the runtime stage for nginx:alpine with the try_files rule shown above — you get compression and real cache headers, and the image carries no Node.js at all:
FROM nginx:alpine
COPY --from=builder /app/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.confEnvironment Variables
No variable is required — every one below has a working default. All of them are read at build time and must be NEXT_PUBLIC_-prefixed: a static export has no server process, so nothing can read a server-only secret at runtime. Set them in your host's build configuration, not in the deployed output.
| Variable | Description |
|---|---|
| NEXT_PUBLIC_SITE_URL | Canonical host written into sitemap.xml and robots.txt. Set this one — the default points at the demo site. |
| NEXT_PUBLIC_BASE_PATH | Sub-path prefix when the site is not served from the domain root. Needed alongside basePath in next.config.ts so the data provider still finds /api/*.json. |
| NEXT_PUBLIC_API_URL | Base URL of your real backend, once you swap the mock provider for the REST one. See src/lib/data/provider.ts. |
Create a .env.local file at the project root for local development. Never commit this file to version control — and remember that a NEXT_PUBLIC_ value is inlined into the JavaScript you ship, so it is public by definition. Nothing secret belongs in one.
Next Steps
Check the Changelog for the latest updates and release notes.