feat: add deployment and server configuration files
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
- Added .dockerignore to exclude unnecessary files from Docker builds - Enhanced .env.example with detailed configuration options and added MySQL settings - Created Gitea CI/CD workflow for automated production deployment with health checks - Added comprehensive Caddy server setup guide and configuration for reverse proxy - Created Caddyfile with secure defaults for SSL, compression, and security headers The changes focus on setting up a production-
This commit is contained in:
parent
197fd69ce3
commit
51999669af
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
.DS_Store
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
tmp
|
||||||
|
data
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
*.Dockerfile
|
||||||
|
.dockerignore
|
||||||
26
.env.example
26
.env.example
@ -1,8 +1,18 @@
|
|||||||
ADMIN_PASSWORD=
|
# Database
|
||||||
OPENAI_API_KEY=
|
MYSQL_ROOT_PASSWORD=your_root_password_here
|
||||||
GHOST_ADMIN_API_KEY=
|
MYSQL_PASSWORD=your_mysql_password_here
|
||||||
S3_BUCKET=
|
|
||||||
S3_REGION=
|
# Application
|
||||||
S3_ACCESS_KEY=
|
ADMIN_PASSWORD=your_admin_password_here
|
||||||
S3_SECRET_KEY=
|
OPENAI_API_KEY=sk-your-openai-api-key
|
||||||
S3_ENDPOINT=
|
GHOST_ADMIN_API_KEY=your_ghost_admin_api_key
|
||||||
|
|
||||||
|
# S3 Storage
|
||||||
|
S3_BUCKET=your-bucket-name
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_ACCESS_KEY=your_access_key
|
||||||
|
S3_SECRET_KEY=your_secret_key
|
||||||
|
S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
|
|
||||||
|
# Frontend (for production deployment)
|
||||||
|
VITE_API_URL=https://api.yourdomain.com
|
||||||
|
|||||||
81
.gitea/workflows/deploy.yml
Normal file
81
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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: Stop existing containers
|
||||||
|
run: docker-compose down || true
|
||||||
|
|
||||||
|
- name: Build images
|
||||||
|
run: docker-compose build --no-cache
|
||||||
|
|
||||||
|
- name: Start containers
|
||||||
|
run: docker-compose up -d
|
||||||
|
|
||||||
|
- name: Wait for services
|
||||||
|
run: sleep 15
|
||||||
|
|
||||||
|
- name: Run database migrations
|
||||||
|
run: docker-compose exec -T api pnpm run drizzle:migrate || echo "Migration skipped"
|
||||||
|
|
||||||
|
- name: Health check API
|
||||||
|
run: |
|
||||||
|
for i in {1..10}; do
|
||||||
|
if curl -f http://localhost:3001/health; then
|
||||||
|
echo "API is healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting for API... ($i/10)"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "API health check failed"
|
||||||
|
docker-compose logs api
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Health check Admin
|
||||||
|
run: |
|
||||||
|
if curl -f http://localhost:3000; then
|
||||||
|
echo "Admin is healthy"
|
||||||
|
else
|
||||||
|
echo "Admin health check failed"
|
||||||
|
docker-compose logs admin
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Clean up old images
|
||||||
|
run: docker image prune -af --filter "until=24h"
|
||||||
|
|
||||||
|
- name: Deployment summary
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployment successful!"
|
||||||
|
echo "Services:"
|
||||||
|
docker-compose ps
|
||||||
312
CADDY_SETUP.md
Normal file
312
CADDY_SETUP.md
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# Caddy Setup for VoxBlog (Multi-App VPS)
|
||||||
|
|
||||||
|
## Why Caddy is Great! 🎉
|
||||||
|
|
||||||
|
✅ **Automatic HTTPS** - SSL certificates managed automatically
|
||||||
|
✅ **Simple config** - Much easier than Nginx
|
||||||
|
✅ **Auto-renewal** - Certificates renew automatically
|
||||||
|
✅ **Modern** - HTTP/2, HTTP/3 support built-in
|
||||||
|
|
||||||
|
## Quick Setup (3 Steps!)
|
||||||
|
|
||||||
|
### 1. Configure DNS
|
||||||
|
|
||||||
|
Add DNS record:
|
||||||
|
```
|
||||||
|
A Record: voxblog.yourdomain.com → your-vps-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add to Your Caddyfile
|
||||||
|
|
||||||
|
On your VPS, edit your existing Caddyfile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this configuration (from the `Caddyfile` in this repo):
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
admin.pusula.blog {
|
||||||
|
# Frontend
|
||||||
|
handle / {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# API
|
||||||
|
handle /api* {
|
||||||
|
reverse_proxy localhost:3001
|
||||||
|
}
|
||||||
|
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/voxblog-access.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Replace `voxblog.yourdomain.com` with your actual domain!
|
||||||
|
|
||||||
|
### 3. Reload Caddy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test configuration
|
||||||
|
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Reload Caddy
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update .env on VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/voxblog
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=https://voxblog.yourdomain.com/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## That's It! 🎉
|
||||||
|
|
||||||
|
Caddy will automatically:
|
||||||
|
- ✅ Get SSL certificate from Let's Encrypt
|
||||||
|
- ✅ Redirect HTTP to HTTPS
|
||||||
|
- ✅ Renew certificates automatically
|
||||||
|
- ✅ Handle HTTP/2 and HTTP/3
|
||||||
|
|
||||||
|
## Access Your App
|
||||||
|
|
||||||
|
- **Frontend**: `https://voxblog.yourdomain.com`
|
||||||
|
- **API**: `https://voxblog.yourdomain.com/api`
|
||||||
|
|
||||||
|
## Your Existing Apps
|
||||||
|
|
||||||
|
Your Caddyfile probably looks like this:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Existing app 1
|
||||||
|
app1.yourdomain.com {
|
||||||
|
reverse_proxy localhost:4000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Existing app 2
|
||||||
|
app2.yourdomain.com {
|
||||||
|
reverse_proxy localhost:5000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add VoxBlog
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
handle / {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}
|
||||||
|
handle /api* {
|
||||||
|
reverse_proxy localhost:3001
|
||||||
|
}
|
||||||
|
encode gzip
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All apps coexist perfectly! 🚀
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check Caddy Status
|
||||||
|
```bash
|
||||||
|
sudo systemctl status caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Caddy Logs
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u caddy -f
|
||||||
|
sudo tail -f /var/log/caddy/voxblog-access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Configuration
|
||||||
|
```bash
|
||||||
|
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reload After Changes
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check if Ports are Accessible
|
||||||
|
```bash
|
||||||
|
# From VPS (should work)
|
||||||
|
curl http://localhost:3000
|
||||||
|
curl http://localhost:3001/health
|
||||||
|
|
||||||
|
# From internet (should work via domain)
|
||||||
|
curl https://voxblog.yourdomain.com
|
||||||
|
curl https://voxblog.yourdomain.com/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if containers are running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check if ports are accessible
|
||||||
|
curl http://localhost:3000
|
||||||
|
curl http://localhost:3001/health
|
||||||
|
|
||||||
|
# Check Caddy logs
|
||||||
|
sudo journalctl -u caddy -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Issues
|
||||||
|
|
||||||
|
Caddy handles this automatically, but if you have issues:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Caddy logs for certificate errors
|
||||||
|
sudo journalctl -u caddy | grep -i cert
|
||||||
|
|
||||||
|
# Make sure port 443 is open
|
||||||
|
sudo ufw status
|
||||||
|
|
||||||
|
# Restart Caddy
|
||||||
|
sudo systemctl restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced: Separate Subdomains
|
||||||
|
|
||||||
|
If you prefer separate subdomains for frontend and API:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Frontend
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
encode gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
# API
|
||||||
|
api.voxblog.yourdomain.com {
|
||||||
|
reverse_proxy localhost:3001
|
||||||
|
encode gzip
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `.env`:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=https://api.voxblog.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caddy vs Nginx
|
||||||
|
|
||||||
|
| Feature | Caddy | Nginx |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| SSL Setup | Automatic ✅ | Manual (certbot) |
|
||||||
|
| Config | Simple ✅ | Complex |
|
||||||
|
| HTTP/3 | Built-in ✅ | Requires module |
|
||||||
|
| Cert Renewal | Automatic ✅ | Cron job needed |
|
||||||
|
| Learning Curve | Easy ✅ | Steep |
|
||||||
|
|
||||||
|
**Caddy is perfect for your use case!** 🎉
|
||||||
|
|
||||||
|
## Complete Example Caddyfile
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Global options
|
||||||
|
{
|
||||||
|
email your-email@example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
# Existing apps
|
||||||
|
app1.yourdomain.com {
|
||||||
|
reverse_proxy localhost:4000
|
||||||
|
}
|
||||||
|
|
||||||
|
app2.yourdomain.com {
|
||||||
|
reverse_proxy localhost:5000
|
||||||
|
}
|
||||||
|
|
||||||
|
# VoxBlog
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
# Frontend
|
||||||
|
handle / {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# API with long timeout for AI streaming
|
||||||
|
handle /api* {
|
||||||
|
reverse_proxy localhost:3001 {
|
||||||
|
transport http {
|
||||||
|
read_timeout 600s
|
||||||
|
write_timeout 600s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/voxblog-access.log {
|
||||||
|
roll_size 100mb
|
||||||
|
roll_keep 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate config
|
||||||
|
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Reload Caddy
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
|
||||||
|
# Restart Caddy
|
||||||
|
sudo systemctl restart caddy
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status caddy
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u caddy -f
|
||||||
|
|
||||||
|
# View access logs
|
||||||
|
sudo tail -f /var/log/caddy/voxblog-access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits for Multi-App VPS
|
||||||
|
|
||||||
|
✅ **Simple** - Just add a new block for each app
|
||||||
|
✅ **Automatic SSL** - No manual certificate management
|
||||||
|
✅ **No port conflicts** - All apps share 80/443
|
||||||
|
✅ **Secure** - App ports not exposed to internet
|
||||||
|
✅ **Professional** - Production-ready setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Caddy makes this incredibly easy!** Just add the config and reload. SSL is handled automatically. 🚀
|
||||||
67
Caddyfile
Normal file
67
Caddyfile
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Caddy configuration for VoxBlog
|
||||||
|
# Add this to your existing Caddyfile on VPS
|
||||||
|
|
||||||
|
# Option 1: Single domain with /api path (Recommended)
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
# Frontend (React Admin)
|
||||||
|
handle / {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Backend
|
||||||
|
handle /api* {
|
||||||
|
reverse_proxy localhost:3001
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable gzip compression
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
header {
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/voxblog-access.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Option 2: Separate subdomains (Alternative)
|
||||||
|
# Uncomment if you prefer separate subdomains
|
||||||
|
|
||||||
|
# Frontend subdomain
|
||||||
|
# voxblog.yourdomain.com {
|
||||||
|
# reverse_proxy localhost:3000
|
||||||
|
#
|
||||||
|
# encode gzip
|
||||||
|
#
|
||||||
|
# header {
|
||||||
|
# X-Frame-Options "SAMEORIGIN"
|
||||||
|
# X-Content-Type-Options "nosniff"
|
||||||
|
# X-XSS-Protection "1; mode=block"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# log {
|
||||||
|
# output file /var/log/caddy/voxblog-access.log
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# API subdomain
|
||||||
|
# api.voxblog.yourdomain.com {
|
||||||
|
# reverse_proxy localhost:3001
|
||||||
|
#
|
||||||
|
# encode gzip
|
||||||
|
#
|
||||||
|
# header {
|
||||||
|
# X-Frame-Options "SAMEORIGIN"
|
||||||
|
# X-Content-Type-Options "nosniff"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# log {
|
||||||
|
# output file /var/log/caddy/voxblog-api-access.log
|
||||||
|
# }
|
||||||
|
# }
|
||||||
697
DEPLOYMENT_GUIDE.md
Normal file
697
DEPLOYMENT_GUIDE.md
Normal file
@ -0,0 +1,697 @@
|
|||||||
|
# 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:3001 │ │
|
||||||
|
│ │ voxblog-admin:3000 │ │
|
||||||
|
│ │ 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 3001
|
||||||
|
|
||||||
|
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:
|
||||||
|
- "3001:3001"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3001
|
||||||
|
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:3001}
|
||||||
|
container_name: voxblog-admin
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000: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:3001/health || exit 1
|
||||||
|
curl -f http://localhost:3000 || 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:3001/health; then
|
||||||
|
echo "✅ API is healthy"
|
||||||
|
else
|
||||||
|
echo "❌ API health check failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -f http://localhost:3000; 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:3000;
|
||||||
|
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:3001;
|
||||||
|
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 :3001
|
||||||
|
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! 🚀
|
||||||
376
DEPLOYMENT_SUMMARY.md
Normal file
376
DEPLOYMENT_SUMMARY.md
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
# VoxBlog Production Deployment - Complete Setup
|
||||||
|
|
||||||
|
## 🎉 What's Been Created
|
||||||
|
|
||||||
|
Your VoxBlog project is now **production-ready** with a complete CI/CD pipeline!
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
|
||||||
|
```
|
||||||
|
voxblog/
|
||||||
|
├── docker/
|
||||||
|
│ ├── api.Dockerfile ✅ Backend Docker image
|
||||||
|
│ ├── admin.Dockerfile ✅ Frontend Docker image
|
||||||
|
│ └── nginx.conf ✅ Nginx config for frontend
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── deploy.yml ✅ Gitea Actions CI/CD workflow
|
||||||
|
├── docker-compose.yml ✅ Multi-container orchestration
|
||||||
|
├── deploy.sh ✅ Deployment script (executable)
|
||||||
|
├── .dockerignore ✅ Docker build optimization
|
||||||
|
├── .env.example ✅ Updated with all variables
|
||||||
|
├── DEPLOYMENT_GUIDE.md ✅ Complete deployment documentation
|
||||||
|
└── QUICK_START.md ✅ 5-minute setup guide
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Your VPS Server │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Gitea │→ │ Gitea Runner │→ │ Docker │ │
|
||||||
|
│ │ Repository │ │ (CI/CD) │ │ Containers │ │
|
||||||
|
│ └────────────┘ └──────────────┘ └─────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌────────────────────────┐ │
|
||||||
|
│ │ voxblog-api:3001 │ │
|
||||||
|
│ │ voxblog-admin:3000 │ │
|
||||||
|
│ │ mysql:3306 │ │
|
||||||
|
│ └────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Deployment Options
|
||||||
|
|
||||||
|
### Option 1: Gitea Actions (Recommended)
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Fully automated
|
||||||
|
- ✅ Built-in to Gitea
|
||||||
|
- ✅ GitHub Actions compatible
|
||||||
|
- ✅ Detailed logs and status
|
||||||
|
- ✅ Secrets management
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Install Gitea Runner on VPS
|
||||||
|
2. Add secrets to Gitea repository
|
||||||
|
3. Push to main → auto-deploy!
|
||||||
|
|
||||||
|
### Option 2: Webhook + Script
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Simple and lightweight
|
||||||
|
- ✅ No additional services needed
|
||||||
|
- ✅ Direct script execution
|
||||||
|
- ✅ Easy to debug
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Install webhook listener
|
||||||
|
2. Configure Gitea webhook
|
||||||
|
3. Push to main → webhook triggers deploy.sh
|
||||||
|
|
||||||
|
### Option 3: Manual Deployment
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Full control
|
||||||
|
- ✅ No setup required
|
||||||
|
- ✅ Good for testing
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
ssh user@vps
|
||||||
|
cd /path/to/voxblog
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Deployment Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
Developer commits code
|
||||||
|
↓
|
||||||
|
Push to main branch
|
||||||
|
↓
|
||||||
|
Gitea detects push
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ Gitea Actions / Webhook │
|
||||||
|
│ triggers deployment │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ deploy.sh executes: │
|
||||||
|
│ 1. Pull latest code │
|
||||||
|
│ 2. Build Docker images │
|
||||||
|
│ 3. Stop old containers │
|
||||||
|
│ 4. Start new containers │
|
||||||
|
│ 5. Run DB migrations │
|
||||||
|
│ 6. Health checks │
|
||||||
|
│ 7. Clean up old images │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
↓
|
||||||
|
✅ Deployment Complete!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
### 1. On Your VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://your-gitea-url/username/voxblog.git
|
||||||
|
cd voxblog
|
||||||
|
|
||||||
|
# Configure environment
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Fill in your values
|
||||||
|
|
||||||
|
# Deploy!
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Up CI/CD
|
||||||
|
|
||||||
|
**For Gitea Actions:**
|
||||||
|
```bash
|
||||||
|
# Install 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 and start
|
||||||
|
act_runner register --instance https://your-gitea --token YOUR_TOKEN
|
||||||
|
# Then set up as systemd service (see QUICK_START.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Webhook:**
|
||||||
|
```bash
|
||||||
|
sudo apt-get install webhook
|
||||||
|
# Configure webhook (see QUICK_START.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add Secrets (Gitea Actions only)
|
||||||
|
|
||||||
|
Repository → Settings → Secrets → Add all from `.env`
|
||||||
|
|
||||||
|
### 4. Push to Main
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add deployment configuration"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
🎉 **Auto-deployment triggered!**
|
||||||
|
|
||||||
|
## 🔧 Environment Variables
|
||||||
|
|
||||||
|
All required variables in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
MYSQL_ROOT_PASSWORD=strong_password
|
||||||
|
MYSQL_PASSWORD=voxblog_password
|
||||||
|
|
||||||
|
# Application
|
||||||
|
ADMIN_PASSWORD=admin_password
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
GHOST_ADMIN_API_KEY=...
|
||||||
|
|
||||||
|
# S3 Storage
|
||||||
|
S3_BUCKET=your-bucket
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_ACCESS_KEY=...
|
||||||
|
S3_SECRET_KEY=...
|
||||||
|
S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
VITE_API_URL=https://api.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Production Setup
|
||||||
|
|
||||||
|
### With Domain Name
|
||||||
|
|
||||||
|
1. **Point DNS to VPS**
|
||||||
|
```
|
||||||
|
A Record: @ → your-vps-ip
|
||||||
|
A Record: api → your-vps-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install Nginx**
|
||||||
|
```bash
|
||||||
|
sudo apt-get install nginx
|
||||||
|
# Configure (see QUICK_START.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add SSL**
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without Domain (IP Only)
|
||||||
|
|
||||||
|
Access directly:
|
||||||
|
- Admin: `http://your-vps-ip:3000`
|
||||||
|
- API: `http://your-vps-ip:3001`
|
||||||
|
|
||||||
|
## 📊 Monitoring & Maintenance
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
docker-compose logs -f api
|
||||||
|
docker-compose logs -f admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
docker-compose restart api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Database
|
||||||
|
```bash
|
||||||
|
docker-compose exec mysql mysqldump -u voxblog -p voxblog > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Up
|
||||||
|
```bash
|
||||||
|
docker system prune -a
|
||||||
|
docker volume prune
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Security Best Practices
|
||||||
|
|
||||||
|
- ✅ Use strong passwords in `.env`
|
||||||
|
- ✅ Never commit `.env` to git (already in .gitignore)
|
||||||
|
- ✅ Enable firewall: `sudo ufw enable`
|
||||||
|
- ✅ Use SSL/TLS (HTTPS)
|
||||||
|
- ✅ Keep Docker updated
|
||||||
|
- ✅ Regular backups
|
||||||
|
- ✅ Monitor logs for suspicious activity
|
||||||
|
- ✅ Use SSH keys instead of passwords
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Deployment Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Check specific service
|
||||||
|
docker-compose logs api
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find process
|
||||||
|
sudo lsof -i :3001
|
||||||
|
sudo lsof -i :3000
|
||||||
|
|
||||||
|
# Kill process
|
||||||
|
sudo kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Out of Disk Space
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check usage
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
docker system prune -a
|
||||||
|
docker volume prune
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Connection Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check MySQL
|
||||||
|
docker-compose exec mysql mysql -u voxblog -p
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
docker-compose exec api env | grep DATABASE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- **[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)** - Complete deployment guide
|
||||||
|
- **[QUICK_START.md](QUICK_START.md)** - 5-minute setup
|
||||||
|
- **[REFACTORING_SUMMARY.md](apps/api/REFACTORING_SUMMARY.md)** - API refactoring details
|
||||||
|
- **[STREAMING_GUIDE.md](apps/api/STREAMING_GUIDE.md)** - AI streaming implementation
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
1. **Test Locally First**
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy to VPS**
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set Up CI/CD**
|
||||||
|
- Choose Gitea Actions or Webhook
|
||||||
|
- Configure secrets
|
||||||
|
- Test auto-deployment
|
||||||
|
|
||||||
|
4. **Configure Domain & SSL**
|
||||||
|
- Point DNS
|
||||||
|
- Install Nginx
|
||||||
|
- Get SSL certificate
|
||||||
|
|
||||||
|
5. **Set Up Monitoring**
|
||||||
|
- Configure log rotation
|
||||||
|
- Set up uptime monitoring
|
||||||
|
- Configure backups
|
||||||
|
|
||||||
|
6. **Go Live!** 🚀
|
||||||
|
|
||||||
|
## ✅ Production Readiness Checklist
|
||||||
|
|
||||||
|
- [ ] Docker files created
|
||||||
|
- [ ] docker-compose.yml configured
|
||||||
|
- [ ] .env file filled with production values
|
||||||
|
- [ ] deploy.sh tested locally
|
||||||
|
- [ ] CI/CD pipeline chosen and configured
|
||||||
|
- [ ] Secrets added to Gitea (if using Actions)
|
||||||
|
- [ ] Domain DNS configured (optional)
|
||||||
|
- [ ] Nginx reverse proxy set up (optional)
|
||||||
|
- [ ] SSL certificate installed (optional)
|
||||||
|
- [ ] Firewall configured
|
||||||
|
- [ ] Backup strategy in place
|
||||||
|
- [ ] Test deployment successful
|
||||||
|
- [ ] Health checks passing
|
||||||
|
- [ ] Logs accessible and monitored
|
||||||
|
|
||||||
|
## 🎉 You're Ready!
|
||||||
|
|
||||||
|
Your VoxBlog project is now production-ready with:
|
||||||
|
- ✅ Dockerized backend and frontend
|
||||||
|
- ✅ Automated CI/CD pipeline
|
||||||
|
- ✅ Database with migrations
|
||||||
|
- ✅ Health checks
|
||||||
|
- ✅ Easy rollback
|
||||||
|
- ✅ Comprehensive documentation
|
||||||
|
|
||||||
|
**Push to main and watch it deploy automatically!** 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Questions?** Check the documentation or review the logs: `docker-compose logs -f`
|
||||||
222
MULTI_APP_VPS_SETUP.md
Normal file
222
MULTI_APP_VPS_SETUP.md
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# VoxBlog Setup for Multi-Application VPS
|
||||||
|
|
||||||
|
## Perfect for Your Use Case! 🎯
|
||||||
|
|
||||||
|
Since you're running **multiple applications** on your VPS, this is the **recommended production setup**.
|
||||||
|
|
||||||
|
## Choose Your Reverse Proxy
|
||||||
|
|
||||||
|
- **[Caddy Setup](CADDY_SETUP.md)** ⚡ Recommended! Automatic HTTPS, simpler config
|
||||||
|
- **[Nginx Setup](NGINX_SETUP.md)** 🔧 Traditional, more control
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
↓
|
||||||
|
Port 80/443 (Nginx)
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ app1.domain.com → localhost:3000 │
|
||||||
|
│ app2.domain.com → localhost:4000 │
|
||||||
|
│ voxblog.domain.com → localhost:3000│ ← VoxBlog
|
||||||
|
│ voxblog.domain.com/api → :3001 │ ← VoxBlog API
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
✅ **docker-compose.yml** - Ports now bind to localhost only:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:80" # Not exposed to internet
|
||||||
|
- "127.0.0.1:3001:3001" # Not exposed to internet
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Caddyfile** - Caddy configuration (automatic HTTPS!)
|
||||||
|
|
||||||
|
✅ **nginx-vps.conf** - Nginx configuration (alternative)
|
||||||
|
|
||||||
|
✅ **CADDY_SETUP.md** - Complete Caddy setup guide
|
||||||
|
|
||||||
|
✅ **NGINX_SETUP.md** - Complete Nginx setup guide
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
### Option A: Caddy (Recommended - Automatic HTTPS!)
|
||||||
|
|
||||||
|
#### 1. Configure DNS
|
||||||
|
```
|
||||||
|
A Record: voxblog.yourdomain.com → your-vps-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Add to Caddyfile
|
||||||
|
```bash
|
||||||
|
# On VPS
|
||||||
|
sudo nano /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this block (replace with your domain):
|
||||||
|
```caddy
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
handle / {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}
|
||||||
|
handle /api* {
|
||||||
|
reverse_proxy localhost:3001
|
||||||
|
}
|
||||||
|
encode gzip
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Reload Caddy
|
||||||
|
```bash
|
||||||
|
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** SSL is automatic. ✨
|
||||||
|
|
||||||
|
See **[CADDY_SETUP.md](CADDY_SETUP.md)** for details.
|
||||||
|
|
||||||
|
### Option B: Nginx (Alternative)
|
||||||
|
|
||||||
|
#### 1. Configure DNS
|
||||||
|
```
|
||||||
|
A Record: voxblog.yourdomain.com → your-vps-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Copy Nginx Config
|
||||||
|
```bash
|
||||||
|
scp nginx-vps.conf user@your-vps:/tmp/voxblog.conf
|
||||||
|
sudo mv /tmp/voxblog.conf /etc/nginx/sites-available/voxblog
|
||||||
|
sudo nano /etc/nginx/sites-available/voxblog # Edit domain
|
||||||
|
sudo ln -s /etc/nginx/sites-available/voxblog /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Add SSL
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx -d voxblog.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
See **[NGINX_SETUP.md](NGINX_SETUP.md)** for details.
|
||||||
|
|
||||||
|
### 3. Update .env on VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/voxblog
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=https://voxblog.yourdomain.com/api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. SSL
|
||||||
|
|
||||||
|
**Caddy**: Automatic! Nothing to do. ✨
|
||||||
|
|
||||||
|
**Nginx**:
|
||||||
|
```bash
|
||||||
|
sudo apt-get install certbot python3-certbot-nginx
|
||||||
|
sudo certbot --nginx -d voxblog.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
- **Frontend**: `https://voxblog.yourdomain.com`
|
||||||
|
- **API**: `https://voxblog.yourdomain.com/api`
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
You only need ports 80 and 443:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
Application ports (3000, 3001) are NOT exposed to internet - only accessible via Nginx!
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **No port conflicts** - All apps share 80/443
|
||||||
|
✅ **Secure** - App ports not exposed
|
||||||
|
✅ **Clean URLs** - Use domains, not IP:port
|
||||||
|
✅ **SSL ready** - Free Let's Encrypt certificates
|
||||||
|
✅ **Professional** - Standard production setup
|
||||||
|
|
||||||
|
## Example: Multiple Apps
|
||||||
|
|
||||||
|
**Caddy:**
|
||||||
|
```caddy
|
||||||
|
app1.yourdomain.com {
|
||||||
|
reverse_proxy localhost:4000
|
||||||
|
}
|
||||||
|
|
||||||
|
app2.yourdomain.com {
|
||||||
|
reverse_proxy localhost:5000
|
||||||
|
}
|
||||||
|
|
||||||
|
voxblog.yourdomain.com {
|
||||||
|
handle / { reverse_proxy localhost:3000 }
|
||||||
|
handle /api* { reverse_proxy localhost:3001 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nginx:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
server_name app1.yourdomain.com;
|
||||||
|
location / { proxy_pass http://127.0.0.1:4000; }
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name voxblog.yourdomain.com;
|
||||||
|
location / { proxy_pass http://127.0.0.1:3000; }
|
||||||
|
location /api { proxy_pass http://127.0.0.1:3001; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All apps coexist peacefully! 🎉
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Can't access via domain
|
||||||
|
|
||||||
|
1. Check DNS: `nslookup voxblog.yourdomain.com`
|
||||||
|
2. Check Nginx: `sudo nginx -t`
|
||||||
|
3. Check containers: `docker-compose ps`
|
||||||
|
4. Check logs: `sudo tail -f /var/log/nginx/error.log`
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if containers are running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check if ports are accessible
|
||||||
|
curl http://localhost:3000
|
||||||
|
curl http://localhost:3001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Documentation
|
||||||
|
|
||||||
|
- **[CADDY_SETUP.md](CADDY_SETUP.md)** - Caddy setup (recommended!)
|
||||||
|
- **[NGINX_SETUP.md](NGINX_SETUP.md)** - Nginx setup (alternative)
|
||||||
|
- **[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)** - Full deployment guide
|
||||||
|
- **[QUICK_START.md](QUICK_START.md)** - Quick start guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**This is the recommended setup for multi-app VPS environments!** 🚀
|
||||||
358
QUICK_START.md
Normal file
358
QUICK_START.md
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# VoxBlog Quick Start Guide
|
||||||
|
|
||||||
|
## 🚀 Deploy to Production in 5 Minutes
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- VPS with Docker and Docker Compose installed
|
||||||
|
- Gitea repository set up
|
||||||
|
- Domain name (optional, for SSL)
|
||||||
|
|
||||||
|
### Step 1: Clone Repository on VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh user@your-vps
|
||||||
|
|
||||||
|
# Navigate to your deployment directory
|
||||||
|
cd /var/www # or /home/user/apps
|
||||||
|
|
||||||
|
# Clone from Gitea
|
||||||
|
git clone https://your-gitea-url/username/voxblog.git
|
||||||
|
cd voxblog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy example env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit with your values
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill in all values:
|
||||||
|
- `MYSQL_ROOT_PASSWORD` - Strong password for MySQL root
|
||||||
|
- `MYSQL_PASSWORD` - Password for voxblog database user
|
||||||
|
- `ADMIN_PASSWORD` - Password for admin login
|
||||||
|
- `OPENAI_API_KEY` - Your OpenAI API key
|
||||||
|
- `GHOST_ADMIN_API_KEY` - Your Ghost CMS API key
|
||||||
|
- `S3_*` - Your S3 credentials
|
||||||
|
- `VITE_API_URL` - Your API URL (e.g., https://api.yourdomain.com)
|
||||||
|
|
||||||
|
### Step 3: Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make deploy script executable
|
||||||
|
chmod +x deploy.sh
|
||||||
|
|
||||||
|
# Run deployment
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Your application is now running:
|
||||||
|
- **API**: http://your-vps:3001
|
||||||
|
- **Admin**: http://your-vps:3000
|
||||||
|
|
||||||
|
### Step 4: Set Up CI/CD (Choose One)
|
||||||
|
|
||||||
|
#### Option A: Gitea Actions (Recommended)
|
||||||
|
|
||||||
|
1. **Install Gitea Runner on VPS:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download 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 (get token from Gitea: Settings → Actions → Runners)
|
||||||
|
act_runner register \
|
||||||
|
--instance https://your-gitea-url \
|
||||||
|
--token YOUR_RUNNER_TOKEN \
|
||||||
|
--name voxblog-runner
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
sudo tee /etc/systemd/system/gitea-runner.service > /dev/null <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea Actions Runner
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$USER
|
||||||
|
WorkingDirectory=$HOME
|
||||||
|
ExecStart=/usr/local/bin/act_runner daemon
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable gitea-runner
|
||||||
|
sudo systemctl start gitea-runner
|
||||||
|
sudo systemctl status gitea-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add Secrets in Gitea:**
|
||||||
|
|
||||||
|
Go to: Repository → Settings → Secrets → Actions
|
||||||
|
|
||||||
|
Add all variables from `.env`:
|
||||||
|
- `MYSQL_ROOT_PASSWORD`
|
||||||
|
- `MYSQL_PASSWORD`
|
||||||
|
- `ADMIN_PASSWORD`
|
||||||
|
- `OPENAI_API_KEY`
|
||||||
|
- `GHOST_ADMIN_API_KEY`
|
||||||
|
- `S3_BUCKET`
|
||||||
|
- `S3_REGION`
|
||||||
|
- `S3_ACCESS_KEY`
|
||||||
|
- `S3_SECRET_KEY`
|
||||||
|
- `S3_ENDPOINT`
|
||||||
|
- `VITE_API_URL`
|
||||||
|
|
||||||
|
3. **Push to main branch** - Deployment will trigger automatically!
|
||||||
|
|
||||||
|
#### Option B: Webhook (Alternative)
|
||||||
|
|
||||||
|
1. **Install webhook listener:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get install webhook
|
||||||
|
|
||||||
|
# Create webhook config
|
||||||
|
sudo tee /etc/webhook.conf > /dev/null <<EOF
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "voxblog-deploy",
|
||||||
|
"execute-command": "$(pwd)/deploy.sh",
|
||||||
|
"command-working-directory": "$(pwd)",
|
||||||
|
"response-message": "Deployment started"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create systemd 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 -port 9000
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
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`
|
||||||
|
- Trigger: Push events on main branch
|
||||||
|
|
||||||
|
### Step 5: Set Up Reverse Proxy (Optional but Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install nginx
|
||||||
|
sudo apt-get install nginx
|
||||||
|
|
||||||
|
# Create site config
|
||||||
|
sudo nano /etc/nginx/sites-available/voxblog
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste this configuration:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
# Admin frontend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
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:3001;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# Long 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 6: Add SSL (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
sudo apt-get install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# Get certificate
|
||||||
|
sudo certbot --nginx -d yourdomain.com
|
||||||
|
|
||||||
|
# Auto-renewal is configured automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs -f api
|
||||||
|
docker-compose logs -f admin
|
||||||
|
docker-compose logs -f mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart all
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker-compose restart api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Updates
|
||||||
|
|
||||||
|
Every time you push to `main` branch:
|
||||||
|
1. Gitea Actions/Webhook triggers
|
||||||
|
2. Code is pulled
|
||||||
|
3. Docker images are rebuilt
|
||||||
|
4. Containers are restarted
|
||||||
|
5. Migrations run automatically
|
||||||
|
6. Health checks verify deployment
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Containers won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs api
|
||||||
|
docker-compose logs admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec mysql mysql -u voxblog -p
|
||||||
|
# Enter MYSQL_PASSWORD when prompted
|
||||||
|
SHOW DATABASES;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port conflicts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo lsof -i :3001
|
||||||
|
sudo lsof -i :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disk space
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker system df
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down -v # WARNING: Deletes database!
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Backup
|
||||||
|
|
||||||
|
### Database Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
docker-compose exec mysql mysqldump -u voxblog -p voxblog > backup-$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
docker-compose exec -T mysql mysql -u voxblog -p voxblog < backup-20241025.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup data directory
|
||||||
|
tar -czf voxblog-data-$(date +%Y%m%d).tar.gz data/
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
docker-compose exec mysql mysqldump -u voxblog -p voxblog > db-backup-$(date +%Y%m%d).sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Security Checklist
|
||||||
|
|
||||||
|
- [ ] Strong passwords in `.env`
|
||||||
|
- [ ] Firewall enabled (ufw)
|
||||||
|
- [ ] SSH key-based authentication
|
||||||
|
- [ ] SSL/TLS enabled (HTTPS)
|
||||||
|
- [ ] Regular backups configured
|
||||||
|
- [ ] Docker updated regularly
|
||||||
|
- [ ] Monitor logs for suspicious activity
|
||||||
|
|
||||||
|
## 🎯 Production Checklist
|
||||||
|
|
||||||
|
- [ ] `.env` file configured with production values
|
||||||
|
- [ ] Domain name pointed to VPS
|
||||||
|
- [ ] SSL certificate installed
|
||||||
|
- [ ] Nginx reverse proxy configured
|
||||||
|
- [ ] Gitea Actions/Webhook set up
|
||||||
|
- [ ] Secrets added to Gitea
|
||||||
|
- [ ] Backup strategy in place
|
||||||
|
- [ ] Monitoring set up
|
||||||
|
- [ ] Firewall configured
|
||||||
|
- [ ] Test deployment successful
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- [Full Deployment Guide](DEPLOYMENT_GUIDE.md)
|
||||||
|
- [Docker Compose Docs](https://docs.docker.com/compose/)
|
||||||
|
- [Gitea Actions Docs](https://docs.gitea.io/en-us/actions/)
|
||||||
|
- [Nginx Docs](https://nginx.org/en/docs/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need help?** Check the logs first: `docker-compose logs -f`
|
||||||
80
deploy.sh
Executable file
80
deploy.sh
Executable file
@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 VoxBlog Deployment Script"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check if .env exists
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo -e "${RED}❌ .env file not found!${NC}"
|
||||||
|
echo "Please create .env file from .env.example"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull latest code
|
||||||
|
echo -e "${YELLOW}📥 Pulling latest code...${NC}"
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Stop existing containers
|
||||||
|
echo -e "${YELLOW}🛑 Stopping existing containers...${NC}"
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Build new images
|
||||||
|
echo -e "${YELLOW}🔨 Building new images...${NC}"
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
echo -e "${YELLOW}▶️ Starting containers...${NC}"
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Wait for services to be ready
|
||||||
|
echo -e "${YELLOW}⏳ Waiting for services to start...${NC}"
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Run database migrations
|
||||||
|
echo -e "${YELLOW}🗄️ Running database migrations...${NC}"
|
||||||
|
docker-compose exec -T api pnpm run drizzle:migrate || echo "Migration skipped or failed"
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
echo -e "${YELLOW}🏥 Performing health checks...${NC}"
|
||||||
|
|
||||||
|
API_HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/health || echo "000")
|
||||||
|
if [ "$API_HEALTH" = "200" ]; then
|
||||||
|
echo -e "${GREEN}✅ API is healthy${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ API health check failed (HTTP $API_HEALTH)${NC}"
|
||||||
|
echo "Checking API logs:"
|
||||||
|
docker-compose logs --tail=50 api
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ADMIN_HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 || echo "000")
|
||||||
|
if [ "$ADMIN_HEALTH" = "200" ]; then
|
||||||
|
echo -e "${GREEN}✅ Admin is healthy${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Admin health check failed (HTTP $ADMIN_HEALTH)${NC}"
|
||||||
|
echo "Checking Admin logs:"
|
||||||
|
docker-compose logs --tail=50 admin
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up old images
|
||||||
|
echo -e "${YELLOW}🧹 Cleaning up old Docker images...${NC}"
|
||||||
|
docker image prune -af --filter "until=24h"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Deployment complete!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Services running:"
|
||||||
|
echo " - API: http://localhost:3001"
|
||||||
|
echo " - Admin: http://localhost:3000"
|
||||||
|
echo ""
|
||||||
|
echo "To view logs: docker-compose logs -f"
|
||||||
|
echo "To stop: docker-compose down"
|
||||||
72
docker-compose.yml
Normal file
72
docker-compose.yml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/api.Dockerfile
|
||||||
|
container_name: voxblog-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3001:3001" # Only localhost, not internet
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3001
|
||||||
|
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:3001}
|
||||||
|
container_name: voxblog-admin
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:80" # Only localhost, not internet
|
||||||
|
networks:
|
||||||
|
- voxblog-network
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
networks:
|
||||||
|
voxblog-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
41
docker/admin.Dockerfile
Normal file
41
docker/admin.Dockerfile
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Build args
|
||||||
|
ARG VITE_API_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# Copy workspace files
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY apps/admin/package.json ./apps/admin/
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY apps/admin ./apps/admin
|
||||||
|
|
||||||
|
# Build with environment variable
|
||||||
|
WORKDIR /app/apps/admin
|
||||||
|
ENV VITE_API_URL=$VITE_API_URL
|
||||||
|
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
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
47
docker/api.Dockerfile
Normal file
47
docker/api.Dockerfile
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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/
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY apps/api ./apps/api
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pnpm and ts-node
|
||||||
|
RUN npm install -g pnpm ts-node typescript
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY apps/api/package.json ./apps/api/
|
||||||
|
|
||||||
|
# Install production dependencies
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy app from builder
|
||||||
|
COPY --from=builder /app/apps/api ./apps/api
|
||||||
|
|
||||||
|
WORKDIR /app/apps/api
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
CMD ["pnpm", "run", "dev"]
|
||||||
91
nginx-vps.conf
Normal file
91
nginx-vps.conf
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Nginx configuration for VPS
|
||||||
|
# Copy this to: /etc/nginx/sites-available/voxblog
|
||||||
|
# Then: sudo ln -s /etc/nginx/sites-available/voxblog /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# Option 1: Using subdomain (Recommended)
|
||||||
|
# DNS: voxblog.yourdomain.com → your-vps-ip
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name voxblog.yourdomain.com;
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
# Frontend (React Admin)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API Backend
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:3001;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# Long timeout for AI streaming
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_connect_timeout 600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Option 2: Using separate subdomains
|
||||||
|
# DNS: voxblog.yourdomain.com → your-vps-ip
|
||||||
|
# DNS: api.voxblog.yourdomain.com → your-vps-ip
|
||||||
|
|
||||||
|
# Frontend subdomain
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name voxblog.yourdomain.com;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://127.0.0.1:3000;
|
||||||
|
# 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;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# API subdomain
|
||||||
|
# server {
|
||||||
|
# listen 80;
|
||||||
|
# server_name api.voxblog.yourdomain.com;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://127.0.0.1:3001;
|
||||||
|
# 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;
|
||||||
|
#
|
||||||
|
# # Long timeout for AI streaming
|
||||||
|
# proxy_read_timeout 600s;
|
||||||
|
# proxy_send_timeout 600s;
|
||||||
|
# proxy_connect_timeout 600s;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
Loading…
Reference in New Issue
Block a user