Add Linode server deployment package
- Create Dockerfile for backend with Python 3.12 and gunicorn - Add docker-compose.yml with PostgreSQL, backend, nginx, certbot - Configure nginx reverse proxy with SSL and rate limiting - Add deployment scripts: deploy.sh, backup-db.sh, setup-ssl.sh - Include environment template and deployment documentation
This commit is contained in:
parent
d8164be49a
commit
3c513594ba
12
deployment/.env.example
Normal file
12
deployment/.env.example
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DB_USER=agenda
|
||||||
|
DB_PASSWORD=your-secure-database-password-here
|
||||||
|
DB_NAME=agenda_tasks
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
SECRET_KEY=your-secret-key-min-32-characters-here
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
# Domain Configuration (for SSL)
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
EMAIL=admin@your-domain.com
|
||||||
319
deployment/README.md
Normal file
319
deployment/README.md
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
# Agenda Tasks - Server Deployment Guide
|
||||||
|
|
||||||
|
This guide explains how to deploy the Agenda Tasks backend to a Linode server.
|
||||||
|
|
||||||
|
## Server Requirements
|
||||||
|
|
||||||
|
**Recommended: Linode 2GB ($12/month)**
|
||||||
|
- 1 CPU core
|
||||||
|
- 2 GB RAM
|
||||||
|
- 50 GB SSD storage
|
||||||
|
- Ubuntu 22.04 LTS
|
||||||
|
|
||||||
|
This configuration is sufficient for a small to medium user base.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A Linode server with Ubuntu 22.04 LTS
|
||||||
|
2. A domain name pointing to your server's IP address
|
||||||
|
3. SSH access to the server
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Connect to Your Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@your-server-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update system
|
||||||
|
apt update && apt upgrade -y
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sh get-docker.sh
|
||||||
|
|
||||||
|
# Install Docker Compose
|
||||||
|
apt install docker-compose-plugin -y
|
||||||
|
|
||||||
|
# Start Docker
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create app directory
|
||||||
|
mkdir -p /opt/agenda-tasks
|
||||||
|
cd /opt/agenda-tasks
|
||||||
|
|
||||||
|
# Clone repository (or copy files)
|
||||||
|
git clone https://your-repo-url.git .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deployment
|
||||||
|
|
||||||
|
# Copy environment template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the following variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database Configuration
|
||||||
|
DB_USER=agenda
|
||||||
|
DB_PASSWORD=<generate-a-secure-password>
|
||||||
|
DB_NAME=agenda_tasks
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
SECRET_KEY=<generate-a-32-character-secret>
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
# Domain Configuration
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
EMAIL=admin@your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate Secure Values:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate DB_PASSWORD (32 characters)
|
||||||
|
openssl rand -base64 32
|
||||||
|
|
||||||
|
# Generate SECRET_KEY (32 characters)
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks/deployment/scripts
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Set Up SSL
|
||||||
|
|
||||||
|
After DNS propagation (may take up to 48 hours):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-ssl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
deployment/
|
||||||
|
├── docker/
|
||||||
|
│ ├── Dockerfile.backend # Backend container
|
||||||
|
│ └── docker-compose.yml # Service orchestration
|
||||||
|
├── nginx/
|
||||||
|
│ ├── nginx.conf # Main nginx config
|
||||||
|
│ └── conf.d/
|
||||||
|
│ └── default.conf # Server block config
|
||||||
|
├── scripts/
|
||||||
|
│ ├── deploy.sh # Deployment script
|
||||||
|
│ ├── backup-db.sh # Database backup
|
||||||
|
│ └── setup-ssl.sh # SSL certificate setup
|
||||||
|
├── backups/ # Database backups (created automatically)
|
||||||
|
├── .env.example # Environment template
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
| Service | Port | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| PostgreSQL | 5432 (internal) | Database |
|
||||||
|
| Backend | 8000 (internal) | FastAPI application |
|
||||||
|
| Nginx | 80, 443 | Reverse proxy with SSL |
|
||||||
|
| Certbot | - | SSL certificate management |
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
After deployment, your API will be available at:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://your-domain.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| POST | /auth/register | Register new user |
|
||||||
|
| POST | /auth/login | Login and get tokens |
|
||||||
|
| POST | /auth/refresh | Refresh access token |
|
||||||
|
| GET | /auth/me | Get current user info |
|
||||||
|
|
||||||
|
### Task Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | /tasks?date=YYYY-MM-DD | Get tasks for date |
|
||||||
|
| POST | /tasks | Create new task |
|
||||||
|
| GET | /tasks/{id} | Get task by ID |
|
||||||
|
| PUT | /tasks/{id} | Update task |
|
||||||
|
| DELETE | /tasks/{id} | Delete task |
|
||||||
|
| PATCH | /tasks/{id}/toggle | Toggle task status |
|
||||||
|
| POST | /tasks/{id}/reschedule | Reschedule task |
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
Interactive API documentation is available at:
|
||||||
|
- Swagger UI: `https://your-domain.com/docs`
|
||||||
|
- ReDoc: `https://your-domain.com/redoc`
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks/deployment/docker
|
||||||
|
|
||||||
|
# All services
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker compose logs -f backend
|
||||||
|
docker compose logs -f nginx
|
||||||
|
docker compose logs -f db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks/deployment/scripts
|
||||||
|
./backup-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups are stored in `/opt/agenda-tasks/deployment/backups/` and automatically cleaned up after 7 days.
|
||||||
|
|
||||||
|
### Restore Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks/deployment/docker
|
||||||
|
|
||||||
|
# Stop backend
|
||||||
|
docker compose stop backend
|
||||||
|
|
||||||
|
# Restore from backup
|
||||||
|
gunzip < ../backups/agenda_tasks_YYYYMMDD_HHMMSS.sql.gz | \
|
||||||
|
docker compose exec -T db psql -U agenda agenda_tasks
|
||||||
|
|
||||||
|
# Start backend
|
||||||
|
docker compose start backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks
|
||||||
|
|
||||||
|
# Pull latest code
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Redeploy
|
||||||
|
cd deployment/scripts
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/agenda-tasks/deployment/docker
|
||||||
|
|
||||||
|
# Restart all
|
||||||
|
docker compose restart
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker compose restart backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] Strong database password (32+ characters)
|
||||||
|
- [ ] Strong secret key (32+ characters)
|
||||||
|
- [ ] SSL certificate installed
|
||||||
|
- [ ] Firewall configured (ports 80, 443 only)
|
||||||
|
- [ ] Regular backups enabled
|
||||||
|
- [ ] DEBUG=false in production
|
||||||
|
|
||||||
|
### Configure Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow SSH
|
||||||
|
ufw allow ssh
|
||||||
|
|
||||||
|
# Allow HTTP/HTTPS
|
||||||
|
ufw allow 80/tcp
|
||||||
|
ufw allow 443/tcp
|
||||||
|
|
||||||
|
# Enable firewall
|
||||||
|
ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flutter App Configuration
|
||||||
|
|
||||||
|
When building the Flutter app for production, specify the API URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Android APK
|
||||||
|
flutter build apk --dart-define=API_URL=https://your-domain.com
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
flutter build ios --dart-define=API_URL=https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Backend Not Starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker compose logs backend
|
||||||
|
|
||||||
|
# Common issues:
|
||||||
|
# - Database connection failed: Check DB_PASSWORD
|
||||||
|
# - Import errors: Rebuild with --no-cache
|
||||||
|
docker compose build --no-cache backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Certificate Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check DNS
|
||||||
|
dig your-domain.com
|
||||||
|
|
||||||
|
# DNS should point to your server IP
|
||||||
|
# Wait for propagation if recently changed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check database is running
|
||||||
|
docker compose ps db
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
docker compose logs db
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
docker compose exec db psql -U agenda -d agenda_tasks -c "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the logs first
|
||||||
|
2. Review this documentation
|
||||||
|
3. Contact the development team
|
||||||
32
deployment/docker/Dockerfile.backend
Normal file
32
deployment/docker/Dockerfile.backend
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY backend/app ./app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1
|
||||||
|
|
||||||
|
# Run with gunicorn
|
||||||
|
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]
|
||||||
72
deployment/docker/docker-compose.yml
Normal file
72
deployment/docker/docker-compose.yml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: agenda_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-agenda}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-agenda_tasks}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-agenda} -d ${DB_NAME:-agenda_tasks}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deployment/docker/Dockerfile.backend
|
||||||
|
container_name: agenda_backend
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${DB_USER:-agenda}:${DB_PASSWORD}@db:5432/${DB_NAME:-agenda_tasks}
|
||||||
|
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required}
|
||||||
|
DEBUG: ${DEBUG:-false}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- backend_network
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: agenda_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ../nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ../nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
- certbot_data:/etc/letsencrypt:ro
|
||||||
|
- certbot_www:/var/www/certbot:ro
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- backend_network
|
||||||
|
|
||||||
|
certbot:
|
||||||
|
image: certbot/certbot
|
||||||
|
container_name: agenda_certbot
|
||||||
|
volumes:
|
||||||
|
- certbot_data:/etc/letsencrypt
|
||||||
|
- certbot_www:/var/www/certbot
|
||||||
|
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
certbot_data:
|
||||||
|
certbot_www:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend_network:
|
||||||
|
driver: bridge
|
||||||
84
deployment/nginx/conf.d/default.conf
Normal file
84
deployment/nginx/conf.d/default.conf
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# HTTP redirect to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Let's Encrypt challenge location
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect all other traffic to HTTPS
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# SSL certificates (will be created by certbot)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
||||||
|
|
||||||
|
# SSL configuration
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:SSL:50m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
# Modern SSL configuration
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# HSTS
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
|
||||||
|
# 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 "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Auth endpoints with rate limiting
|
||||||
|
location ~ ^/auth/(login|register)$ {
|
||||||
|
limit_req zone=auth_limit burst=10 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
|
# API endpoints with rate limiting
|
||||||
|
location / {
|
||||||
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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_set_header Connection "";
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint (no rate limiting)
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://backend/docs;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
}
|
||||||
47
deployment/nginx/nginx.conf
Normal file
47
deployment/nginx/nginx.conf
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
use epoll;
|
||||||
|
multi_accept on;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
|
|
||||||
|
# Rate limiting zones
|
||||||
|
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;
|
||||||
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||||
|
|
||||||
|
# Upstream backend
|
||||||
|
upstream backend {
|
||||||
|
server backend:8000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include additional configs
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
58
deployment/scripts/backup-db.sh
Executable file
58
deployment/scripts/backup-db.sh
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOYMENT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
BACKUP_DIR="$DEPLOYMENT_DIR/backups"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
if [ -f "$DEPLOYMENT_DIR/.env" ]; then
|
||||||
|
source "$DEPLOYMENT_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
DB_USER="${DB_USER:-agenda}"
|
||||||
|
DB_NAME="${DB_NAME:-agenda_tasks}"
|
||||||
|
|
||||||
|
# Create backup directory
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Database Backup ===${NC}"
|
||||||
|
echo -e "${YELLOW}Database: $DB_NAME${NC}"
|
||||||
|
echo -e "${YELLOW}User: $DB_USER${NC}"
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
echo -e "${GREEN}Creating backup: $BACKUP_FILE${NC}"
|
||||||
|
|
||||||
|
cd "$DEPLOYMENT_DIR/docker"
|
||||||
|
|
||||||
|
docker compose exec -T db pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE"
|
||||||
|
|
||||||
|
if [ -f "$BACKUP_FILE" ]; then
|
||||||
|
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||||
|
echo -e "${GREEN}Backup created successfully!${NC}"
|
||||||
|
echo -e "${YELLOW}Size: $BACKUP_SIZE${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Backup failed!${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up old backups (keep last 7 days)
|
||||||
|
echo -e "${GREEN}Cleaning up old backups...${NC}"
|
||||||
|
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete
|
||||||
|
|
||||||
|
# List current backups
|
||||||
|
echo -e "${GREEN}Current backups:${NC}"
|
||||||
|
ls -lh "$BACKUP_DIR"/*.sql.gz 2>/dev/null || echo "No backups found"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Backup complete! ===${NC}"
|
||||||
81
deployment/scripts/deploy.sh
Executable file
81
deployment/scripts/deploy.sh
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOYMENT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
PROJECT_DIR="$(dirname "$DEPLOYMENT_DIR")"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Agenda Tasks Deployment ===${NC}"
|
||||||
|
|
||||||
|
# Check if .env file exists
|
||||||
|
if [ ! -f "$DEPLOYMENT_DIR/.env" ]; then
|
||||||
|
echo -e "${RED}Error: .env file not found!${NC}"
|
||||||
|
echo "Please copy .env.example to .env and configure it:"
|
||||||
|
echo " cp $DEPLOYMENT_DIR/.env.example $DEPLOYMENT_DIR/.env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
source "$DEPLOYMENT_DIR/.env"
|
||||||
|
|
||||||
|
# Validate required variables
|
||||||
|
required_vars=("DB_PASSWORD" "SECRET_KEY" "DOMAIN" "EMAIL")
|
||||||
|
for var in "${required_vars[@]}"; do
|
||||||
|
if [ -z "${!var}" ]; then
|
||||||
|
echo -e "${RED}Error: $var is not set in .env${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Domain: $DOMAIN${NC}"
|
||||||
|
echo -e "${YELLOW}Email: $EMAIL${NC}"
|
||||||
|
|
||||||
|
# Navigate to docker directory
|
||||||
|
cd "$DEPLOYMENT_DIR/docker"
|
||||||
|
|
||||||
|
# Pull latest images
|
||||||
|
echo -e "${GREEN}Pulling latest images...${NC}"
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# Build backend
|
||||||
|
echo -e "${GREEN}Building backend...${NC}"
|
||||||
|
docker compose build backend
|
||||||
|
|
||||||
|
# Stop existing containers
|
||||||
|
echo -e "${GREEN}Stopping existing containers...${NC}"
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
echo -e "${GREEN}Starting services...${NC}"
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Wait for services to be healthy
|
||||||
|
echo -e "${GREEN}Waiting for services to be healthy...${NC}"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
echo -e "${GREEN}Checking service status...${NC}"
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Check backend health
|
||||||
|
if docker compose exec -T backend python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')"; then
|
||||||
|
echo -e "${GREEN}Backend is healthy!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Backend health check failed!${NC}"
|
||||||
|
docker compose logs backend
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Deployment complete! ===${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Set up SSL with: ./setup-ssl.sh"
|
||||||
|
echo "2. Configure your DNS to point to this server"
|
||||||
|
echo "3. Access the API at: https://$DOMAIN"
|
||||||
174
deployment/scripts/setup-ssl.sh
Executable file
174
deployment/scripts/setup-ssl.sh
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOYMENT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== SSL Certificate Setup ===${NC}"
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
if [ ! -f "$DEPLOYMENT_DIR/.env" ]; then
|
||||||
|
echo -e "${RED}Error: .env file not found!${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$DEPLOYMENT_DIR/.env"
|
||||||
|
|
||||||
|
# Validate required variables
|
||||||
|
if [ -z "$DOMAIN" ] || [ -z "$EMAIL" ]; then
|
||||||
|
echo -e "${RED}Error: DOMAIN and EMAIL must be set in .env${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Domain: $DOMAIN${NC}"
|
||||||
|
echo -e "${YELLOW}Email: $EMAIL${NC}"
|
||||||
|
|
||||||
|
cd "$DEPLOYMENT_DIR/docker"
|
||||||
|
|
||||||
|
# Create a temporary nginx config for initial certificate request
|
||||||
|
echo -e "${GREEN}Creating temporary nginx config for certificate request...${NC}"
|
||||||
|
|
||||||
|
# Create temp config directory if it doesn't exist
|
||||||
|
mkdir -p "$DEPLOYMENT_DIR/nginx/conf.d"
|
||||||
|
|
||||||
|
# Create temporary HTTP-only config
|
||||||
|
cat > "$DEPLOYMENT_DIR/nginx/conf.d/default.conf" << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 200 'Waiting for SSL certificate...';
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Restart nginx with temporary config
|
||||||
|
echo -e "${GREEN}Restarting nginx with temporary config...${NC}"
|
||||||
|
docker compose up -d nginx
|
||||||
|
|
||||||
|
# Wait for nginx to start
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Request certificate
|
||||||
|
echo -e "${GREEN}Requesting SSL certificate from Let's Encrypt...${NC}"
|
||||||
|
|
||||||
|
docker compose run --rm certbot certonly \
|
||||||
|
--webroot \
|
||||||
|
--webroot-path=/var/www/certbot \
|
||||||
|
--email "$EMAIL" \
|
||||||
|
--agree-tos \
|
||||||
|
--no-eff-email \
|
||||||
|
-d "$DOMAIN"
|
||||||
|
|
||||||
|
# Check if certificate was created
|
||||||
|
if docker compose run --rm certbot certificates | grep -q "$DOMAIN"; then
|
||||||
|
echo -e "${GREEN}Certificate obtained successfully!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed to obtain certificate!${NC}"
|
||||||
|
echo "Please check:"
|
||||||
|
echo "1. DNS is pointing to this server"
|
||||||
|
echo "2. Port 80 is accessible from the internet"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore the full nginx config with SSL
|
||||||
|
echo -e "${GREEN}Restoring full nginx config with SSL...${NC}"
|
||||||
|
|
||||||
|
cat > "$DEPLOYMENT_DIR/nginx/conf.d/default.conf" << EOF
|
||||||
|
# HTTP redirect to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://\$host\$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
|
||||||
|
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:SSL:50m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
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 "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
location ~ ^/auth/(login|register)\$ {
|
||||||
|
limit_req zone=auth_limit burst=10 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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_set_header Connection "";
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://backend/docs;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
echo -e "${GREEN}Reloading nginx with SSL config...${NC}"
|
||||||
|
docker compose exec nginx nginx -s reload
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== SSL setup complete! ===${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Your API is now available at: https://$DOMAIN"
|
||||||
|
echo ""
|
||||||
|
echo "Certificate will auto-renew via the certbot container."
|
||||||
Loading…
x
Reference in New Issue
Block a user