DEPLOY, WITHOUT THE GUESSWORK

How to Deploy Next.js to a VPS (Docker + HTTPS)

A repeatable Next.js VPS deploy: Docker image, reverse proxy, HTTPS, and a clean update workflow you can reuse for every app.

How to Deploy Next.js to a VPS (Docker + HTTPS)

You are going to deploy Next.js to a VPS in a way that is boring and repeatable:

If you want "deploy now and worry later", use a PaaS. If you want to pay VPS pricing and still ship fast, this is the path.

What you will have at the end

Step 1: Update the server and install Docker

On the VPS:

sudo apt update && sudo apt upgrade -y

Install Docker:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Install the Compose plugin (Ubuntu usually has it, but confirm):

docker compose version

Step 2: Create the app directory

mkdir -p ~/apps/nextjs-web
cd ~/apps/nextjs-web

You want each app isolated in its own directory. That makes backups, debugging, and migrations obvious.

Step 3: Add a production Dockerfile (simple and stable)

Create Dockerfile:

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
RUN npm ci || npm install

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
EXPOSE 3000
CMD [\"npm\", \"start\"]

Notes:

Step 4: Add a reverse proxy (HTTPS) you can reuse

You can do this with Nginx or Traefik. I recommend Traefik when you want to host multiple apps later, because the routing rules live in labels and you avoid hand-editing Nginx blocks forever.

Create docker-compose.yml:

services:
  traefik:
    image: traefik:v2.11
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - [email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    restart: unless-stopped

  nextjs:
    build: .
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.nextjs.rule=Host(`your-domain.com`)
      - traefik.http.routers.nextjs.entrypoints=websecure
      - traefik.http.routers.nextjs.tls=true
      - traefik.http.routers.nextjs.tls.certresolver=letsencrypt
      - traefik.http.services.nextjs.loadbalancer.server.port=3000

volumes:
  letsencrypt:

Replace:

Step 5: Deploy

Copy your app code into this folder (git clone is fine), then run:

docker compose up -d --build

If DNS is correct, Traefik should issue a certificate and your app should come up.

Step 6: Updates (your repeatable deploy loop)

Your deploy loop should be boring:

git pull
docker compose up -d --build

That is it.

If you want safer deploys, add:

Troubleshooting (the errors you will actually hit)

"Certificate not issued" or HTTPS never works

docker logs -f $(docker ps --filter name=traefik -q)

"502 Bad Gateway"

This usually means your Next.js container is not listening on port 3000.

Deploys are slow or memory errors happen

Internal links (recommended next reads)

From across the StoicSoft network

Hand-curated reads on the same topic from sister sites in the StoicSoft family.