Skip to main content

OpportunityDAO - Systemd Deployment Guide

This guide covers deploying OpportunityDAO to EC2 with systemd (without Docker).

Architecture

GitLab (Self-hosted) → Push to main

GitLab CI/CD Pipeline

EC2 Instance (Ubuntu 24 + Virtualmin + Apache)
├── Systemd Service: opportunitydao-app (Next.js on port 3000)
├── Systemd Timer: opportunitydao-deposit-processor (Background job)
└── Apache → Reverse Proxy → Node.js App

Prerequisites

On EC2 Instance

  1. Install Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node --version # Should be v20.x
  1. Create deployment directory
sudo mkdir -p /var/www/opportunitydao
sudo chown $USER:$USER /var/www/opportunitydao
  1. Setup environment variables
# Create .env file
cat > /var/www/opportunitydao/.env <<'EOF'
# Database
DATABASE_URL="postgresql://user:pass@hostip:5432/opportunitydao"

# JWT
JWT_SECRET="your-secret-key-change-this"

# Public URL
NEXT_PUBLIC_API_URL="https://opportunitydao.app"

# Optional: Blockchain RPC endpoints
BSC_RPC_URL=""
ETH_RPC_URL=""
POLYGON_RPC_URL=""
BLOCKFROST_API_KEY=""
BLOCKFROST_NETWORK="mainnet"
EOF

chmod 600 /var/www/opportunitydao/.env

Systemd Services

1. Main App Service

Create /etc/systemd/system/opportunitydao-app.service:

[Unit]
Description=OpportunityDAO Next.js Application
After=network.target postgresql.service
Wants=opportunitydao-deposit-processor.timer

