diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index b5b240f..9ea59c7 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,102 +1,340 @@ -# Deployment +# Deploying Eventara on a VPS (DigitalOcean Droplet) -Deploy Eventara on any VPS with Docker. +A step‑by‑step guide to deploy Eventara on a fresh Ubuntu VPS. +Everything runs from a **single `docker compose` command** — the dashboard, API, Kafka, PostgreSQL, and Redis are all included. -## Prerequisites +--- -- Docker Engine + Docker Compose plugin -- ~2 CPU / 4 GB RAM (minimum) +## Minimum Requirements -## Quick deploy +| Resource | Minimum | Recommended | +|---|---|---| +| **RAM** | 4 GB | 8 GB | +| **vCPUs** | 2 | 4 | +| **Disk** | 25 GB SSD | 50 GB SSD | +| **OS** | Ubuntu 22.04+ | Ubuntu 24.04 LTS | + +> **Why 4 GB minimum?** +> Kafka alone needs ~700 MB, PostgreSQL ~500 MB, the JVM ~512 MB, plus Redis and the OS. +> With 2 GB you **will** hit OOM kills. 4 GB works for demos and light traffic (~50 events/sec). +> For production workloads (100+ events/sec), use 8 GB. + +### DigitalOcean Droplet Sizes + +| Droplet | RAM | vCPUs | Monthly Cost | Use Case | +|---|---|---|---|---| +| Basic $24 | 4 GB | 2 | $24/mo | Demos, dev, light production | +| Basic $48 | 8 GB | 4 | $48/mo | Production workloads | + +--- + +## Step 1 — Install Docker + +You're already SSH'd into the droplet. Run these commands: + +```bash +# Update packages +sudo apt update && sudo apt upgrade -y + +# Install Docker via the official convenience script +curl -fsSL https://get.docker.com | sh + +# Add your user to the docker group (avoids needing sudo for docker commands) +sudo usermod -aG docker $USER + +# Apply group change (or log out and back in) +newgrp docker + +# Verify Docker is working +docker --version +docker compose version +``` + +You should see something like: +``` +Docker version 27.x.x +Docker Compose version v2.x.x +``` + +--- + +## Step 2 — Clone the Repository + +```bash +cd ~ +git clone https://github.com/tusharkhatriofficial/Eventara.git +cd Eventara +``` + +--- + +## Step 3 — Configure Environment Variables ```bash -git clone https://github.com/tusharkhatriofficial/eventara.git -cd eventara cp .env.example .env -# Edit .env — at minimum change POSTGRES_PASSWORD +nano .env +``` + +**At minimum, change these values:** + +```env +# REQUIRED — change the default password! +POSTGRES_PASSWORD=your_strong_password_here +# OPTIONAL — change the port if 8080 is taken +EVENTARA_PORT=8080 + +# OPTIONAL — tune JVM memory based on your droplet RAM +# For 4 GB droplet: +JAVA_OPTS=-Xms256m -Xmx512m +# For 8 GB droplet: +# JAVA_OPTS=-Xms512m -Xmx1g +``` + +Save and exit (`Ctrl+O`, `Enter`, `Ctrl+X` in nano). + +--- + +## Step 4 — Build & Start + +```bash docker compose --env-file .env -f docker-compose.prod.yaml up -d --build ``` -The build takes 2-5 minutes. It compiles the React dashboard and Spring Boot app into a **single container** — both API and dashboard are served on port `8080`. +**First build takes 3‑8 minutes** (downloads dependencies, compiles Java, builds React dashboard). +Subsequent deploys take ~1 minute since Docker caches layers. + +--- + +## Step 5 — Verify Everything is Running + +```bash +# Check container status +docker compose -f docker-compose.prod.yaml ps +``` + +You should see all 4 containers running: + +``` +NAME STATUS PORTS +eventara-eventara-1 Up 30 seconds 0.0.0.0:8080->8080/tcp +eventara-kafka-1 Up 45 seconds (healthy) 9092/tcp +eventara-postgres-1 Up 45 seconds (healthy) 5432/tcp +eventara-redis-1 Up 45 seconds (healthy) 6379/tcp +``` + +**Test the API:** + +```bash +curl -s http://localhost:8080/api/v1/events \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"eventType":"deploy.test","source":"vps","userId":"admin","severity":"INFO"}' +``` + +Expected response: +```json +{"eventId":"evt_...","eventType":"deploy.test","status":"accepted",...} +``` + +**Access from your browser:** | What | URL | |---|---| -| Dashboard + API | `http://:8080` | -| Swagger | `http://:8080/swagger-ui.html` | +| Dashboard | `http://:8080` | +| Swagger API Docs | `http://:8080/swagger-ui.html` | + +--- + +## Common Operations + +```bash +# View logs (follow mode) +docker compose -f docker-compose.prod.yaml logs -f eventara -## Coolify +# View logs for a specific service +docker compose -f docker-compose.prod.yaml logs -f kafka -1. **Add a new resource** → Docker Compose -2. Paste the contents of `docker-compose.prod.yaml` (or point it to the repo) -3. Add the environment variables from `.env.example` in the **Environment Variables** tab -4. Set the exposed port to `8080` -5. Deploy +# Restart the app (without rebuilding) +docker compose -f docker-compose.prod.yaml restart eventara -Coolify will handle the build and TLS for you. +# Stop everything +docker compose -f docker-compose.prod.yaml down + +# Stop and wipe ALL data (database, kafka, redis) +docker compose -f docker-compose.prod.yaml down -v + +# Rebuild after pulling new code +git pull origin main +docker compose --env-file .env -f docker-compose.prod.yaml up -d --build +``` + +--- -## DigitalOcean / Generic Ubuntu VPS +## Setting Up a Domain with HTTPS (Optional but Recommended) + +### Option A — Caddy (easiest, auto‑HTTPS) ```bash -# SSH into your droplet -ssh root@ +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list +sudo apt update +sudo apt install caddy +``` -# Install Docker (if not already installed) -curl -fsSL https://get.docker.com | sh +Create a Caddyfile: -# Clone and deploy -git clone https://github.com/tusharkhatriofficial/eventara.git -cd eventara -cp .env.example .env -nano .env # change POSTGRES_PASSWORD at minimum +```bash +sudo nano /etc/caddy/Caddyfile +``` + +``` +eventara.yourdomain.com { + reverse_proxy localhost:8080 +} +``` + +```bash +sudo systemctl restart caddy +``` + +That's it — Caddy automatically provisions an SSL certificate via Let's Encrypt. + +### Option B — Nginx + Certbot + +```bash +sudo apt install -y nginx certbot python3-certbot-nginx + +sudo nano /etc/nginx/sites-available/eventara +``` + +```nginx +server { + server_name eventara.yourdomain.com; + + location / { + proxy_pass http://localhost:8080; + 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; + } + + # WebSocket support for real-time dashboard + location /ws { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} +``` + +```bash +sudo ln -s /etc/nginx/sites-available/eventara /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx +sudo certbot --nginx -d eventara.yourdomain.com +``` + +--- + +## Firewall Setup + +Only open the ports you need: + +```bash +# Allow SSH +sudo ufw allow 22 + +# Allow HTTP/HTTPS (for Caddy or Nginx) +sudo ufw allow 80 +sudo ufw allow 443 + +# If accessing the app directly without a reverse proxy: +sudo ufw allow 8080 + +# Enable the firewall +sudo ufw enable +``` + +> **Important:** Do NOT open ports for Postgres (5432), Redis (6379), or Kafka (9092/9094). +> These are internal services and should never be exposed to the internet. +--- + +## Updating the App + +When you push new code: + +```bash +cd ~/Eventara +git pull origin main docker compose --env-file .env -f docker-compose.prod.yaml up -d --build ``` -## Common operations +Docker will only rebuild changed layers, so updates are much faster than the first build. + +--- + +## Monitoring & Troubleshooting ```bash -# Logs -docker compose -f docker-compose.prod.yaml logs -f eventara +# Check which containers are running +docker compose -f docker-compose.prod.yaml ps -# Stop -docker compose -f docker-compose.prod.yaml down +# Check resource usage +docker stats --no-stream + +# Check if a specific service is unhealthy +docker compose -f docker-compose.prod.yaml logs kafka --tail 50 + +# Check if the app started successfully +docker compose -f docker-compose.prod.yaml logs eventara --tail 100 + +# Restart a single service +docker compose -f docker-compose.prod.yaml restart eventara -# Wipe all data (DB + Kafka + Redis) +# Nuclear reset (wipes all data, starts fresh) docker compose -f docker-compose.prod.yaml down -v +docker compose --env-file .env -f docker-compose.prod.yaml up -d --build ``` -## Resource limits - -The prod compose file sets memory limits: +### Common Issues -| Service | Memory Limit | +| Problem | Fix | |---|---| -| Postgres | 1 GB | -| Kafka | 1 GB | -| Redis | 512 MB | -| Eventara (API + Dashboard) | 1 GB | +| `port is already allocated` | Change `EVENTARA_PORT=3000` in `.env` | +| Containers keep restarting | Check logs: `docker compose -f docker-compose.prod.yaml logs eventara` | +| Out of memory (OOM killed) | Upgrade to a larger droplet (8 GB) or reduce JVM: `JAVA_OPTS=-Xms128m -Xmx256m` | +| Kafka unhealthy | Wait 30-60 seconds, Kafka is slow to start. If persistent: `docker compose -f docker-compose.prod.yaml restart kafka` | +| Can't connect from browser | Ensure firewall allows port 8080: `sudo ufw allow 8080` | + +--- -Total: ~3.5 GB. A 4 GB VPS works for demos and light use. +## Resource Limits -## Environment variables +The production compose file caps memory to prevent any single service from crashing the server: -| Variable | Default | Description | +| Service | Memory Limit | Purpose | |---|---|---| -| `POSTGRES_USER` | `postgres` | Database user | -| `POSTGRES_PASSWORD` | `mysecretpassword` | **Change this** | -| `POSTGRES_DB` | `eventara` | Database name | -| `CLUSTER_ID` | `NvDmnaWzQgiH8qbnraqxcg` | Kafka KRaft cluster ID — don't change after first boot | -| `EVENTARA_PORT` | `8080` | Host port for the app | -| `JAVA_OPTS` | `-Xms256m -Xmx512m` | JVM memory settings | +| PostgreSQL | 1 GB | Database | +| Kafka | 1 GB | Event streaming | +| Redis | 512 MB | Caching & metrics | +| Eventara (API + Dashboard) | 1 GB | Application | +| **Total** | **~3.5 GB** | | -## Kafka (KRaft mode) +This leaves ~500 MB for the OS on a 4 GB droplet. -No Zookeeper needed. `CLUSTER_ID` must stay stable for a given Kafka data volume. If you change it, wipe the volume first (`docker compose down -v`). +--- -## Production hardening checklist +## Production Hardening Checklist -- [ ] Change default database password in `.env` -- [ ] Put behind a reverse proxy (Nginx/Caddy) for TLS -- [ ] Don't expose Postgres/Redis ports to the internet (prod compose already doesn't) -- [ ] Use a managed database if scaling beyond a single node +- [ ] Changed default `POSTGRES_PASSWORD` in `.env` +- [ ] Set up a domain with HTTPS (Caddy or Nginx + Certbot) +- [ ] Enabled UFW firewall (only ports 22, 80, 443 open) +- [ ] Did NOT expose Postgres/Redis/Kafka ports to the internet +- [ ] Set up automated backups for the Postgres volume +- [ ] Tested the deployment with a test event via curl diff --git a/eventara-dashboard/src/hooks/useWebSocket.ts b/eventara-dashboard/src/hooks/useWebSocket.ts index 8ea5cc5..02c43bb 100644 --- a/eventara-dashboard/src/hooks/useWebSocket.ts +++ b/eventara-dashboard/src/hooks/useWebSocket.ts @@ -7,15 +7,23 @@ interface UseWebSocketMetricsReturn extends UseWebSocketReturn { metrics: ComprehensiveMetrics | null; } -export const useWebSocketMetrics = (url: string = `${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/ws`): UseWebSocketMetricsReturn => { +// Derive WebSocket URL from the current page location so it works on any deployment +const getDefaultWsUrl = (): string => { + if (import.meta.env.VITE_API_URL) return `${import.meta.env.VITE_API_URL}/ws`; + // In production, use the same origin as the page (works for VPS, Coolify, etc.) + if (typeof window !== 'undefined') return `${window.location.origin}/ws`; + return 'http://localhost:8080/ws'; +}; + +export const useWebSocketMetrics = (url: string = getDefaultWsUrl()): UseWebSocketMetricsReturn => { const [metrics, setMetrics] = useState(null); const [connectionState, setConnectionState] = useState(ConnectionState.CONNECTING); const [error, setError] = useState(null); - + const clientRef = useRef(null); const reconnectTimeoutRef = useRef(null); const reconnectAttemptsRef = useRef(0); - + const MAX_RECONNECT_ATTEMPTS = 10; const RECONNECT_INTERVAL = 3000; // 3 seconds @@ -80,13 +88,13 @@ export const useWebSocketMetrics = (url: string = `${import.meta.env.VITE_API_UR stompClient.onWebSocketClose = () => { console.log('🔌 WebSocket connection closed'); setConnectionState(ConnectionState.DISCONNECTED); - + // Attempt reconnection if (reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) { reconnectAttemptsRef.current++; console.log(`Reconnecting... (Attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS})`); setConnectionState(ConnectionState.RECONNECTING); - + reconnectTimeoutRef.current = setTimeout(() => { connect(); }, RECONNECT_INTERVAL); @@ -111,12 +119,12 @@ export const useWebSocketMetrics = (url: string = `${import.meta.env.VITE_API_UR if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } - + if (clientRef.current) { clientRef.current.deactivate(); clientRef.current = null; } - + setConnectionState(ConnectionState.DISCONNECTED); console.log('🔌 Disconnected'); }, []);