Deploy Next.js Supabase to a VPS
Deploy your MakerKit Next.js Supabase application to a VPS like Digital Ocean, Hetzner, or Linode. Covers server setup, Docker deployment, Nginx, and SSL configuration.
Deploy your MakerKit Next.js 16 application to a Virtual Private Server (VPS) for full infrastructure control and predictable costs. This guide covers Ubuntu server setup, Docker deployment, Nginx reverse proxy, and Let's Encrypt SSL. The steps work with Digital Ocean, Hetzner, Linode, Vultr, and other VPS providers.
Overview
| Step | Purpose |
|---|---|
| Create VPS | Provision your server |
| Install dependencies | Docker, Node.js, Nginx |
| Deploy application | Using Docker or direct build |
| Configure Nginx | Reverse proxy and SSL |
| Set up SSL | HTTPS with Let's Encrypt |
Prerequisites
Before starting:
- Set up Supabase project
- Generate environment variables
- Domain name pointing to your VPS IP address
Step 1: Create Your VPS
Digital Ocean
- Go to Digital Ocean
- Click Create Droplet
- Choose:
- OS: Ubuntu 24.04 LTS
- Plan: Basic ($12/month minimum recommended for building)
- Region: Close to your users and Supabase instance
- Add your SSH key for secure access
- Create the Droplet
Recommended Specifications
| Use Case | RAM | CPU | Storage |
|---|---|---|---|
| Building on VPS | 4GB+ | 2 vCPU | 50GB |
| Running only (pre-built image) | 2GB | 1 vCPU | 25GB |
| Production with traffic | 4GB+ | 2 vCPU | 50GB |
Step 2: Initial Server Setup
SSH into your server:
ssh root@your-server-ipUpdate System
apt update && apt upgrade -yInstall Docker
Follow the official Docker installation guide or run:
# Add Docker's official GPG keycurl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg# Set up repositoryecho "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null# Install Dockerapt updateapt install -y docker-ce docker-ce-cli containerd.io docker-compose-pluginConfigure Firewall
# Allow SSHufw allow 22# Allow HTTP and HTTPSufw allow 80ufw allow 443# Allow app port (if not using Nginx)ufw allow 3000# Enable firewallufw enableStep 3: Deploy Your Application
Choose one of two approaches:
Option A: Pull Pre-Built Docker Image (Recommended)
If you built and pushed your image to a container registry (see Docker guide):
# Login to registrydocker login ghcr.io# Pull your imagedocker pull ghcr.io/YOUR_USERNAME/myapp:latest# Create env filenano .env.production.local# Paste your environment variables# Run containerdocker run -d \ -p 3000:3000 \ --env-file .env.production.local \ --name myapp \ --restart unless-stopped \ ghcr.io/YOUR_USERNAME/myapp:latestOption B: Build on VPS
For VPS with enough resources (4GB+ RAM):
Install Node.js and pnpm
# Install nvm (check https://github.com/nvm-sh/nvm for latest version)curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash# Load nvmexport NVM_DIR="$HOME/.nvm"[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"# Install Node.js (LTS version)nvm install --lts# Install pnpmnpm install -g pnpmClone and Build
# Create Personal Access Token on GitHub with repo access# Clone repositorygit clone https://<YOUR_GITHUB_PAT>@github.com/YOUR_USERNAME/your-repo.gitcd your-repo# Install dependenciespnpm install# Generate Dockerfilepnpm run turbo gen docker# Create env filecp turbo/generators/templates/env/.env.local apps/web/.env.production.localnano apps/web/.env.production.local# Edit with your production values# Build Docker imagedocker build -t myapp:latest .# Run containerdocker run -d \ -p 3000:3000 \ --env-file apps/web/.env.production.local \ --name myapp \ --restart unless-stopped \ myapp:latestMemory during build
If the build fails with memory errors, increase your VPS size temporarily or build locally and push to a registry.
Step 4: Configure Nginx
Install Nginx as a reverse proxy:
apt install -y nginxCreate Nginx Configuration
nano /etc/nginx/sites-available/myappAdd:
server { listen 80; server_name yourdomain.com www.yourdomain.com; 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; proxy_read_timeout 86400; }}Enable the Site
# Create symlinkln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/# Remove default siterm /etc/nginx/sites-enabled/default# Test configurationnginx -t# Restart Nginxsystemctl restart nginxStep 5: Set Up SSL with Let's Encrypt
Install Certbot:
apt install -y certbot python3-certbot-nginxObtain SSL certificate:
certbot --nginx -d yourdomain.com -d www.yourdomain.comCertbot automatically:
- Obtains the certificate
- Updates Nginx configuration
- Sets up auto-renewal
Verify auto-renewal:
certbot renew --dry-runStep 6: Post-Deployment Configuration
Update Supabase URLs
In Supabase Dashboard (Authentication > URL Configuration):
| Field | Value |
|---|---|
| Site URL | https://yourdomain.com |
| Redirect URLs | https://yourdomain.com/auth/callback** |
Configure Webhooks
Point your webhooks to your new domain:
- Supabase DB webhook:
https://yourdomain.com/api/db/webhook - Stripe webhook:
https://yourdomain.com/api/billing/webhook - Lemon Squeezy webhook:
https://yourdomain.com/api/billing/webhook
Monitoring and Maintenance
View Logs
# Docker logsdocker logs -f myapp# Nginx access logstail -f /var/log/nginx/access.log# Nginx error logstail -f /var/log/nginx/error.logRestart Application
docker restart myappUpdate Application
# Pull new imagedocker pull ghcr.io/YOUR_USERNAME/myapp:latest# Stop old containerdocker stop myappdocker rm myapp# Start new containerdocker run -d \ -p 3000:3000 \ --env-file .env.production.local \ --name myapp \ --restart unless-stopped \ ghcr.io/YOUR_USERNAME/myapp:latestAutomated Updates with Watchtower (Optional)
Auto-update containers when new images are pushed:
docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ containrrr/watchtower \ --interval 300 \ myappTroubleshooting
Application not accessible
- Check Docker container is running:
docker ps - Check firewall allows port 3000:
ufw status - Check Nginx is running:
systemctl status nginx - Check Nginx config:
nginx -t
SSL certificate issues
- Ensure DNS is properly configured
- Wait for DNS propagation (up to 48 hours)
- Check Certbot logs:
cat /var/log/letsencrypt/letsencrypt.log
Container keeps restarting
Check logs for errors:
docker logs myappCommon causes:
- Missing environment variables
- Database connection issues
- Port conflicts
High memory usage
Monitor with:
docker statsConsider:
- Increasing VPS size
- Configuring memory limits on container
- Enabling swap space
Cost Comparison
| Provider | Basic VPS | Notes |
|---|---|---|
| Digital Ocean | $12/month | Good documentation |
| Hetzner | $4/month | Best value, EU-based |
| Linode | $12/month | Owned by Akamai |
| Vultr | $12/month | Good global coverage |
Frequently Asked Questions
Which VPS provider should I choose?
How much RAM do I need?
Do I need Nginx if I'm using Docker?
Is VPS cheaper than Vercel?
Next Steps
- Docker Deployment: Build and push Docker images with CI/CD
- Monitoring Setup: Add Sentry or PostHog for error tracking
- Environment Variables: Complete variable reference