Key Takeaways
output: 'standalone'innext.config.tsis the key to self-hosting — it produces a minimal.next/standalonedirectory that runs withoutnode_moduleson the server.- Three deployment options: Docker (recommended, portable), PM2 (simpler, Node.js ecosystem), or
next startdirectly (development/testing only). - Nginx in front: Next.js runs on port 3000; Nginx handles SSL, compression, and static file serving on port 443.
- Environment variables at build AND runtime:
NEXT_PUBLIC_*vars are baked into the client bundle at build time; server-only vars are read at runtime. Both need to be present in the right place.
Introduction
Direct Answer: How do I deploy Next.js 15 on my own server without Vercel in 2026?
Add output: 'standalone' to next.config.ts. Run npm run build. The build produces .next/standalone/ — a self-contained Node.js server. Copy .next/standalone/, public/, and .next/static/ to your server. Set environment variables, run node server.js, and your Next.js app is live. For production, wrap this in Docker: create a multi-stage Dockerfile with FROM node:22-alpine AS base, a builder stage that runs npm run build, and a runner stage that copies only the standalone output and runs node server.js. Put Nginx in front for HTTPS. Use Docker Compose to manage both services. The entire stack runs on a Hetzner CX22 (€4/month) for small to medium applications.
Part 1: Configure Next.js for Standalone Output
# Create or update next.config.ts
cat > next.config.ts << 'EOF'
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone", // ← Required for self-hosting
// Optional: disable Vercel-specific features
images: {
// Allow your domain for image optimisation
domains: ["yourdomain.com"],
// Or use unoptimized: true if you don't need next/image optimisation
},
};
export default nextConfig;
EOF
# Build
npm run build
ls -la .next/standalone/
Expected output:
drwxr-xr-x node_modules/
-rw-r--r-- package.json
-rw-r--r-- server.js ← The standalone server entry point
drwxr-xr-x .next/
# Test standalone locally
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/static
cd .next/standalone
node server.js
Expected output:
▲ Next.js 15.2.0
- Local: http://localhost:3000
- Network: http://0.0.0.0:3000
✓ Starting...
✓ Ready in 892ms
Part 2: Dockerfile for Production
# Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
# ── Dependency stage ───────────────────────────────────────────────────────
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --only=production
# ── Build stage ────────────────────────────────────────────────────────────
FROM base AS builder
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Build args for NEXT_PUBLIC_ variables (baked into client bundle)
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
# ── Production stage ───────────────────────────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
# Build and test the image
docker build -t myapp:latest \
--build-arg NEXT_PUBLIC_API_URL=https://api.myapp.com \
.
docker run -p 3000:3000 --rm myapp:latest &
sleep 3
curl -sI http://localhost:3000 | head -3
Expected output:
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
x-powered-by: Next.js
Part 3: Docker Compose with Nginx
mkdir -p ~/myapp && cd ~/myapp
cat > docker-compose.yml << 'EOF'
name: myapp
services:
nextjs:
image: myapp:latest
container_name: nextjs
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # Only accessible to Nginx
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=https://myapp.example.com
volumes:
- nextjs-cache:/app/.next/cache # Persist image optimisation cache
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
nginx:
image: nginx:1.27-alpine
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
nextjs:
condition: service_healthy
volumes:
nextjs-cache:
EOF
cat > .env << 'EOF'
DATABASE_URL=postgresql://user:pass@db:5432/myapp
NEXTAUTH_SECRET=generate_with_openssl_rand_base64_32
EOF
cat > nginx.conf << 'EOF'
server {
listen 80;
server_name myapp.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Serve Next.js static assets directly from Nginx (faster)
location /_next/static/ {
proxy_pass http://nextjs:3000;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy everything else to Next.js
location / {
proxy_pass http://nextjs:3000;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # WebSocket support
proxy_read_timeout 60s;
}
}
EOF
docker compose up -d
docker compose ps
Expected output:
NAME IMAGE STATUS
nginx nginx:1.27-alpine Up 5 seconds
nextjs myapp:latest Up 10 seconds (healthy)
Part 4: CI/CD — Auto-Deploy on Push
# .github/workflows/deploy.yml
name: Deploy Next.js
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} \
--build-arg NEXT_PUBLIC_API_URL=${{ vars.API_URL }} \
.
docker tag myapp:${{ github.sha }} myapp:latest
- name: Deploy to server
env:
PROD_HOST: ${{ secrets.PROD_HOST }}
PROD_USER: ${{ secrets.PROD_USER }}
SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
run: |
echo "$SSH_KEY" > /tmp/key && chmod 600 /tmp/key
# Export image and upload
docker save myapp:latest | gzip | \
ssh -i /tmp/key -o StrictHostKeyChecking=no \
"$PROD_USER@$PROD_HOST" \
"docker load && cd ~/myapp && docker compose up -d --no-deps nextjs"
Part 5: Environment Variables
# Variables in Next.js 15:
# NEXT_PUBLIC_* → exposed to browser (baked at build time)
# Everything else → server-only (read at runtime from process.env)
# Create .env.local for development
cat > .env.local << 'EOF'
# Public (visible in browser)
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_SITE_NAME=MyApp
# Server-only (never sent to client)
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
NEXTAUTH_SECRET=dev-secret-change-in-production
EOF
# Production: set via Docker Compose env_file or environment: section
# NEVER commit .env.local to Git
echo ".env.local" >> .gitignore
echo ".env*.local" >> .gitignore
Troubleshooting
ENOENT: no such file or directory, open '.next/BUILD_ID'
Cause: Standalone output wasn’t copied correctly — missing .next/static/ or public/ alongside server.js.
Fix: Ensure the Dockerfile COPY commands include all three: standalone/, .next/static/, and public/.
Images not loading — /_next/image returns 404
Cause: next/image optimisation needs write access to .next/cache. In Docker, this directory gets recreated empty on each deploy.
Fix: Mount a named volume at /app/.next/cache in docker-compose.yml (already done in the example above).
Server Actions fail in production
Cause: Missing environment variables that Server Actions depend on (DB credentials, API keys).
Fix: Ensure all server-side env vars are in the Docker Compose environment: section or .env file. NEXT_PUBLIC_* vars must be passed as --build-arg at build time AND set at runtime.
Conclusion
Next.js 15 is deployed sovereignly: Docker Compose manages the containerised app, Nginx handles SSL termination and static asset serving, and GitHub Actions automates deployment on every push to main. No Vercel account, no vendor lock-in, ~€4/month on Hetzner.
See React and Vite 2026 for the simpler SPA alternative when you don’t need SSR, and Docker Compose Tutorial 2026 for the Docker patterns used throughout this guide.
People Also Ask
Do I need Vercel to run Next.js in production?
No. Vercel is one hosting option for Next.js, not a requirement. Next.js is open-source and runs on any infrastructure that supports Node.js — your own VPS, AWS EC2, Google Cloud Run, or any Docker host. The output: 'standalone' build mode produces a minimal self-contained server. Vercel’s advantages are zero-config deployment, edge caching, and auto-scaling. The trade-off is vendor lock-in, per-seat pricing, and sending your application code and traffic through Vercel’s infrastructure.
What’s the difference between next start and the standalone output?
next start starts the Next.js server from the full project directory — it requires node_modules (hundreds of MB) and the full .next/ build output. The standalone output (output: 'standalone') produces a minimal directory that includes only the code actually needed at runtime — typically 50–80MB versus 300–500MB for a full installation. Use standalone output for Docker images and production deployments; use next start for development and simple VPS deployments where disk space isn’t a concern.
Part 4: Build-Time vs Runtime Environment Variables
Understanding where environment variables are evaluated is critical for secure self-hosting. Next.js has two categories:
NEXT_PUBLIC_*variables are embedded into the client bundle at build time.- non-public variables are only available to server-side code at runtime.
For a self-hosted deployment, this means the Docker build stage can bake client-side variables into the static assets, but runtime secrets like database credentials must be supplied when the container runs.
4.1 Best practice for build-time variables
Use build-time variables only for configuration that does not expose secrets.
NEXT_PUBLIC_API_URL=https://api.myapp.example.com npm run build
If you set a secret in NEXT_PUBLIC_, it becomes visible to anyone who can inspect the browser bundle. Avoid that.
4.2 Runtime secrets in Docker Compose
In your docker-compose.yml, provide runtime-only secrets through environment variables or an external secrets file.
services:
nextjs:
environment:
- DATABASE_URL=${DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
Keep .env out of source control. For local production, store the file on the server only.
4.3 Secret management for self-hosted apps
For more security, mount a file containing secrets instead of injecting them directly into the compose environment.
services:
nextjs:
env_file:
- .env.production
This pattern is simple and works well for single-node self-hosted stacks.
Part 5: Nginx Reverse Proxy Security and Performance
Nginx is the best practice edge layer for a self-hosted Next.js deployment. It handles SSL, compression, caching headers, and request buffering.
5.1 Strong SSL defaults
Use the latest secure TLS settings. Avoid legacy ciphers and prefer TLS 1.3.
ssl_protocols TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
5.2 HSTS and redirection
Force HTTPS and improve security with HSTS.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Redirect all HTTP traffic to HTTPS:
server {
listen 80;
server_name myapp.example.com;
return 301 https://$host$request_uri;
}
5.3 Static asset handling
Let Nginx serve static assets and cache them aggressively.
location /_next/static/ {
alias /usr/share/nginx/html/_next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
If you keep .next/static in the container, mount it as a volume so image optimisation remains persistent.
5.4 Proxying dynamic requests
Proxy all other traffic to the local Next.js container.
location / {
proxy_pass http://nextjs: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_buffering off;
}
Using proxy_buffering off can reduce latency for streaming and Server Action responses.
Part 6: TLS Certificates with Certbot
A self-hosted app still benefits from automatic certificate renewal. Use Certbot on the host or inside a dedicated container.
6.1 Certbot with Docker
If you prefer containerised certificate management, use the official Certbot image.
docker run -it --rm -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt -v /var/www/certbot:/var/www/certbot certbot/certbot certonly --webroot -w /var/www/certbot -d myapp.example.com
Then reload Nginx:
docker exec nginx_container nginx -s reload
6.2 Renewal automation
Create a cron job or system timer to renew certificates and reload Nginx.
0 3 * * * docker run --rm -v /etc/letsencrypt:/etc/letsencrypt -v /var/lib/letsencrypt:/var/lib/letsencrypt -v /var/www/certbot:/var/www/certbot certbot/certbot renew --quiet && docker exec nginx_container nginx -s reload
If your server is air-gapped, use a private CA or self-signed certificate with your own trust bundle.
Part 7: Health Checks and Service Readiness
A production-ready self-hosted stack includes health checks for both Next.js and Nginx.
7.1 Application health endpoint
Add a lightweight API route such as /api/health or /health that returns a minimal JSON payload.
export default function handler(req, res) {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}
This route should not require authentication and should be fast.
7.2 Docker healthcheck
Use the health endpoint in your docker-compose.yml.
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 20s
timeout: 10s
retries: 3
start_period: 15s
If the container becomes unhealthy, Docker can restart it automatically.
7.3 Nginx readiness probe
Nginx should also start only after the app is healthy. Use depends_on with condition: service_healthy for the proxy service.
This ensures the reverse proxy does not forward traffic to an unready backend.
Part 8: Logging, Observability, and Local Audit Trails
Self-hosting means the logs are yours. Capture them in a structured way and keep them accessible for troubleshooting.
8.1 Next.js application logs
In production, send stderr and stdout to the container runtime. The Next.js server prints startup information and request logs.
For more structured logs, use a small logger such as pino or winston in your custom server.
8.2 Nginx access and error logs
Configure Nginx to log requests and errors.
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
Mount /var/log/nginx as a volume and rotate logs using logrotate on the host.
8.3 Log rotation and retention
Rotate logs daily and keep enough history to investigate incidents.
Example /etc/logrotate.d/nginx configuration:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
On a self-hosted server, 14 to 30 days of logs is usually sufficient unless you have compliance requirements.
Part 9: Database and External Service Connectivity
A self-hosted frontend is only as robust as its external dependencies. Secure and monitor the backend services your Next.js app depends on.
9.1 Connection strings and pool sizing
Use a single trusted database connection string, and configure pooling in your application.
If you use Prisma:
DATABASE_URL=postgresql://user:pass@db:5432/myapp?schema=public&connection_limit=10
Keep connection limits low on a small host.
9.2 Local service discovery
If the database is on the same host or private network, use a stable DNS name such as db.local or a Docker network alias. Avoid hard-coded IP addresses.
9.3 Resilience for remote services
Use retries for transient failures and circuit breakers if a remote service is unstable.
A simple retry helper can make api calls more tolerant of short outages.
Part 10: CI/CD Pipeline for Self-Hosted Deployments
Automate building and pushing Docker images so your self-hosted app is easy to update.
10.1 Example GitHub Actions workflow
Create .github/workflows/deploy.yml with a build-and-push step.
name: Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version: '22'
- run: npm ci
- run: npm run build
- run: docker build -t myapp:${{ github.sha }} .
- run: docker tag myapp:${{ github.sha }} myregistry.example.com/myapp:latest
- run: docker push myregistry.example.com/myapp:latest
This workflow builds the production image and pushes it to a registry. Your self-hosted server can then pull the image to deploy.
10.2 Deployment strategy
For a minimal update process, use a simple docker-compose pull && docker-compose up -d command on the server. That keeps deployment atomic and repeatable.
If you use an image registry behind a VPN or private network, the self-hosted server can keep credentials secure.
Part 11: Rollback and Release Management
A good self-hosted release process includes a rollback path.
11.1 Tagging images
Tag production images explicitly, for example myapp:2026-05-22-1 and myapp:stable.
Your deployment script should be able to switch back to the previous tag quickly if a release fails.
11.2 Health-based rollback
After deploying, run smoke checks against the new version. If the app fails startup or the health endpoint returns an error, roll back immediately.
A shell script example:
docker-compose pull
if docker-compose up -d; then
sleep 10
if curl -f http://localhost:3000/api/health; then
echo 'Deploy successful'
else
echo 'Health failed, rolling back'
docker-compose down
docker-compose up -d myapp:previous
fi
fi
This pattern reduces the time to recover from bad releases.
Part 12: Container Security and Minimal Images
Security starts with a minimal runtime image. The standalone output is already small, but you can further reduce risk by using a lightweight base image and a non-root user.
12.1 Non-root container user
Your production image should not run as root.
RUN addgroup --system nextjs && adduser --system --ingroup nextjs nextjs
USER nextjs
12.2 Read-only filesystem where possible
Make the container filesystem immutable except for required cache paths.
services:
nextjs:
read_only: true
tmpfs:
- /tmp
- /app/.next/cache
This limits the damage if an attacker gains execution inside the container.
12.3 Docker image scanning
Scan your image for vulnerabilities as part of CI. Tools such as trivy or grype can catch outdated packages before deployment.
trivy image myapp:latest
A self-hosted stack still benefits from the same supply chain checks as cloud deployments.
Part 13: Optional PM2 Deployment Without Docker
If you prefer a lighter setup, you can self-host Next.js directly on the server with PM2 instead of Docker.
13.1 Install PM2 globally
npm install -g pm2
13.2 Launch the standalone server
cd /var/www/myapp
pm install --production
pm run build
cd .next/standalone
pm2 start server.js --name myapp
pm2 save
pm2 startup
PM2 provides process supervision, log management, and restart behavior. It is not as portable as Docker, but it is a valid self-hosted option for smaller teams.
13.3 Environment variable management with PM2
Use a process file to store runtime variables securely.
{
"apps": [{
"name": "myapp",
"script": "server.js",
"env_production": {
"NODE_ENV": "production",
"DATABASE_URL": "postgresql://...",
"NEXTAUTH_SECRET": "..."
}
}]
}
Keep the process file on the server only, and do not check secret values into Git.
Part 14: Caching, Compression, and Asset Optimization
A self-hosted Next.js app should make the most of caching and compression.
14.1 gzip and brotli compression
Compress HTML, CSS, JS, and JSON on the proxy layer.
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 256;
If you need even better compression, use brotil modules or pre-compress assets during the build.
14.2 Image optimisation
If you use next/image, mount .next/cache as a persistent volume so optimised images are reused after restarts.
volumes:
nextjs-cache:
On a self-hosted host with limited disk, monitor the cache with du -sh /path/to/.next/cache and prune if necessary.
14.3 HTTP caching headers
Serve immutable assets with far-future cache headers and use a cache-busting strategy for updates.
location ~* \.(?:js|css|png|jpg|jpeg|svg|gif|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
For HTML pages, use shorter caching periods or disable caching entirely if the content changes frequently.
Part 15: Performance Tuning and Resource Management
Monitor CPU, memory, and request latency on the host.
15.1 Node.js memory limits
If your container runs out of memory, set a Node.js memory cap.
command: ["node", "--max-old-space-size=1024", "server.js"]
This keeps the Node process from consuming the whole host.
15.2 Scaling on a single host
If traffic grows, scale with multiple containers behind a load balancer or use a larger VPS. On a single host, a second Next.js container is usually enough for modest load.
services:
nextjs:
deploy:
replicas: 2
For most self-hosted applications, the combination of Docker Compose and Nginx is enough to support moderate production traffic.
Part 16: Staging and Local Development Parity
Keep a staging environment that mirrors production. For self-hosting, a second VPS or a dedicated staging site is the easiest approach.
16.1 Staging deployment pattern
Use the same Docker Compose files and Nginx config in staging as in production. Replace only the domain name and secrets.
This helps catch configuration differences before they reach the live site.
16.2 Local development environment
Use Docker Compose or npx next dev for local work. Prefer a local .env.local file for development-only variables.
A good local setup includes the same environment variable names as production, so bugs caused by missing config are easier to reproduce.
Part 17: Disaster Recovery and Backups
A self-hosted deployment must include backup and recovery procedures.
17.1 Code and configuration backups
Back up your repository, Docker Compose files, Nginx configuration, and environment files to a secure location. If you use Git, store only non-secret configuration in the repo and back up secrets separately.
17.2 Backup verification
Periodically restore the backup to a test host. Verify that the container starts, the app builds, and the site responds correctly.
This makes recovery predictable rather than a gamble.
Part 18: Troubleshooting Common Self-Hosted Issues
A few issues recur often in self-hosted Next.js deployments.
18.1 502 Bad Gateway from Nginx
This usually means the Next.js container is not running or the health endpoint failed. Check the nextjs container logs and confirm it is listening on port 3000.
18.2 Missing environment variable
If the app crashes at startup with a missing config error, verify the runtime environment variables in docker-compose.yml or your PM2 process file.
18.3 Image optimisation errors
If next/image fails, check that /app/.next/cache is writable by the running user and persisted across restarts.
18.4 Memory-related crashes
On small VPS hosts, limit Node.js memory, use fewer worker threads, or switch to a smaller build target. Avoid bundling huge assets into the server.
Part 19: Final Production Checklist
-
output: 'standalone'is enabled innext.config.ts - build and runtime env vars are separated correctly
- Docker image is built from a minimal runtime stage
- Nginx handles SSL, caching, and reverse proxying
- certificates renew automatically and reload Nginx
- application and proxy health checks are configured
- logs are collected, rotated, and retained
- runtime secrets are not baked into the client bundle
- rolling back to a previous image is documented
- backups and restore testing are performed regularly
- monitoring alerts are in place for errors and high latency
- static assets have long cache lifetimes and immutable headers
- the production stack is reproducible from code/config
- the app can be deployed from CI/CD with a single command
- the server is hardened and does not run containers as root
Part 20: Why Self-Hosting Still Matters in 2026
Self-hosting Next.js in 2026 is not about rejecting cloud platforms entirely; it is about choosing control and predictability. When you own the deployment environment, you can enforce security policies, manage secrets locally, and tune the stack to your exact requirements.
For teams that want the power of Next.js without Vercel lock-in, this guide shows the practical path: build standalone output, package it in a minimal Docker image, let Nginx terminate TLS, and run the stack with health checks, logging, and deployment automation. That combination is the essence of a sovereign frontend deployment.
Further Reading
- React and Vite 2026 — simpler frontend alternative for SPAs
- Docker Compose Tutorial 2026 — the deployment pattern used in this guide
- Nginx Reverse Proxy Tutorial 2026 — Nginx configuration for the proxy layer
- Node.js vs Bun vs Deno 2026 — choose the right runtime for your Next.js deployment
Tested on: Ubuntu 24.04 LTS (Hetzner CX22). Next.js 15.2.0, Node.js 22.11.0, Docker CE 27.3.1. Last verified: April 28, 2026.