voxblog/DEPLOYMENT_GUIDE.md
Ender f160b26564
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
feat: update service ports from 3000/3001 to 3300/3301
- Changed admin frontend port from 3000 to 3300 across all configuration files
- Changed API backend port from 3001 to 3301 across all configuration files
- Updated health check endpoints to use new ports in CI/CD workflow
- Modified documentation and deployment guides to reflect new port numbers
- Updated Caddy and Nginx reverse proxy configurations to use new ports
2025-10-26 00:25:40 +02:00

698 lines
16 KiB
Markdown

# VoxBlog Production Deployment Guide
## Overview
Complete CI/CD pipeline for deploying VoxBlog to your VPS with Gitea using Docker and Gitea Actions (similar to GitHub Actions).
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Your VPS Server │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Gitea │ │ Gitea Runner │ │ Docker │ │
│ │ Repository │→ │ (CI/CD) │→ │ Containers │ │
│ └────────────┘ └──────────────┘ └─────────────┘ │
│ ↓ │
│ ┌────────────────────────┐ │
│ │ voxblog-api:3301 │ │
│ │ voxblog-admin:3300 │ │
│ │ mysql:3306 │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Project Structure
```
voxblog/
├── apps/
│ ├── api/ # Backend (Express + TypeScript)
│ └── admin/ # Frontend (React + Vite)
├── packages/
│ └── config-ts/
├── .gitea/
│ └── workflows/
│ └── deploy.yml
├── docker/
│ ├── api.Dockerfile
│ ├── admin.Dockerfile
│ └── nginx.conf
├── docker-compose.yml
└── deploy.sh
```
## Step 1: Create Dockerfiles
### API Dockerfile
```dockerfile
# docker/api.Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy workspace files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json ./apps/api/
COPY packages/config-ts/package.json ./packages/config-ts/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY apps/api ./apps/api
COPY packages/config-ts ./packages/config-ts
# Build
WORKDIR /app/apps/api
RUN pnpm run build || echo "No build script, using ts-node"
# Production image
FROM node:18-alpine
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json ./apps/api/
COPY packages/config-ts/package.json ./packages/config-ts/
# Install production dependencies only
RUN pnpm install --frozen-lockfile --prod
# Copy built app
COPY --from=builder /app/apps/api ./apps/api
COPY --from=builder /app/packages/config-ts ./packages/config-ts
WORKDIR /app/apps/api
EXPOSE 3301
CMD ["pnpm", "run", "dev"]
```
### Admin Dockerfile
```dockerfile
# docker/admin.Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy workspace files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/admin/package.json ./apps/admin/
COPY packages/config-ts/package.json ./packages/config-ts/
# Install pnpm
RUN npm install -g pnpm
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY apps/admin ./apps/admin
COPY packages/config-ts ./packages/config-ts
# Build
WORKDIR /app/apps/admin
RUN pnpm run build
# Production image with nginx
FROM nginx:alpine
# Copy built files
COPY --from=builder /app/apps/admin/dist /usr/share/nginx/html
# Copy nginx config
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### Nginx Config
```nginx
# docker/nginx.conf
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;
# SPA routing - all routes go to index.html
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";
}
# 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;
}
```
## Step 2: Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: voxblog-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: voxblog
MYSQL_USER: voxblog
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
networks:
- voxblog-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
api:
build:
context: .
dockerfile: docker/api.Dockerfile
container_name: voxblog-api
restart: unless-stopped
ports:
- "3301:3301"
environment:
NODE_ENV: production
PORT: 3301
DATABASE_URL: mysql://voxblog:${MYSQL_PASSWORD}@mysql:3306/voxblog
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
OPENAI_API_KEY: ${OPENAI_API_KEY}
GHOST_ADMIN_API_KEY: ${GHOST_ADMIN_API_KEY}
S3_BUCKET: ${S3_BUCKET}
S3_REGION: ${S3_REGION}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_ENDPOINT: ${S3_ENDPOINT}
depends_on:
mysql:
condition: service_healthy
networks:
- voxblog-network
volumes:
- ./data:/app/data
admin:
build:
context: .
dockerfile: docker/admin.Dockerfile
args:
VITE_API_URL: ${VITE_API_URL:-http://localhost:3301}
container_name: voxblog-admin
restart: unless-stopped
ports:
- "3300:80"
networks:
- voxblog-network
depends_on:
- api
networks:
voxblog-network:
driver: bridge
volumes:
mysql_data:
```
## Step 3: Gitea Actions Workflow
```yaml
# .gitea/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Create .env file
run: |
cat > .env << EOF
MYSQL_ROOT_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }}
MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}
GHOST_ADMIN_API_KEY=${{ secrets.GHOST_ADMIN_API_KEY }}
S3_BUCKET=${{ secrets.S3_BUCKET }}
S3_REGION=${{ secrets.S3_REGION }}
S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }}
S3_ENDPOINT=${{ secrets.S3_ENDPOINT }}
VITE_API_URL=${{ secrets.VITE_API_URL }}
EOF
- name: Build and deploy
run: |
docker-compose down
docker-compose build --no-cache
docker-compose up -d
- name: Run database migrations
run: |
docker-compose exec -T api pnpm run drizzle:migrate
- name: Health check
run: |
sleep 10
curl -f http://localhost:3301/api/health || exit 1
curl -f http://localhost:3300 || exit 1
- name: Clean up old images
run: |
docker image prune -af --filter "until=24h"
```
## Step 4: Deployment Script (Alternative to Gitea Actions)
If Gitea Actions is not available, use a webhook + script approach:
```bash
#!/bin/bash
# deploy.sh
set -e
echo "🚀 Starting deployment..."
# Pull latest code
echo "📥 Pulling latest code..."
git pull origin main
# Create .env if not exists
if [ ! -f .env ]; then
echo "⚠️ .env file not found! Please create it from .env.example"
exit 1
fi
# Stop existing containers
echo "🛑 Stopping existing containers..."
docker-compose down
# Build new images
echo "🔨 Building new images..."
docker-compose build --no-cache
# Start containers
echo "▶️ Starting containers..."
docker-compose up -d
# Wait for services to be ready
echo "⏳ Waiting for services..."
sleep 10
# Run migrations
echo "🗄️ Running database migrations..."
docker-compose exec -T api pnpm run drizzle:migrate
# Health check
echo "🏥 Health check..."
if curl -f http://localhost:3301/api/health; then
echo "✅ API is healthy"
else
echo "❌ API health check failed"
exit 1
fi
if curl -f http://localhost:3300; then
echo "✅ Admin is healthy"
else
echo "❌ Admin health check failed"
exit 1
fi
# Clean up
echo "🧹 Cleaning up old images..."
docker image prune -af --filter "until=24h"
echo "✅ Deployment complete!"
```
Make it executable:
```bash
chmod +x deploy.sh
```
## Step 5: Gitea Webhook Setup
### Option A: Using Gitea Actions (Recommended)
1. **Install Gitea Runner on your VPS:**
```bash
# Download Gitea Runner
wget https://dl.gitea.com/act_runner/latest/act_runner-latest-linux-amd64
chmod +x act_runner-latest-linux-amd64
sudo mv act_runner-latest-linux-amd64 /usr/local/bin/act_runner
# Register runner
act_runner register --instance https://your-gitea-url --token YOUR_RUNNER_TOKEN
# Run as service
sudo tee /etc/systemd/system/gitea-runner.service > /dev/null <<EOF
[Unit]
Description=Gitea Actions Runner
After=network.target
[Service]
Type=simple
User=git
WorkingDirectory=/home/git
ExecStart=/usr/local/bin/act_runner daemon
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable gitea-runner
sudo systemctl start gitea-runner
```
2. **Add secrets in Gitea:**
- Go to your repository → Settings → Secrets
- Add all environment variables as secrets
### Option B: Using Webhook + Script
1. **Create webhook endpoint:**
```bash
# Install webhook listener
sudo apt-get install webhook
# Create webhook config
sudo tee /etc/webhook.conf > /dev/null <<EOF
[
{
"id": "voxblog-deploy",
"execute-command": "/path/to/voxblog/deploy.sh",
"command-working-directory": "/path/to/voxblog",
"response-message": "Deployment started",
"trigger-rule": {
"match": {
"type": "payload-hash-sha256",
"secret": "YOUR_WEBHOOK_SECRET",
"parameter": {
"source": "header",
"name": "X-Gitea-Signature"
}
}
}
}
]
EOF
# Run webhook as service
sudo tee /etc/systemd/system/webhook.service > /dev/null <<EOF
[Unit]
Description=Webhook Service
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/webhook -hooks /etc/webhook.conf -verbose
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable webhook
sudo systemctl start webhook
```
2. **Configure Gitea webhook:**
- Repository → Settings → Webhooks → Add Webhook
- URL: `http://your-vps:9000/hooks/voxblog-deploy`
- Secret: `YOUR_WEBHOOK_SECRET`
- Trigger: Push events on main branch
## Step 6: Reverse Proxy (Nginx)
```nginx
# /etc/nginx/sites-available/voxblog
server {
listen 80;
server_name voxblog.yourdomain.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name voxblog.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/voxblog.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/voxblog.yourdomain.com/privkey.pem;
# Admin frontend
location / {
proxy_pass http://localhost:3300;
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;
}
# API backend
location /api {
proxy_pass http://localhost:3301;
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;
# Increase timeout for AI streaming
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
```
Enable site:
```bash
sudo ln -s /etc/nginx/sites-available/voxblog /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## Step 7: SSL Certificate
```bash
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d voxblog.yourdomain.com
# Auto-renewal is set up automatically
```
## Step 8: Environment Variables
Create `.env` on your VPS:
```bash
cd /path/to/voxblog
cp .env.example .env
nano .env
```
Fill in all values from `.env.example`.
## Step 9: Initial Deployment
```bash
# Clone repository
cd /var/www # or your preferred location
git clone https://your-gitea-url/your-username/voxblog.git
cd voxblog
# Create .env file
cp .env.example .env
nano .env # Fill in values
# Initial deployment
./deploy.sh
```
## Step 10: Monitoring & Logs
```bash
# View logs
docker-compose logs -f
# View specific service
docker-compose logs -f api
docker-compose logs -f admin
# Check status
docker-compose ps
# Restart services
docker-compose restart api
docker-compose restart admin
```
## Deployment Workflow
```
Developer pushes to main
Gitea detects push
Triggers Gitea Actions / Webhook
Runs deploy.sh or workflow
1. Pull latest code
2. Build Docker images
3. Stop old containers
4. Start new containers
5. Run migrations
6. Health check
7. Clean up
Deployment complete! ✅
```
## Rollback Strategy
```bash
# View recent images
docker images | grep voxblog
# Rollback to previous version
docker tag voxblog-api:latest voxblog-api:backup
docker tag voxblog-api:previous voxblog-api:latest
docker-compose up -d
# Or use git
git log --oneline
git checkout <previous-commit-hash>
./deploy.sh
```
## Best Practices
1. **Always test locally first:**
```bash
docker-compose up --build
```
2. **Use health checks** in docker-compose.yml
3. **Backup database regularly:**
```bash
docker-compose exec mysql mysqldump -u voxblog -p voxblog > backup.sql
```
4. **Monitor disk space:**
```bash
docker system df
docker system prune -a
```
5. **Use secrets management** - never commit `.env` to git
6. **Set up monitoring** (optional):
- Portainer for Docker management
- Grafana + Prometheus for metrics
- Uptime Kuma for uptime monitoring
## Troubleshooting
### Container won't start
```bash
docker-compose logs api
docker-compose exec api sh # Debug inside container
```
### Database connection issues
```bash
docker-compose exec mysql mysql -u voxblog -p
# Check if database exists
SHOW DATABASES;
```
### Port already in use
```bash
sudo lsof -i :3301
sudo kill -9 <PID>
```
### Out of disk space
```bash
docker system prune -a --volumes
```
## Security Checklist
- [ ] Use strong passwords in `.env`
- [ ] Enable firewall (ufw)
- [ ] Keep Docker updated
- [ ] Use SSL/TLS (HTTPS)
- [ ] Limit SSH access
- [ ] Regular backups
- [ ] Monitor logs for suspicious activity
- [ ] Use Docker secrets for sensitive data (advanced)
## Next Steps
1. Create all Docker files
2. Set up Gitea Runner or webhook
3. Configure environment variables
4. Test deployment locally
5. Deploy to production
6. Set up monitoring
7. Configure backups
---
**Status**: Ready for production deployment! 🚀