DEV Community

Cover image for Next.js Self-Hosting VPS Basic Setup
Rizqiyanto Imanullah
Rizqiyanto Imanullah

Posted on

Next.js Self-Hosting VPS Basic Setup

Prerequisites

  1. VPS server (Ubuntu/Debian) (Preferred 6gb memory)
  2. Domain name (optional)
  3. GitHub repository with Next.js project
  4. Basic terminal knowledge

Goals

  1. Auto deployment with Github Actions (single build)
  2. Custom domain with SSL/HTTPS encryption

Steps

1. Connect into your SSH server with root user

ssh root@<your_vps_ip_address>
Enter fullscreen mode Exit fullscreen mode
ssh-keygen -t rsa -b 4096 -f ~/.ssh/github_actions -N ""
Enter fullscreen mode Exit fullscreen mode
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

# you need to copy the content of this file for next step

cat ~/.ssh/github_actions
Enter fullscreen mode Exit fullscreen mode

2. Install packages, docker and project directory

Update system and install packages

apt update && apt upgrade -y
apt install -y curl wget git ufw
Enter fullscreen mode Exit fullscreen mode

Install Docker

curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
Enter fullscreen mode Exit fullscreen mode

Install Docker Compose

curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Enter fullscreen mode Exit fullscreen mode

Setup firewall

ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Enter fullscreen mode Exit fullscreen mode

Create project directory and clone repository

mkdir -p /var/www/<your_project_dir_name>
cd /var/www/<your_project_dir_name>

# Clone your repository (replace with your actual repository URL)
git clone https://github.com/yourusername/your-repo.git .
Enter fullscreen mode Exit fullscreen mode

Note: If you have a private repository, you'll need to set up SSH keys:

# Generate SSH key for GitHub access (as root)
ssh-keygen -t rsa -b 4096 -C "your-email@example.com" -f /root/.ssh/github_repo

# Display public key to add to GitHub
cat /root/.ssh/github_repo.pub

# Add to SSH config
echo "Host github.com
  HostName github.com
  User git
  IdentityFile /root/.ssh/github_repo" >> /root/.ssh/config

# Test connection
ssh -T git@github.com
Enter fullscreen mode Exit fullscreen mode

3. Setup Docker, Nginx, Github Actions in Next.js Project

Dockerfile

FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

USER nextjs

EXPOSE 3000
CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

version: '3.8'

services:
  nextjs-app:
    build: .
    container_name: <container_name>
    restart: unless-stopped
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
    volumes:
      - ./logs:/app/logs
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /var/www/certbot:/var/www/certbot:ro
    depends_on:
      - nextjs-app
    networks:
      - app-network

networks:
  app-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream nextjs {
        server nextjs-app:3000;
    }

    server {
        listen 80;
        server_name <your_domain_name>.com www.<your_domain_name>.com;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://$server_name$request_uri;
        }
    }

    server {
        listen 443 ssl http2;
        server_name <your_domain_name>.com www.<your_domain_name>.com;

        ssl_certificate /etc/letsencrypt/live/<your_domain_name>.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<your_domain_name>.com/privkey.pem;

        location / {
            proxy_pass http://nextjs;
            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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

.github/workflows/deploy.yml

name: Deploy to VPS

on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Deploy to VPS
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.PRIVATE_KEY }}
        script: |
          cd /var/www/<your_project_dir_name>
          git pull origin master
          docker-compose down
          docker-compose build --no-cache
          docker-compose up -d
          docker system prune -f
Enter fullscreen mode Exit fullscreen mode

Push into git

git add .
git commit -m "chore: setup deployment"
git push origin master
Enter fullscreen mode Exit fullscreen mode

4. Github Setup for Secrets

Go to GitHub Repository → Settings → Secrets and variables → Actions
Add these secrets:

  • HOST: Your VPS IP address
  • USERNAME: root
  • PRIVATE_KEY: Content from ~/.ssh/github_actions file on previous step

5. Domain and SSL Setup

Setup DNS Records

In your domain registrar, point your domain to your VPS IP:

Type: A Record
Name: @ (or leave blank)
Value: YOUR_VPS_IP

Type: A Record  
Name: www
Value: YOUR_VPS_IP
Enter fullscreen mode Exit fullscreen mode

Important: Wait for DNS propagation (5-30 minutes) before proceeding.

