← Home/Blog/How to Deploy Next.js App Router to a Ubuntu VPS with Docker and Nginx
DevOps#docker#nginx#nextjs#vps

How to Deploy Next.js App Router to a Ubuntu VPS with Docker and Nginx

A step-by-step guide: multi-stage Dockerfile, Docker Compose, Nginx reverse proxy, free SSL with Certbot, and automated CI/CD with GitHub Actions.

CP

CHAEI PUEI Tech

12 min read

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

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:

📘typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

â„šī¸ After building, Next.js creates a .next/standalone directory with all the server code needed for production — no full node_modules required 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

đŸ’ģbash
node_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:

âš™ī¸yaml
services:
  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

đŸ’ģbash
sudo 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):

📄nginx
server {
    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

đŸ’ģbash
sudo 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:

âš™ī¸yaml
name: 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 .env files to Git. Create a .env.production file directly on the VPS and mount it via env_file in 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.

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.

Found this useful?

Subscribe to get the latest technical articles and reviews from CHAEI PUEI Tech.

Subscribe for free