Skip to content
·12 min read·DevOps

Deploying Next.js on Oracle's 'Always Free' VPS in 2026

A battle-tested guide to hosting your portfolio on Oracle Cloud. From handling OOM kills to configuring Nginx, SSL, and GitHub Actions automation.

Why Self-Host?

I recently moved ishowon.com from Vercel to a self-hosted VPS on Oracle Cloud. Why? Because I wanted full control, a dedicated IP for better SEO, and 0$ cost without hitting bandwidth limits for simple projects.

Oracle's "Always Free" tier offers a VM.Standard.E2.1.Micro instance. It’s modest—1 OCPU and 1GB RAM—but with the right tuning, it screams.

Here is the exact roadmap I used to deploy this site, including fixes for every error I hit along the way.

1. The Setup (Oracle Cloud)

Sign up at cloud.oracle.com. Once you're in, create a compute instance:

  • Image: Oracle Linux 9 (or Ubuntu, but this guide assumes Oracle Linux).
  • Shape: VM.Standard.E2.1.Micro (Always Free).
  • Networking: Create a VCN and ensure you assign a public IP.
  • SSH Keys: Download your private key (.key) securely.

The Security List (Firewall Layer 1)

Before you even SSH in, go to your Virtual Cloud Network (VCN) > Security Lists > Ingress Rules. Add these rules to allow traffic:

  • Unit: TCP, Port: 80 (HTTP), Source: 0.0.0.0/0
  • Unit: TCP, Port: 443 (HTTPS), Source: 0.0.0.0/0
  • Unit: TCP, Port: 22 (SSH), Source: 0.0.0.0/0

2. Server Tuning: The Swap File

The 1GB RAM is the bottleneck. If you try to run pnpm build or npm install raw, your process will get "Killed" (OOM). You must add Swap memory.

SSH into your server (ssh -i key.key opc@IP) and run:

# Check existing memory
free -h

# Create a 4GB swap file (lifesaver)
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# Make it permanent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

3. The Stack: Node.js, PM2, and Nginx

Oracle Linux uses dnf instead of apt.

# Install Git and Nginx
sudo dnf install git nginx -y

# Enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx

# Install fnm (Fast Node Manager)
curl -fsSL https://fnm.vercel.app/install | bash
# (Restart terminal)
fnm install 20
fnm use 20

# Install global tools
npm install -g pnpm pm2

4. The Firewalld (Firewall Layer 2)

Oracle Linux has its own internal firewall. You opened the Ports in the cloud panel, now open them in the OS:

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

5. Deployment

Clone your repo efficiently. I recommend cloning into /var/www/portfolio:

sudo mkdir -p /var/www/portfolio
sudo chown -R opc:opc /var/www/portfolio
git clone https://github.com/your-username/your-repo.git /var/www/portfolio
cd /var/www/portfolio

The Build

This is where 90% of deployments fail. Use the extra memory flag:

pnpm install
# Prevent OOM during build
NODE_OPTIONS="--max-old-space-size=512" pnpm build

Start with PM2

pm2 start "pnpm start" --name "portfolio"
pm2 save
pm2 startup
# (Run the command output by startup)

6. Nginx & SSL (The "502 Bad Gateway" Fix)

We use acme.sh for SSL because it's lighter and more reliable on Enterprise Linux than Certbot.

  1. Install acme.sh: curl https://get.acme.sh | sh
  2. Issue Cert: ~/.acme.sh/acme.sh --issue -d yourdomain.com -w /var/www/portfolio/public --server letsencrypt
  3. Install Cert:
    mkdir -p /etc/nginx/ssl
    ~/.acme.sh/acme.sh --install-cert -d yourdomain.com \
    --key-file       /etc/nginx/ssl/key.pem  \
    --fullchain-file /etc/nginx/ssl/cert.pem \
    --reloadcmd     "service nginx force-reload"
    

The Nginx Config

Create /etc/nginx/conf.d/portfolio.conf:

server {
    listen 80;
    server_name ishowon.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name ishowon.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        proxy_pass http://127.0.0.1:3000; # The port PM2 runs on
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

The Hidden Boss: SELinux

If you get a 502 Bad Gateway even though PM2 is running, it's SELinux blocking Nginx from talking to the network.

The Fix:

sudo setsebool -P httpd_can_network_connect 1

7. Automation: GitHub Actions

Manual builds are boring. Let's auto-deploy on git push.

1. On VPS: Create deploy.sh inside /var/www/portfolio:

#!/bin/bash
# Load Node path (modify version/path as needed based on `which node`)
export PATH="/root/.local/share/fnm/installation/bin:$PATH"

cd /var/www/portfolio
git pull origin main
# Build with memory limit
NODE_OPTIONS="--max-old-space-size=512" pnpm build
# Restart
pm2 restart portfolio

Make it executable: chmod +x deploy.sh

2. On GitHub: Add your SSH Private Key to Settings > Secrets > Actions > SSH_PRIVATE_KEY.

3. The Workflow: Create .github/workflows/deploy.yml:

name: Deploy Portfolio
on:
  push:
    branches: [ main ]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Remote Deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }} # Or hardcode IP if you prefer
          username: opc
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/portfolio
            sudo ./deploy.sh

Now, every commit automagically updates your live site.