Phase 1: Initial HTTP-only Nginx Configuration

First, create a temporary HTTP-only nginx config to obtain SSL certificates:

# Create temporary HTTP-only config
sudo nano /var/www/<your_project_dir_name>/nginx-temp.conf
Enter fullscreen mode Exit fullscreen mode

Add this temporary configuration:

events {
    worker_connections 1024;
}

http {
    upstream nextjs {
        server nextjs-app:3000;
    }

    server {
        listen 80;
        server_name <your_domain_name>.com www.<your_domain_name>.com;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            proxy_pass http://nextjs;
            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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Update docker-compose.yml for temporary config

# Update the nginx volumes in docker-compose.yml temporarily
# Change this line:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# To:
# - ./nginx-temp.conf:/etc/nginx/nginx.conf:ro
Enter fullscreen mode Exit fullscreen mode

Start services with HTTP-only config

cd /var/www/<your_project_dir_name>
docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Install Certbot and obtain SSL certificates

# Install certbot
apt install certbot python3-certbot-nginx -y

# Create certbot directory
mkdir -p /var/www/certbot

# Stop nginx container temporarily
docker-compose stop nginx

# Get SSL certificates using standalone mode
certbot certonly --standalone \
  --preferred-challenges http \
  -d <your_domain_name>.com \
  -d www.<your_domain_name>.com

# Restart nginx container
docker-compose start nginx
Enter fullscreen mode Exit fullscreen mode

Phase 2: Update to HTTPS Configuration

Now update your nginx.conf file with the HTTPS configuration:

events {
    worker_connections 1024;
}

http {
    upstream nextjs {
        server nextjs-app:3000;
    }

    server {
        listen 80;
        server_name <your_domain_name>.com www.<your_domain_name>.com;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://$server_name$request_uri;
        }
    }

    server {
        listen 443 ssl http2;
        server_name <your_domain_name>.com www.<your_domain_name>.com;

        ssl_certificate /etc/letsencrypt/live/<your_domain_name>.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<your_domain_name>.com/privkey.pem;

        # SSL security settings
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;

        location / {
            proxy_pass http://nextjs;
            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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Update docker-compose.yml back to HTTPS config

# Revert the nginx volumes in docker-compose.yml
# Change back to:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
Enter fullscreen mode Exit fullscreen mode

Final deployment with HTTPS

# Pull latest changes
git pull origin master

# Restart with HTTPS configuration
docker-compose down
docker-compose build --no-cache
docker-compose up -d

# Clean up
rm nginx-temp.conf
docker system prune -f
Enter fullscreen mode Exit fullscreen mode

Setup automatic SSL renewal

# Add cron job for automatic renewal (as root)
echo "0 12 * * * /usr/bin/certbot renew --quiet && docker-compose restart nginx" | crontab -
Enter fullscreen mode Exit fullscreen mode

Test your deployment

# Test HTTP redirect
curl -I http://<your_domain_name>.com

# Test HTTPS
curl -I https://<your_domain_name>.com

# Check SSL certificate
openssl s_client -connect <your_domain_name>.com:443 -servername <your_domain_name>.com
Enter fullscreen mode Exit fullscreen mode

6. Troubleshooting Common Issues

Check container status

# View running containers
docker ps

# Check container logs
docker logs <container_name>
docker-compose logs -f
Enter fullscreen mode Exit fullscreen mode

Common fixes

# If containers won't start
docker-compose down
docker system prune -f
docker-compose up -d

# If SSL issues persist
sudo certbot certificates
sudo certbot renew --dry-run

# Check nginx configuration
docker exec nginx-proxy nginx -t

# Restart specific services
docker-compose restart nginx
docker-compose restart nextjs-app
Enter fullscreen mode Exit fullscreen mode

Verify deployment

  • ✅ HTTP redirects to HTTPS: curl -I http://yourdomain.com
  • ✅ HTTPS works: curl -I https://yourdomain.com
  • ✅ Containers running: docker ps
  • ✅ SSL certificate valid: Check browser or use SSL checker online

Achieved:

  1. Auto deployment with Github Actions (single build) ✅ (~5 mins process)
  2. Custom domain with SSL/HTTPS encryption ✅
  3. Secure server setup with firewall ✅
  4. Automatic SSL certificate renewal ✅

Top comments (0)