[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/var/www/opportunitydao
EnvironmentFile=/var/www/opportunitydao/.env
Environment=NODE_ENV=production
Environment=PORT=3000

ExecStart=/usr/bin/npm start

Restart=always
RestartSec=10

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=opportunitydao-app

# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/www/opportunitydao

# Resource limits
MemoryLimit=2G
CPUQuota=200%

[Install]
WantedBy=multi-user.target

2. Deposit Processor (Already Created)

Your existing systemd service from systemd/ directory works perfectly. Just update the path:

# Update paths in systemd files
cd /var/www/opportunitydao/systemd
sed -i 's|/var/www/opportunitydao|/var/www/opportunitydao|g' opportunitydao-deposit-processor.service
sed -i 's|/var/www/opportunitydao|/var/www/opportunitydao|g' opportunitydao-deposit-processor.timer

# Copy to systemd
sudo cp opportunitydao-deposit-processor.service /etc/systemd/system/
sudo cp opportunitydao-deposit-processor.timer /etc/systemd/system/

Enable and Start Services

# Reload systemd
sudo systemctl daemon-reload

# Enable services (auto-start on boot)
sudo systemctl enable opportunitydao-app
sudo systemctl enable opportunitydao-deposit-processor.timer

# Start services
sudo systemctl start opportunitydao-app
sudo systemctl start opportunitydao-deposit-processor.timer

# Check status
sudo systemctl status opportunitydao-app
sudo systemctl status opportunitydao-deposit-processor.timer

Apache Configuration

Same as Docker deployment - see DEPLOYMENT_DOCKER.md Apache section.

Summary:

# Enable modules
sudo a2enmod proxy proxy_http ssl headers

# Create virtual host
sudo nano /etc/apache2/sites-available/opportunitydao.conf
# [Add Apache config from DEPLOYMENT_DOCKER.md]

# Enable site
sudo a2ensite opportunitydao
sudo apache2ctl configtest
sudo systemctl reload apache2

# Setup SSL
sudo certbot --apache -d opportunitydao.com

GitLab CI/CD Pipeline

Create .gitlab-ci.yml.systemd:

stages:
- build
- deploy

variables:
APP_NAME: opportunitydao
DEPLOY_PATH: /var/www/opportunitydao

build:
stage: build
image: node:20
cache:
paths:
- node_modules/
- .next/cache/
script:
- npm ci
- npx prisma generate
- npm run build
artifacts:
paths:
- .next/
- node_modules/
- public/
- prisma/
- package*.json
expire_in: 1 hour
only:
- main

deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -H $EC2_HOST >> ~/.ssh/known_hosts
script:
# Sync files to EC2
- rsync -avz --delete
--exclude='.git'
--exclude='.env'
./ $EC2_USER@$EC2_HOST:$DEPLOY_PATH/

# Deploy on EC2
- ssh $EC2_USER@$EC2_HOST "
cd $DEPLOY_PATH &&
npm ci --production=false &&
npx prisma generate &&
npx prisma migrate deploy &&
npm run build &&
sudo systemctl restart opportunitydao-app
"
only:
- main
environment:
name: production
url: https://opportunitydao.app
when: manual

Management Commands

Service Control

# App service
sudo systemctl start opportunitydao-app
sudo systemctl stop opportunitydao-app
sudo systemctl restart opportunitydao-app
sudo systemctl status opportunitydao-app

# Deposit processor
sudo systemctl start opportunitydao-deposit-processor.service # Manual run
sudo systemctl status opportunitydao-deposit-processor.timer # Check timer

Logs

# View app logs
sudo journalctl -u opportunitydao-app -f # Follow live
sudo journalctl -u opportunitydao-app -n 100 # Last 100 lines
sudo journalctl -u opportunitydao-app --since today # Today's logs

# View deposit processor logs
sudo journalctl -u opportunitydao-deposit-processor -f

Updates

cd /var/www/opportunitydao
git pull origin main
npm ci
npx prisma generate
npx prisma migrate deploy
npm run build
sudo systemctl restart opportunitydao-app

Comparison: Docker vs Systemd

FeatureDockerSystemd
Isolation✅ Full containerization⚠️ Shares host system
Dependencies✅ Bundled in image⚠️ Must manage on host
Setup Complexity⚠️ More complex✅ Simpler
Resource Usage⚠️ Slightly higher✅ Lower overhead
Rollback✅ Easy (old images)⚠️ Manual git checkout
Multi-tenancy✅ Better for shared hosting⚠️ Less isolated
Monitoring⚠️ Container tools needed✅ journalctl built-in
Your Current Setup⚠️ New tool to learn✅ Already using for processor

Recommendation for Your Setup

Use Docker if:

  • Running multiple apps on same server (shared hosting)
  • Want strong isolation between applications
  • Want easy rollback capabilities
  • Plan to scale horizontally

Use Systemd if:

  • Simple single-app deployment
  • Want to minimize overhead
  • Comfortable with Linux system administration
  • Already familiar with systemd (you're using it for deposit processor)

Given your Virtualmin shared hosting setup, Docker is recommended for better isolation from other sites.

Troubleshooting

App won't start

# Check logs
sudo journalctl -u opportunitydao-app -n 50

# Check if port 3000 is in use
sudo netstat -tulpn | grep 3000

# Verify Node.js version
node --version

# Test manually
cd /var/www/opportunitydao
npm start

Database connection errors

# Test database connection
psql $DATABASE_URL -c "SELECT 1;"

# Check environment variables
sudo systemctl show opportunitydao-app -p Environment

High memory usage

# Check current usage
systemctl status opportunitydao-app

# Adjust memory limit
sudo systemctl edit opportunitydao-app
# Add: MemoryLimit=1G

sudo systemctl daemon-reload
sudo systemctl restart opportunitydao-app

Security

Same considerations as Docker deployment:

  • Firewall configuration
  • SSL certificates
  • Environment variable protection
  • Regular updates
# Automated security updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

Backup

#!/bin/bash
# /usr/local/bin/backup-opportunitydao.sh

BACKUP_DIR="/var/backups/opportunitydao"
mkdir -p $BACKUP_DIR

# Backup database
pg_dump $DATABASE_URL > $BACKUP_DIR/db-$(date +%Y%m%d).sql

# Backup application
tar -czf $BACKUP_DIR/app-$(date +%Y%m%d).tar.gz /var/www/opportunitydao

# Keep only last 7 days
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete

Add to cron:

sudo crontab -e
# Add: 0 2 * * * /usr/local/bin/backup-opportunitydao.sh