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
- 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
- Create deployment directory
sudo mkdir -p /var/www/opportunitydao
sudo chown $USER:$USER /var/www/opportunitydao
- 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
| Feature | Docker | Systemd |
|---|---|---|
| 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