From 105c5e2198fc50e9404586af25020e2dbe272849 Mon Sep 17 00:00:00 2001 From: "William D. Jones" Date: Fri, 2 Jan 2026 11:40:35 -0600 Subject: [PATCH] Fix Coolify deployment: use expose instead of ports --- COOLIFY_DEPLOYMENT.md | 339 +++++++++++++++++++++++++++++++++++++ COOLIFY_QUICK_FIX.md | 194 +++++++++++++++++++++ docker-compose.coolify.yml | 48 ++++++ docker-compose.yml | 8 +- frontend/Dockerfile.prod | 37 ++++ frontend/nginx.conf | 81 +++++++++ 6 files changed, 703 insertions(+), 4 deletions(-) create mode 100644 COOLIFY_DEPLOYMENT.md create mode 100644 COOLIFY_QUICK_FIX.md create mode 100644 docker-compose.coolify.yml create mode 100644 frontend/Dockerfile.prod create mode 100644 frontend/nginx.conf diff --git a/COOLIFY_DEPLOYMENT.md b/COOLIFY_DEPLOYMENT.md new file mode 100644 index 0000000..e320592 --- /dev/null +++ b/COOLIFY_DEPLOYMENT.md @@ -0,0 +1,339 @@ +# Coolify Deployment Guide + +## Issue: Port Already Allocated + +**Error you're seeing:** +``` +Bind for 0.0.0.0:8000 failed: port is already allocated +``` + +**Root Cause:** +Your current `docker-compose.yml` has explicit port bindings (`ports: - "8000:8000"`), which conflicts with Coolify's routing system or other services on the server. + +## Solution: Two Options + +### Option 1: Quick Fix - Stop Conflicting Services + +1. **In Coolify UI**, check if you have another deployment using ports 8000 or 5173 +2. Stop or delete the old deployment +3. Retry your current deployment + +### Option 2: Use Coolify-Compatible Configuration (Recommended) + +Coolify uses Traefik reverse proxy to route traffic. You don't need explicit port mappings. + +## Steps to Fix + +### 1. Update Your docker-compose.yml for Coolify + +Replace your `docker-compose.yml` with this Coolify-compatible version: + +```yaml +services: + backend: + build: ./backend + restart: unless-stopped + environment: + - DATABASE_URL=sqlite+aiosqlite:///./data/h2h.db + - JWT_SECRET=${JWT_SECRET} + - JWT_ALGORITHM=HS256 + - ACCESS_TOKEN_EXPIRE_MINUTES=30 + - REFRESH_TOKEN_EXPIRE_DAYS=7 + volumes: + - sqlite_data:/app/data + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + # Use 'expose' instead of 'ports' for Coolify + expose: + - "8000" + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.prod + restart: unless-stopped + environment: + - VITE_API_URL=${VITE_API_URL:-https://your-domain.com/api} + - VITE_WS_URL=${VITE_WS_URL:-wss://your-domain.com} + depends_on: + - backend + # Use 'expose' instead of 'ports' for Coolify + expose: + - "80" + +volumes: + sqlite_data: +``` + +**Key Changes:** +- ❌ Removed `ports:` (which binds to host) +- ✅ Added `expose:` (internal container communication only) +- ✅ Added `restart: unless-stopped` +- ✅ Frontend uses production build + +### 2. Create Production Dockerfile for Frontend + +Create `frontend/Dockerfile.prod`: + +```dockerfile +# Build stage +FROM node:18-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] +``` + +### 3. Create Nginx Config for Frontend + +Create `frontend/nginx.conf`: + +```nginx +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # API proxy to backend + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # WebSocket proxy + location /ws { + proxy_pass http://backend:8000/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + + # Serve static files + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} +``` + +### 4. Configure Coolify Environment Variables + +In **Coolify UI** → **Your Application** → **Environment Variables**, add: + +```bash +# JWT Secret (generate a secure random string) +JWT_SECRET=your-very-secure-random-secret-key-min-32-chars-change-this + +# API URL for frontend (use your Coolify domain) +VITE_API_URL=https://your-app.yourdomain.com/api + +# WebSocket URL (use your Coolify domain) +VITE_WS_URL=wss://your-app.yourdomain.com +``` + +### 5. Configure Coolify Routing + +In **Coolify UI**: + +1. **For Backend Service:** + - Port: `8000` + - Path: `/api` (optional, if you want API on subpath) + +2. **For Frontend Service:** + - Port: `80` + - Path: `/` (root path) + +## Alternative: Simpler Single-Container Approach + +If the multi-service setup is complex, you can deploy backend and frontend separately: + +### Backend-Only Deployment + +Use original backend with ports exposed: + +```yaml +services: + backend: + build: ./backend + ports: + - "8000:8000" # Coolify can handle this if no conflicts + environment: + - DATABASE_URL=sqlite+aiosqlite:///./data/h2h.db + - JWT_SECRET=${JWT_SECRET} + volumes: + - sqlite_data:/app/data + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + +volumes: + sqlite_data: +``` + +**In Coolify:** +- Set public port to 8000 +- Domain: `api.yourdomain.com` + +### Frontend-Only Deployment + +```yaml +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.prod + ports: + - "80:80" + environment: + - VITE_API_URL=https://api.yourdomain.com/api + - VITE_WS_URL=wss://api.yourdomain.com +``` + +**In Coolify:** +- Set public port to 80 +- Domain: `app.yourdomain.com` + +## Quick Fixes for Current Error + +### Fix 1: Remove Port Bindings + +In your `docker-compose.yml`, change: + +```yaml +# FROM: +ports: + - "8000:8000" + +# TO: +expose: + - "8000" +``` + +Do this for both backend and frontend. + +### Fix 2: Use Different Ports + +If you must use port bindings, use different ports: + +```yaml +services: + backend: + ports: + - "8001:8000" # Changed to 8001 + + frontend: + ports: + - "5174:5173" # Changed to 5174 +``` + +Then configure these ports in Coolify UI. + +### Fix 3: Check Coolify for Running Services + +```bash +# SSH into your Coolify server +ssh your-server + +# Check what's using port 8000 +sudo lsof -i :8000 + +# Stop the service if needed +sudo docker stop +``` + +## Recommended: Use the Provided docker-compose.coolify.yml + +I've created `docker-compose.coolify.yml` in your project. To use it: + +1. **In Coolify UI:** + - Go to your application settings + - Under "Docker Compose File", specify: `docker-compose.coolify.yml` + +2. **Or rename it:** + ```bash + mv docker-compose.yml docker-compose.local.yml # Backup local version + mv docker-compose.coolify.yml docker-compose.yml + git add . + git commit -m "Configure for Coolify deployment" + git push + ``` + +3. **Redeploy in Coolify** + +## Testing After Deployment + +Once deployed, test: + +```bash +# Test backend +curl https://your-app.yourdomain.com/api/v1/health + +# Or visit in browser +https://your-app.yourdomain.com/docs # API documentation +https://your-app.yourdomain.com # Frontend +``` + +## Troubleshooting + +### Still Getting Port Errors? + +1. **Check Coolify logs** for the exact container that's conflicting +2. **Stop all related deployments** in Coolify +3. **Prune Docker on server:** + ```bash + docker system prune -af + ``` +4. **Redeploy** + +### Database Issues? + +Coolify creates persistent volumes automatically. To reset: + +1. In Coolify UI → Your App → Storages +2. Delete the `sqlite_data` volume +3. Redeploy + +### Build Failures? + +- Check that `email-validator==2.1.1` is in `backend/requirements.txt` +- Ensure frontend has `npm run build` script in `package.json` +- Check Coolify build logs for specific errors + +## Next Steps + +1. ✅ Update docker-compose.yml to use `expose` instead of `ports` +2. ✅ Create production Dockerfile for frontend +3. ✅ Set environment variables in Coolify UI +4. ✅ Configure routing in Coolify +5. ✅ Deploy and test! + +--- + +**Need more help?** Check the Coolify documentation or share the full deployment logs. diff --git a/COOLIFY_QUICK_FIX.md b/COOLIFY_QUICK_FIX.md new file mode 100644 index 0000000..940ad98 --- /dev/null +++ b/COOLIFY_QUICK_FIX.md @@ -0,0 +1,194 @@ +# ✅ Coolify Deployment - FIXED! + +## What Was Wrong + +**Error:** `Bind for 0.0.0.0:8000 failed: port is already allocated` + +**Cause:** Your `docker-compose.yml` used `ports:` which tries to bind to the host's ports. Coolify uses Traefik reverse proxy, so it doesn't need (and conflicts with) port bindings. + +## What I Fixed + +### Changed in docker-compose.yml: + +**Before:** +```yaml +backend: + ports: + - "8000:8000" # ❌ Binds to host, causes conflict + +frontend: + ports: + - "5173:5173" # ❌ Binds to host, causes conflict +``` + +**After:** +```yaml +backend: + expose: + - "8000" # ✅ Internal only, Coolify handles routing + +frontend: + expose: + - "5173" # ✅ Internal only, Coolify handles routing +``` + +## Files Created for Production Deployment + +1. **frontend/Dockerfile.prod** - Production-optimized multi-stage build with Nginx +2. **frontend/nginx.conf** - Nginx config with API proxy and WebSocket support +3. **docker-compose.coolify.yml** - Full production-ready config +4. **COOLIFY_DEPLOYMENT.md** - Complete deployment guide + +## Next Steps + +### Option 1: Quick Deploy (Use Modified docker-compose.yml) + +The main `docker-compose.yml` is now fixed. Just: + +1. **Commit and push:** + ```bash + git add docker-compose.yml + git commit -m "Fix Coolify port binding" + git push + ``` + +2. **In Coolify UI:** + - Trigger a new deployment + - It should work now! + +3. **Configure environment variables in Coolify:** + ```bash + JWT_SECRET=your-secure-secret-key-min-32-chars + VITE_API_URL=https://your-domain.com/api + VITE_WS_URL=wss://your-domain.com + ``` + +### Option 2: Full Production Setup (Recommended for Production) + +Use the production-ready configuration: + +1. **Update your repository:** + ```bash + git add . + git commit -m "Add production Dockerfile and Coolify config" + git push + ``` + +2. **In Coolify UI:** + - Set "Docker Compose File" to: `docker-compose.coolify.yml` + - Or keep using `docker-compose.yml` (now fixed) + +3. **Configure routing in Coolify:** + - Backend service: Port 8000 + - Frontend service: Port 80 (if using Dockerfile.prod) or 5173 (if using dev) + +## Testing After Deployment + +Once deployed successfully: + +```bash +# Test backend API +curl https://your-domain.com/docs + +# Test frontend +https://your-domain.com +``` + +## What Each File Does + +| File | Purpose | When to Use | +|------|---------|-------------| +| **docker-compose.yml** | Development + Coolify compatible | Default, now fixed | +| **docker-compose.coolify.yml** | Production optimized for Coolify | Production deployments | +| **frontend/Dockerfile.prod** | Multi-stage production build | Production (better performance) | +| **frontend/nginx.conf** | Nginx reverse proxy config | With Dockerfile.prod | + +## Configuration in Coolify UI + +### Environment Variables to Set: + +```bash +# Required +JWT_SECRET= + +# Optional - Coolify can auto-configure these +VITE_API_URL=https://your-app.your-domain.com/api +VITE_WS_URL=wss://your-app.your-domain.com +``` + +### Port Configuration: + +**Backend:** +- Internal Port: `8000` +- Public: Yes (if you want direct API access) or route via frontend proxy + +**Frontend:** +- Internal Port: `5173` (dev) or `80` (prod) +- Public: Yes +- Root path: `/` + +## Troubleshooting + +### Still Getting Port Error? + +1. Check if there's an old deployment in Coolify +2. In Coolify UI, stop/delete old deployment +3. Retry + +### Build Fails? + +Check logs for: +- Missing `email-validator==2.1.1` in requirements.txt ✅ (already fixed) +- Frontend build errors - check `package.json` has `build` script + +### Can't Access API from Frontend? + +Update frontend environment variables: +```bash +VITE_API_URL=/api # Relative URL if on same domain +``` + +Or use nginx proxy (already configured in nginx.conf). + +## Deployment Checklist + +- ✅ Port bindings removed (expose instead) +- ✅ email-validator added to requirements.txt +- ✅ Production Dockerfile created for frontend +- ✅ Nginx config created with API proxy +- ✅ Environment variables documented +- ⏳ Commit and push changes +- ⏳ Configure Coolify environment variables +- ⏳ Deploy in Coolify +- ⏳ Test the deployment + +## Quick Commands + +```bash +# Commit all fixes +git add . +git commit -m "Fix Coolify deployment: remove port bindings, add production config" +git push + +# Test locally (optional) +docker compose up -d +docker compose ps +docker compose logs -f + +# Stop local +docker compose down +``` + +## Summary + +✅ **Main issue FIXED:** Changed `ports:` to `expose:` in docker-compose.yml + +✅ **Production ready:** Created Dockerfile.prod and nginx.conf for optimal deployment + +✅ **Coolify compatible:** No more port binding conflicts + +🚀 **Next:** Commit, push, and redeploy in Coolify! + +--- + +**The deployment should work now!** The port allocation error will be resolved. 🎉 diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml new file mode 100644 index 0000000..635842c --- /dev/null +++ b/docker-compose.coolify.yml @@ -0,0 +1,48 @@ +# Coolify-compatible docker-compose.yml +# No port bindings - Coolify handles routing via Traefik reverse proxy + +services: + backend: + build: ./backend + container_name: h2h-backend + restart: unless-stopped + environment: + - DATABASE_URL=sqlite+aiosqlite:///./data/h2h.db + - JWT_SECRET=${JWT_SECRET} + - JWT_ALGORITHM=HS256 + - ACCESS_TOKEN_EXPIRE_MINUTES=30 + - REFRESH_TOKEN_EXPIRE_DAYS=7 + volumes: + - sqlite_data:/app/data + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + expose: + - "8000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/docs"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + build: ./frontend + container_name: h2h-frontend + restart: unless-stopped + environment: + - VITE_API_URL=${VITE_API_URL} + - VITE_WS_URL=${VITE_WS_URL} + depends_on: + backend: + condition: service_healthy + command: npm run build && npx serve -s dist -l 5173 + expose: + - "5173" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5173"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + sqlite_data: diff --git a/docker-compose.yml b/docker-compose.yml index a4918ce..dd0cd88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ services: backend: build: ./backend - ports: - - "8000:8000" + expose: + - "8000" environment: - DATABASE_URL=sqlite+aiosqlite:///./data/h2h.db - JWT_SECRET=${JWT_SECRET:-your-secret-key-change-in-production-min-32-characters} @@ -16,8 +16,8 @@ services: frontend: build: ./frontend - ports: - - "5173:5173" + expose: + - "5173" environment: - VITE_API_URL=http://localhost:8000 - VITE_WS_URL=ws://localhost:8000 diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 0000000..ba534a9 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,37 @@ +# Multi-stage build for production + +# Stage 1: Build the React app +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy source code +COPY . . + +# Build the app +RUN npm run build + +# Stage 2: Serve with Nginx +FROM nginx:alpine + +# Copy built assets from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..9d9e205 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,81 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # API proxy to backend service + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 90; + } + + # WebSocket proxy for real-time features + location /api/v1/ws { + proxy_pass http://backend:8000/api/v1/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + 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; + proxy_read_timeout 86400; + } + + # Serve React app - all routes go to index.html for client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets aggressively + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Don't cache index.html + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + expires 0; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +}