Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Ignore local environment files
**/*.env
!.env
.env.local

# Ignore build output directories
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
.env
.env

*.DS_Store
174 changes: 172 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,183 @@ npm run build # Check for TypeScript errors

## Deployment

### Backend
### AWS Elastic Beanstalk (Recommended for Production)

This application is configured for deployment on AWS Elastic Beanstalk with Docker Compose.

#### Prerequisites

- AWS Account
- EB CLI installed (`pip install awsebcli`)
- Docker and Docker Compose configured

#### Deployment Steps

1. **Initialize Elastic Beanstalk**
```bash
eb init -p docker waffle-chat --region ap-southeast-2
```

2. **Create Environment with Load Balancer**
```bash
eb create waffle-chat-env --instance-type t2.small --elb-type application
```

⚠️ **CRITICAL**: Always create with load balancer and enable sticky sessions from the start!

3. **Configure Environment Variables**
```bash
eb setenv \
PORT=5000 \
MONGO_URI="your-mongodb-uri" \
NODE_ENV=production \
JWT_SECRET="your-secret" \
CLIENT_URL="http://your-domain.com" \
CLOUDINARY_CLOUD_NAME="your-cloud-name" \
CLOUDINARY_API_KEY="your-api-key" \
CLOUDINARY_API_SECRET="your-api-secret"
```

4. **Enable Sticky Sessions** (REQUIRED for Socket.IO)
- Go to AWS Console → Elastic Beanstalk → Configuration → Load Balancer
- Edit default process
- Enable **Stickiness** → `Load balancer generated cookie`
- Set **Cookie duration**: `86400` (1 day)
- Save and apply

5. **Deploy Application**
```bash
# Create deployment package
zip -r deploy.zip . -x "*.git*" "*node_modules/*" "*.env" "*.zip"

# Deploy
eb deploy
```

6. **Configure HTTPS** (Required for microphone access)
- Request SSL certificate in AWS Certificate Manager
- Add HTTPS listener on port 443 in Load Balancer configuration
- Enable HTTP → HTTPS redirect

#### Common Deployment Issues & Solutions

##### Issue 1: CSP Blocking Resources
**Symptoms**: Image previews not showing, inline styles blocked, WebSocket connections failing

**Solution**: Update `nginx.conf` to include proper CSP directives:
```nginx
# Add blob: to img-src for image previews
set $CSP_image "img-src 'self' data: blob: https://res.cloudinary.com";

# Add media-src for audio/video
set $CSP_media "media-src 'self' blob: data: https://res.cloudinary.com";

# Include in final CSP
set $CSP "default-src 'self'; ${CSP_script}; ${CSP_style}; ${CSP_image}; ${CSP_media}; ${CSP_connect}; ${CSP_font}; ${CSP_frame}; ${CSP_object}; ${CSP_base}";
```

##### Issue 2: Socket.IO Connection Fails with Load Balancer
**Symptoms**: 500 errors, WebSocket upgrade fails, real-time features not working

**Root Cause**: Without sticky sessions, each request hits a different instance. Socket.IO's multi-step handshake breaks because:
- Step 1 (polling): Goes to Instance A → Creates session
- Step 2 (WebSocket upgrade): Goes to Instance B → Doesn't know about session → FAILS

**Solution**: ALWAYS enable sticky sessions (see step 4 above)

**Why it works**: Sticky sessions use cookies (AWSALB) to route all requests from same user to same instance, maintaining Socket.IO connection state.

##### Issue 3: Deployment Timeout
**Symptoms**: `Command execution completed on all instances. Summary: [Successful: 0, TimedOut: 1]`

**Solutions**:
1. Increase timeout: `eb deploy --timeout 20`
2. Terminate stuck instance via EC2 Console (load balancer auto-creates new one)
3. Restart environment: `eb restart`
4. If persistent, create new environment with clean state

##### Issue 4: Corrupted Dockerfile in Deployment
**Symptoms**: `ERROR: failed to solve: dockerfile parse error: unknown instruction: pax_global_header`

**Cause**: Archive created with tar before zip (double-archiving)

**Solution**: Create ZIP directly without tar:
```bash
zip -r deploy.zip . -x "*.git*" "*node_modules/*" "*.env"
```

Never do: `tar -czf project.tar.gz . && zip deploy.zip project.tar.gz`

##### Issue 5: SSM Agent Not Online / Can't SSH
**Symptoms**: Session Manager shows "SSM Agent is not online"

**Solutions**:
1. Use AWS Console → EC2 → Instance Connect (alternative to SSM)
2. Add EC2 key pair to environment:
- Configuration → Security → Edit → Select EC2 key pair
- Apply (will recreate instance)
3. For logs without SSH: `eb logs --all`

##### Issue 6: Missing Environment Variable Warnings
**Symptoms**: `The "CLIENT_URL" variable is not set. Defaulting to a blank string`

**Solution**: Set via EB CLI or Console:
```bash
eb setenv CLIENT_URL=https://your-domain.com
```

##### Issue 7: Microphone Access Denied
**Symptoms**: Browser blocks microphone access with "Not allowed" error

**Cause**: Modern browsers require HTTPS for microphone/camera access

**Solution**: Configure HTTPS with AWS Certificate Manager (see step 6 above)

#### Architecture Notes

- **nginx**: Serves frontend static files, proxies API and Socket.IO to backend
- **backend**: Node.js/Express container on port 5000
- **Load Balancer**: Routes traffic with sticky sessions for Socket.IO
- **MongoDB**: External (MongoDB Atlas recommended)
- **Cloudinary**: External for image/audio storage

#### Health Checks

Load balancer checks path `/` on port 80 (nginx). For better monitoring, add health endpoint to backend:

```javascript
// backend/server.js
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
```

Then configure load balancer health check path to `/api/health`.

#### Monitoring

```bash
# Check environment status
eb health

# View logs
eb logs --stream

# Check running containers
eb ssh
sudo docker ps
sudo docker logs waffle-chat-backend
```

### Alternative: Traditional Deployment

#### Backend

1. Set environment variables
2. Ensure MongoDB is accessible
3. Run `npm start` or use PM2

### Frontend
#### Frontend

1. Build: `npm run build`
2. Serve dist/ folder with nginx/vercel/netlify
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/socket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { io, Socket } from 'socket.io-client';
import type { Message } from '../types';

const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || 'http://localhost:5000';
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || window.location.origin;

class SocketService {
private socket: Socket | null = null;
Expand Down
4 changes: 2 additions & 2 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

// https://vite.dev/config/
export default defineConfig({
Expand Down
46 changes: 20 additions & 26 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,24 @@ http {
index index.html;

# --- CRITICAL FIX: Allow blob: URLs and the specific extension ID in scripts ---
set $CSP_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: chrome-extension://*/ ; ";
set $CSP_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: chrome-extension://*";
# --- Styles: Necessary for modern JS frameworks (React/Vite/Tailwind) ---
set $CSP_style "style-src 'self' 'unsafe-inline' *.googleapis.com; ";
set $CSP_style "style-src 'self' 'unsafe-inline' *.googleapis.com";
# --- Images: Allow self, data URIs, and Cloudinary for user uploads ---
set $CSP_image "img-src 'self' data: https://res.cloudinary.com; ";
set $CSP_image "img-src 'self' data: blob: https://res.cloudinary.com";
# --- Media: Allow blob and data URIs for audio/video previews ---
set $CSP_media "media-src 'self' blob: data: https://res.cloudinary.com";
# --- Connect: CRUCIAL for Socket.IO WebSockets (ws/wss) ---
# Allows connections back to the host via all protocols.
set $CSP_connect "connect-src 'self' ws://\$host wss://\$host; ";
# Allows connections back to the host via host variable
set $CSP_connect "connect-src 'self' ws: wss:";
# --- Fonts: Allow self and common CDNs ---
set $CSP_font "font-src 'self' data: *.googleapis.com *.gstatic.com; ";
set $CSP_font "font-src 'self' data: *.googleapis.com *.gstatic.com";
# --- Other directives (kept restrictive) ---
set $CSP_frame "frame-src 'self'; ";
set $CSP_object "object-src 'self'; ";
set $CSP_base "base-uri 'self'; ";
set $CSP_frame "frame-src 'self'";
set $CSP_object "object-src 'self'";
set $CSP_base "base-uri 'self'";
# --- Assemble the Final CSP Header ---
set $CSP "default-src 'self' ; \
${CSP_script} \
${CSP_style} \
${CSP_image} \
${CSP_connect} \
${CSP_font} \
${CSP_frame} \
${CSP_object} \
${CSP_base}";
set $CSP "default-src 'self'; ${CSP_script}; ${CSP_style}; ${CSP_image}; ${CSP_connect}; ${CSP_font}; ${CSP_frame}; ${CSP_object}; ${CSP_base}";
add_header Content-Security-Policy $CSP;

location / {
Expand All @@ -52,13 +46,13 @@ http {
}

# proxy websocket traffic for socket.io
# location /socket.io/ {
# proxy_pass http://backend:5000/socket.io/;
# 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;
# }
location /socket.io/ {
proxy_pass http://backend:5000/socket.io/;
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;
}
}
}