Nginx for Next.js: a production config with HTTPS, caching and standalone builds
Run Next.js behind Nginx: a complete config with HTTPS and long caching for _next/static, the standalone build, a systemd service, and the errors people run into.
by OhSudo ·
next start runs a fine production server, but on port 3000 and without HTTPS. Nginx adds those, and it can also serve Next.js’s build files directly, which are safe to cache forever.
The Nginx Config Generator with these options writes the HTTPS and proxy parts for your domain. The /_next/static/ location below is the part you add.
The config
server { listen 80; listen [::]:80; server_name example.com; return 301 https://example.com$request_uri;}
server { listen 443 ssl; listen [::]:443 ssl; server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
gzip on; gzip_types text/css application/javascript application/json image/svg+xml;
# Build files have a hash in their name, so they never change. location /_next/static/ { alias /var/www/myapp/.next/static/; expires 1y; add_header Cache-Control "public, immutable"; access_log off; }
location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host;
# Next.js sends large headers with some features. proxy_buffer_size 16k; proxy_buffers 8 16k; }}1. Build and run the app
cd /var/www/myappnpm cinpm run buildSave this as /etc/systemd/system/myapp.service:
[Unit]Description=Next.js appAfter=network.target
[Service]User=www-dataWorkingDirectory=/var/www/myappEnvironment=NODE_ENV=productionExecStart=/usr/bin/npx next start -H 127.0.0.1 -p 3000Restart=on-failure
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now myappcurl -I http://127.0.0.1:3000/-H 127.0.0.1 keeps port 3000 off the internet, so everything goes through Nginx. The systemd Unit Generator can write a fuller unit.
2. Or use the standalone build
For deployments, it’s smaller and doesn’t need your whole node_modules. In next.config.js:
module.exports = { output: 'standalone' };The build then has its own server. It leaves out public and .next/static, so copy those in yourself. Forgetting them is the usual reason for a site with no styles and no images:
npm run buildcp -r public .next/standalone/cp -r .next/static .next/standalone/.next/Change the service to run it:
WorkingDirectory=/var/www/myapp/.next/standaloneEnvironment=NODE_ENV=productionEnvironment=PORT=3000Environment=HOSTNAME=127.0.0.1ExecStart=/usr/bin/node server.jsPoint alias in the Nginx config at wherever .next/static really is, so that’s /var/www/myapp/.next/standalone/.next/static/. The trailing / on alias matters.
The Dockerfile Generator can write an image for Node.js apps too.
3. Get HTTPS, and turn it on
sudo certbot certonly --standalone -d example.com \ --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/sudo nginx -tsudo systemctl reload nginxIf this fails, see Certbot failed to authenticate.
4. Check the caching
curl -I https://example.com/curl -I https://example.com/_next/static/chunks/main-abc123.jsThe second should have Cache-Control: public, immutable and be served by Nginx. Don’t apply this caching to / or to pages: they’re not hashed, and users would keep seeing old content after a deploy. Next.js sets its own headers there, as for pages using revalidate.
5. Things that need the real address
Server Actions check that the request’s origin matches the host. Without Host and X-Forwarded-Host, they fail with Invalid Server Actions request. With another proxy in front, set this in next.config.js:
module.exports = { experimental: { serverActions: { allowedOrigins: ['example.com'] } },};Authentication. NextAuth and similar libraries need to know the public address, so set AUTH_URL=https://example.com (or NEXTAUTH_URL on older versions) and AUTH_TRUST_HOST=true.
Streaming of responses, used with Suspense and server components, may arrive all at once, since Nginx buffers by default. For a route that needs real streaming, send the header X-Accel-Buffering: no from your code, or set proxy_buffering off; for that location.
When it goes wrong
| You see | Means |
|---|---|
upstream sent too big header in the error log, with a 502 |
Raise proxy_buffer_size, as in the config above |
No styles or images, with 404s on /_next/static/ |
The alias path is wrong, or .next/static wasn’t copied |
Old content after a deploy, or ChunkLoadError |
Old pages asking for files from a previous build: keep old static files for a while |
Image optimization errors |
sharp isn’t installed on this system: run npm ci on the server, not on your laptop |
502 Bad Gateway |
The app isn’t running: see Nginx 502 |
504 Gateway Time-out |
A slow server-rendered page: see Nginx 504 |
403 on /_next/static/ |
Nginx can’t read the folder: see Nginx 403 |
Warning
Variables named NEXT_PUBLIC_ are copied into the JavaScript at build time, so anyone can read them, and changing them on the server does nothing until you rebuild. Keep secrets in variables without that prefix.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.