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.
| Benefit | Vercel | VPS |
|---|---|---|
| Cost at Scale | $20+/month (Pro) | $5-15/month |
| Bandwidth | Limited | Unlimited |
| CPU/RAM | Shared | Dedicated |
| Control | Limited | Full |
| Custom Domain SSL | Included | DIY (free with Caddy) |
| Deploy Complexity | Simple | Moderate |
| Provider | Best For | Min Price | Datacenters |
|---|---|---|---|
| Hostinger | Beginners | $4.99/mo | USA, EU, Asia |
| Hetzner | Price/Performance | €4.15/mo | EU, USA |
| DigitalOcean | Developer Experience | $6/mo | Global |
| Vultr | High Performance | $6/mo | Global |
| Contabo | Maximum Resources | €5.99/mo | EU, USA |
| Linode | Reliability | $5/mo | Global |
| Traffic Level | RAM | vCPU | Storage | Estimated Cost |
|---|---|---|---|---|
| Starter (< 10k visits/mo) | 2GB | 1 | 50GB | $5-8/mo |
| Growth (< 100k visits/mo) | 4GB | 2 | 80GB | $12-20/mo |
| Scale (< 500k visits/mo) | 8GB | 4 | 160GB | $24-40/mo |
# 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# 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 versionufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable# 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.productionEdit 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# 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"]# 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:# 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
}// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// ... rest of your config
};
export default nextConfig;# 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/healthFor those who prefer running Node.js directly:
# 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 pm2cd ~/ignitionstack
npm ci
npm run build// 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
}
}]
};pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup# 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 caddyCreate .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 --buildVPS_HOST: Your server IPVPS_USER: SSH username (e.g., deployer)VPS_SSH_KEY: Private SSH key# Netdata for real-time monitoring
bash <(curl -Ss https://my-netdata.io/kickstart.sh)
# Access at http://your-server-ip:19999# View application logs
docker compose logs -f app
# Or with PM2
pm2 logs ignitionstack
# Set up log rotation
pm2 install pm2-logrotate# 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 -Caddy automatically provisions and renews SSL certificates from Let’s Encrypt. Just ensure:
# 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-upgradesApplication won’t start:
# Check logs
docker compose logs app
# or
pm2 logs ignitionstack
# Check if port is in use
netstat -tlnp | grep 3000SSL certificate issues:
# Check Caddy logs
journalctl -u caddy -f
# Verify DNS
dig +short your-domain.comOut 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