ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

VPS Deployment Guide

This guide covers deploying IgnitionStack to a VPS (Virtual Private Server) instead of Vercel. Perfect for cost optimization at scale or when you need full infrastructure control.

Why Deploy on VPS?

BenefitVercelVPS
Cost at Scale$20+/month (Pro)$5-15/month
BandwidthLimitedUnlimited
CPU/RAMSharedDedicated
ControlLimitedFull
Custom Domain SSLIncludedDIY (free with Caddy)
Deploy ComplexitySimpleModerate

Provider Comparison

ProviderBest ForMin PriceDatacenters
HostingerBeginners$4.99/moUSA, EU, Asia
HetznerPrice/Performance€4.15/moEU, USA
DigitalOceanDeveloper Experience$6/moGlobal
VultrHigh Performance$6/moGlobal
ContaboMaximum Resources€5.99/moEU, USA
LinodeReliability$5/moGlobal
Traffic LevelRAMvCPUStorageEstimated Cost
Starter (< 10k visits/mo)2GB150GB$5-8/mo
Growth (< 100k visits/mo)4GB280GB$12-20/mo
Scale (< 500k visits/mo)8GB4160GB$24-40/mo

Prerequisites

Step 1: Initial Server Setup

# Connect to your VPS ssh root@your-server-ip # Update system apt update && apt upgrade -y # Install essential tools apt install -y curl git ufw # Create non-root user (security best practice) adduser deployer usermod -aG sudo deployer

Step 2: Install Docker

# Install Docker curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh # Add user to docker group usermod -aG docker deployer # Install Docker Compose apt install -y docker-compose-plugin # Verify docker --version docker compose version

Step 3: Configure Firewall

ufw allow OpenSSH ufw allow 80 ufw allow 443 ufw enable

Step 4: Clone and Configure Project

# Switch to deployer user su - deployer # Clone your repository git clone https://github.com/your-username/ignitionstack.git cd ignitionstack # Create production env file cp .env.example .env.production nano .env.production

Edit your environment variables:

# .env.production NODE_ENV=production # Supabase NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # App NEXT_PUBLIC_BASE_URL=https://your-domain.com # Stripe STRIPE_SECRET_KEY=sk_live_xxx NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx STRIPE_WEBHOOK_SECRET=whsec_xxx # Email (Resend) RESEND_API_KEY=re_xxx # AI (if using) OPENAI_API_KEY=sk-xxx GOOGLE_GENERATIVE_AI_API_KEY=xxx

Step 5: Create Dockerfile

# Dockerfile FROM node:20-alpine AS base # Install dependencies only when needed FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED 1 RUN npm run build # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" CMD ["node", "server.js"]

Step 6: Create Docker Compose

# docker-compose.yml version: '3.8' services: app: build: context: . dockerfile: Dockerfile restart: always ports: - "3000:3000" env_file: - .env.production healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s caddy: image: caddy:2-alpine restart: always ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config depends_on: - app volumes: caddy_data: caddy_config:

Step 7: Create Caddyfile

# Caddyfile your-domain.com { reverse_proxy app:3000 encode gzip header { # Security headers Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" } } www.your-domain.com { redir https://your-domain.com{uri} permanent }

Step 8: Update next.config.mjs

// next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', // ... rest of your config }; export default nextConfig;

Step 9: Deploy

# Build and start docker compose up -d --build # Check status docker compose ps # View logs docker compose logs -f app # Check health curl http://localhost:3000/api/health

Method 2: PM2 Deployment (Alternative)

For those who prefer running Node.js directly:

Step 1: Install Node.js

# Install NVM curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc # Install Node.js nvm install 20 nvm use 20 # Install PM2 npm install -g pm2

Step 2: Build Application

cd ~/ignitionstack npm ci npm run build

Step 3: Create PM2 Ecosystem

// ecosystem.config.js module.exports = { apps: [{ name: 'ignitionstack', script: 'npm', args: 'start', cwd: '/home/deployer/ignitionstack', instances: 'max', exec_mode: 'cluster', env_production: { NODE_ENV: 'production', PORT: 3000 } }] };

Step 4: Start with PM2

pm2 start ecosystem.config.js --env production pm2 save pm2 startup

Step 5: Install and Configure Caddy

# Install Caddy apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list apt update apt install caddy # Configure cat > /etc/caddy/Caddyfile << EOF your-domain.com { reverse_proxy localhost:3000 encode gzip } EOF # Start systemctl restart caddy

Continuous Deployment

GitHub Actions for Auto-Deploy

Create .github/workflows/deploy.yml:

name: Deploy to VPS on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy via SSH uses: appleboy/ssh-action@master with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} script: | cd ~/ignitionstack git pull origin main docker compose up -d --build

Required GitHub Secrets

Monitoring & Maintenance

Install Monitoring

# Netdata for real-time monitoring bash <(curl -Ss https://my-netdata.io/kickstart.sh) # Access at http://your-server-ip:19999

Log Management

# View application logs docker compose logs -f app # Or with PM2 pm2 logs ignitionstack # Set up log rotation pm2 install pm2-logrotate

Backup Strategy

# Create backup script cat > ~/backup.sh << 'EOF' #!/bin/bash DATE=$(date +%Y%m%d) BACKUP_DIR=~/backups mkdir -p $BACKUP_DIR # Backup application tar -czf $BACKUP_DIR/app_$DATE.tar.gz ~/ignitionstack # Backup environment cp ~/ignitionstack/.env.production $BACKUP_DIR/env_$DATE # Keep last 7 backups find $BACKUP_DIR -mtime +7 -delete EOF chmod +x ~/backup.sh # Add to crontab (crontab -l 2>/dev/null; echo "0 3 * * * ~/backup.sh") | crontab -

SSL & Security

Automatic SSL with Caddy

Caddy automatically provisions and renews SSL certificates from Let’s Encrypt. Just ensure:

  1. Your domain DNS points to your server IP
  2. Ports 80 and 443 are open
  3. Your Caddyfile uses your domain name

Additional Security

# Fail2ban for brute-force protection apt install fail2ban systemctl enable fail2ban systemctl start fail2ban # Automatic security updates apt install unattended-upgrades dpkg-reconfigure -plow unattended-upgrades

Troubleshooting

Common Issues

Application won’t start:

# Check logs docker compose logs app # or pm2 logs ignitionstack # Check if port is in use netstat -tlnp | grep 3000

SSL certificate issues:

# Check Caddy logs journalctl -u caddy -f # Verify DNS dig +short your-domain.com

Out of memory:

# Check memory usage free -h docker stats # Add swap space fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab