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
69 changes: 69 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Backend-only Docker image - exclude frontend entirely
frontend/

# Python
backend/venv/
backend/__pycache__/
backend/**/__pycache__/
backend/**/*.pyc
backend/**/*.pyo
backend/**/*.pyd
backend/.pytest_cache/
backend/htmlcov/
backend/.coverage
backend/.env
backend/.env.local
backend/env.production.template

# Git
.git/
.gitignore
.gitattributes

# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store

# Documentation (not needed in container)
docs/
*.md

# Testing
backend/tests/

# Build artifacts
*.log

# Misc
.cursor/
terminals/
*.local.conf
*.example
*.template
Dockerfile
docker-compose.yml
.dockerignore

# CI/CD
.github/

# Nginx configs
nginx/
nginx.conf

# Project specific
Multi\ User\ Pregame\ Lobby\ API.postman_collection.json
image.png
lobby.png
LICENSE
GAME.MD
SERVING.md
node_modules/
package.json
package-lock.json
eslint.config.js

116 changes: 116 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Deploy Backend to AWS ECS

on:
push:
branches:
- main
paths:
- 'backend/**'
- 'Dockerfile'
- '.github/workflows/deploy.yml'
workflow_dispatch:

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: lockout-game-backend
ECS_CLUSTER: lockout-game-cluster
ECS_SERVICE: lockout-game-service
ECS_TASK_DEFINITION: lockout-game-task
CONTAINER_NAME: lockout-game-backend

jobs:
test:
name: Run Backend Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: backend/requirements.txt

- name: Install backend dependencies
working-directory: backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov

- name: Run backend tests
working-directory: backend
run: pytest

build-and-deploy:
name: Build and Deploy to ECS
needs: test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
# Build Docker image
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest

# Push to ECR
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

# Output image URI for use in next steps
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

- name: Download current task definition
run: |
aws ecs describe-task-definition \
--task-definition ${{ env.ECS_TASK_DEFINITION }} \
--query taskDefinition > task-definition.json

- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}

- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true

- name: Deployment summary
run: |
echo "### Backend Deployment Successful! 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: ${{ steps.build-image.outputs.image }}" >> $GITHUB_STEP_SUMMARY
echo "- **Cluster**: ${{ env.ECS_CLUSTER }}" >> $GITHUB_STEP_SUMMARY
echo "- **Service**: ${{ env.ECS_SERVICE }}" >> $GITHUB_STEP_SUMMARY
echo "- **Region**: ${{ env.AWS_REGION }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check the [ECS Console](https://console.aws.amazon.com/ecs/v2/clusters/${{ env.ECS_CLUSTER }}/services/${{ env.ECS_SERVICE }}/health?region=${{ env.AWS_REGION }}) for service status." >> $GITHUB_STEP_SUMMARY
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
run: npm ci

- name: Run tests
run: npm run test
run: npm run test -- --run
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Backend-only Dockerfile for Lockout Game Flask API
FROM python:3.12-slim

WORKDIR /app

# Install system dependencies: curl for health checks
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Copy backend requirements and install Python dependencies
COPY backend/requirements.txt ./backend/
RUN pip install --no-cache-dir -r backend/requirements.txt

# Copy backend application code
COPY backend/ ./backend/

# Expose port 5000 (Flask/Gunicorn)
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1

