After building a Next.js App Router project, the next question is always: where and how do I deploy it? Vercel is the easiest option, but if you need more control, want to self-host, or simply want to cut costs, a VPS running Docker and Nginx is an incredibly capable stack.
This guide walks you through the whole process: configuring Next.js standalone output, writing an optimized multi-stage Dockerfile, installing Docker on Ubuntu, setting up Nginx as a reverse proxy, and enabling free SSL with Certbot. There's also a bonus section on automated CI/CD with GitHub Actions.
Prerequisites
- Ubuntu 22.04+ VPS (DigitalOcean, Vultr, Vietnix...) â minimum 1 vCPU / 1 GB RAM
- A domain with its A record pointing to your VPS IP
- A Next.js 13+ project using App Router (Node.js 18+ on your local machine)
- An SSH key installed on the VPS
- Basic familiarity with the Linux terminal
Step 1: Enable standalone output in Next.js
Next.js has a standalone output mode that bundles everything needed to run the server into a compact directory â perfect for packaging inside a Docker image. Add this to next.config.ts:
đtypescriptimport type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", }; export default nextConfig;
âšī¸ After building, Next.js creates a
.next/standalonedirectory with all the server code needed for production â no fullnode_modulesrequired at runtime.
Step 2: Write a multi-stage Dockerfile
Multi-stage builds keep the production image as small as possible â only what's needed to run, no devDependencies or build tools:
đŗdockerfile# Stage 1: Install dependencies FROM node:20-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # Stage 2: Build the project FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # Stage 3: Production image (minimal) FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 # Create a non-root user for security 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 CMD ["node", "server.js"]
Step 3: Create a .dockerignore
đģbashnode_modules .next .git .env*.local *.md .DS_Store Dockerfile .dockerignore
Step 4: Write a Docker Compose file
Docker Compose makes container management much easier, especially when you later want to add a database, Redis, and so on:
âī¸yamlservices: app: build: context: . dockerfile: Dockerfile ports: - "3000:3000" environment: - NODE_ENV=production restart: unless-stopped healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000"] interval: 30s timeout: 10s retries: 3
Step 5: Install Docker on your Ubuntu VPS
SSH into the VPS and run the following commands in order:
đģbash# Update packages and install dependencies sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg # Add the Docker GPG key sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Add the Docker repository echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Add your user to the docker group sudo usermod -aG docker $USER && newgrp docker # Verify docker --version && docker compose version
Step 6: Deploy to the VPS
đģbash# Clone the project onto the VPS git clone git@github.com:your-username/your-repo.git /var/www/myapp cd /var/www/myapp # Build the image and start the container in the background docker compose up -d --build # Follow logs docker compose logs -f app # Check running containers docker compose ps
đĄ The first build may take 3â5 minutes depending on your VPS speed. Subsequent builds will be faster thanks to Docker layer caching.
Step 7: Install and configure Nginx
đģbashsudo apt install -y nginx sudo systemctl enable nginx && sudo systemctl start nginx sudo nano /etc/nginx/sites-available/myapp
Config file contents (replace yourdomain.com with your actual domain):
đnginxserver { listen 80; server_name yourdomain.com www.yourdomain.com; client_max_body_size 50M; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; 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_cache_bypass $http_upgrade; } }
đģbash# Enable the config sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx
Step 8: Enable free HTTPS with Certbot
đģbashsudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Test auto-renewal sudo certbot renew --dry-run
âšī¸ Certbot automatically adds a cron job to renew the certificate before it expires after 90 days. Nothing else to do.
Bonus: Auto-deploy with GitHub Actions
Every push to the main branch triggers GitHub Actions to SSH into the VPS and rebuild:
âī¸yamlname: Deploy to VPS on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy via SSH uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | cd /var/www/myapp git pull origin main docker compose up -d --build --remove-orphans docker image prune -f
Add 3 secrets in your GitHub repo under Settings â Secrets â Actions: VPS_HOST, VPS_USER, VPS_SSH_KEY.
â ī¸ Never commit
.envfiles to Git. Create a.env.productionfile directly on the VPS and mount it viaenv_filein docker-compose.yml.
Summary
Once you're done, you have a complete deploy pipeline: push code â GitHub Actions SSHes into the VPS â pulls â rebuilds Docker â Nginx serves over HTTPS.
- â Next.js in a Docker container â isolated, reproducible, easy to roll back
- â Nginx reverse proxy â supports multiple domains, static file caching
- â Free SSL with Let's Encrypt â auto-renews
- â Automated CI/CD with GitHub Actions â zero-downtime deploys
If your project grows, the natural next steps are adding PostgreSQL + Docker, Redis, or migrating to Kubernetes. But for most side projects and early-stage startups, this stack is more than enough.
Related Articles
What Is an Unmanaged VPS? Pros, Cons, and How It Compares to Managed VPS
An unmanaged VPS gives you full control over your server â but full responsibility too. This article breaks down the differences, walks through SSH hardening, UFW, and Fail2Ban setup, and helps you pick the right option.
9 min read â
ProductivityHow I Build a Landing Page from Zero to Live in One Day
The real stack I use to ship landing pages fast: Next.js + shadcn/ui + Vercel. From setup to deploy, with a list of UI tools and hosting options I actually trust.
8 min read â
Found this useful?
Subscribe to get the latest technical articles and reviews from CHAEI PUEI Tech.
Subscribe for free