Skip to content
ohsudo, Home
Networking

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.

Intermediate3 min readApplies to Nginx 1.18+ · Next.js 13+ · Node.js 20+ · Ubuntu · Debian

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

Terminal window
cd /var/www/myapp
npm ci
npm run build

Save this as /etc/systemd/system/myapp.service:

[Unit]
Description=Next.js app
After=network.target
[Service]
User=www-data
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/npx next start -H 127.0.0.1 -p 3000
Restart=on-failure
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
curl -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:

Terminal window
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/

Change the service to run it:

WorkingDirectory=/var/www/myapp/.next/standalone
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
ExecStart=/usr/bin/node server.js

Point 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

Terminal window
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 -t
sudo systemctl reload nginx

If this fails, see Certbot failed to authenticate.

4. Check the caching

Terminal window
curl -I https://example.com/
curl -I https://example.com/_next/static/chunks/main-abc123.js

The 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.

Tools

Try it yourself

Articles

Related articles