# Run Gunicorn with eventlet worker for WebSocket support
CMD ["gunicorn", "--bind", "0.0.0.0:5000", \
"--workers", "2", \
"--worker-class", "eventlet", \
"--timeout", "120", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info", \
"backend.app:app"]

88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,94 @@ Optionally, the **Game Host** can use **Force Start** (bypasses balance check bu

---

## 🚀 Deployment

The Lockout Game is designed for deployment on AWS using a split architecture:

- **Backend (Flask + Socket.IO)**: AWS ECS Fargate with Application Load Balancer
- **Frontend (React)**: AWS Amplify with CloudFront CDN

### Deployment Architecture

```
┌─────────────┐
│ Users │
└──────┬──────┘
│ HTTPS
┌──────▼────────┐
│ AWS Amplify │
│ Frontend │
│ CloudFront │
└──────┬────────┘
│ API/WebSocket
│ HTTPS
┌──────▼─────────┐
│ ALB (HTTPS) │
│ Backend API │
└──────┬─────────┘
┌──────▼────────┐
│ ECS Fargate │
│ Flask Tasks │
└───────────────┘
```

### Quick Start Deployment

1. **Deploy Backend to ECS Fargate**

- See [AWS Backend Deployment Guide](docs/aws-backend-deployment.md)
- Estimated setup time: 30-60 minutes

2. **Deploy Frontend to Amplify**

- See [AWS Frontend Deployment Guide](docs/aws-frontend-deployment.md)
- Estimated setup time: 15-30 minutes

3. **Troubleshooting**
- See [Deployment Troubleshooting Guide](docs/deployment-troubleshooting.md)

### Local Testing with Docker

Test the backend in a production-like environment:

```bash
# Build and run backend container
docker-compose up --build

# Backend available at http://localhost:5000
# Test health: curl http://localhost:5000/health
```

### CI/CD

Automated deployments are configured via GitHub Actions:

- **Backend**: Deploys to ECS on changes to `backend/` directory
- **Frontend**: Deploys to Amplify on changes to `frontend/` directory

See `.github/workflows/` for workflow configurations.

### Environment Variables

**Backend** (AWS Secrets Manager):

- `FLASK_ENV` - Application environment (production)
- `SECRET_KEY` - Flask secret key (generate securely)
- `FRONTEND_URL` - Frontend domain for CORS
- `ALLOWED_ORIGINS` - Comma-separated list of allowed origins

**Frontend** (AWS Amplify Environment Variables):

- `VITE_API_URL` - Backend API URL
- `VITE_SOCKET_URL` - Backend WebSocket URL

See `env.template` and `backend/env.production.template` for examples.

---

## 📜 Contribution Notes

- Use `Grid2` from `@mui/material` with the `size={{ xs, sm }}` prop.
Expand Down
19 changes: 15 additions & 4 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# backend/app.py
from flask import Flask
from flask import Flask, jsonify
from flask_socketio import SocketIO
from flask_cors import CORS
from .config import Config
Expand All @@ -10,9 +10,20 @@

app = Flask(__name__)
app.config.from_object(Config)
CORS(app)

socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
# Configure CORS with allowed origins from config
CORS(app, origins=app.config.get('ALLOWED_ORIGINS', '*'))

socketio = SocketIO(app, cors_allowed_origins=app.config.get('ALLOWED_ORIGINS', '*'), async_mode='eventlet')

# Health check endpoint for ALB
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint for load balancer"""
return jsonify({
'status': 'healthy',
'service': 'lockout-game'
}), 200

# Register REST API routes
app.register_blueprint(lobby_bp, url_prefix='/api')
Expand All @@ -23,4 +34,4 @@
register_game_socket_handlers(socketio) # Register our new game socket handlers

if __name__ == '__main__':
socketio.run(app, debug=True)
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
20 changes: 20 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,24 @@
load_dotenv()

class Config:
# Environment
FLASK_ENV = os.getenv('FLASK_ENV', 'development')
DEBUG = FLASK_ENV == 'development'

# Security
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')

# URLs and Origins
FRONTEND_URL = os.getenv('FRONTEND_URL', 'http://localhost:5173') # Default for local dev

# Configure allowed origins based on environment
if FLASK_ENV == 'production':
# In production, use specific allowed origins
ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS', FRONTEND_URL).split(',')
else:
# In development, allow all origins for easier testing
ALLOWED_ORIGINS = '*'

# Server configuration
PORT = int(os.getenv('PORT', 5000))
HOST = os.getenv('HOST', '0.0.0.0')
Loading