05/31 Phase 1: initial codes

This commit is contained in:
2026-05-31 10:03:28 -04:00
parent b0a5ce5399
commit b0c0bd363b
35 changed files with 2871 additions and 164 deletions
+672
View File
@@ -0,0 +1,672 @@
# PFMS — Server Deployment Guide
> Ubuntu Server · Nginx · Gunicorn · MySQL · Groq API
---
## 0. Assumptions
| Item | Value |
|------|-------|
| OS | Ubuntu 22.04 LTS |
| App user | `pfm` |
| App directory | `/home/pfm/app` |
| Domain / IP | `pfm.ngodanguyen.tech` |
| SSL | Let's Encrypt via Certbot |
| MySQL version | 8.0 |
| Python | 3.11+ |
---
## 1. System Prep
```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv python3-dev \
build-essential libssl-dev libffi-dev \
nginx mysql-server git curl wget \
libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b \
libffi-dev libjpeg-dev libopenjp2-7-dev
```
> `libpango*` and related libs are required by WeasyPrint for PDF generation.
---
## 2. Create App User
```bash
# Skip if user pfm already exists
id pfm &>/dev/null || sudo useradd -m -s /bin/bash pfm
sudo usermod -aG www-data pfm
```
---
## 3. MySQL Setup
```bash
sudo mysql_secure_installation
# Follow prompts: set root password, remove anonymous users, disable remote root
```
```bash
sudo mysql -u root -p
```
```sql
CREATE DATABASE pfm_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pfm_user'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD_HERE';
GRANT ALL PRIVILEGES ON pfm_db.* TO 'pfm_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
---
## 4. App Directory & Virtual Environment
```bash
sudo mkdir -p /home/pfm/app
sudo chown pfm:www-data /home/pfm/app
sudo chmod 750 /home/pfm/app
sudo -u pfm bash
cd /home/pfm/app
python3 -m venv venv
source venv/bin/activate
# Clone from your Gitea
git clone https://gitea.yourdomain.com/youruser/pfm.git .
pip install --upgrade pip
pip install -r requirements.txt
```
---
## 5. Environment File
```bash
sudo -u pfm nano /home/pfm/app/.env
```
```env
SECRET_KEY=generate-a-long-random-string-here
DATABASE_URL=mysql+pymysql://pfm_user:STRONG_PASSWORD_HERE@localhost/pfm_db
GROQ_API_KEY=your-groq-api-key-here
GROQ_MODEL=llama-3.3-70b-versatile
UPLOAD_FOLDER=/home/pfm/app/uploads
MAX_CONTENT_LENGTH=10485760
FLASK_ENV=production
APP_CURRENCY=USD
APP_CURRENCY_SYMBOL=$
APP_TIMEZONE=Asia/Ho_Chi_Minh
```
```bash
chmod 600 /home/pfm/app/.env
```
Generate a secret key:
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```
---
## 6. Initialize Database
```bash
cd /home/pfm/app
source venv/bin/activate
flask db upgrade # Run all migrations
python scripts/init_db.py # Seed default categories + create admin user
```
---
## 7. Upload Directory
```bash
sudo mkdir -p /home/pfm/app/uploads
sudo chown pfm:www-data /home/pfm/app/uploads
sudo chmod 750 /home/pfm/app/uploads
```
---
## 8. Gunicorn Setup (systemd service)
```bash
sudo nano /etc/systemd/system/pfm.service
```
```ini
[Unit]
Description=PFMS — Personal Finance Management System
After=network.target mysql.service
[Service]
User=pfm
Group=www-data
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/gunicorn \
--workers 2 \
--bind unix:/run/pfm/pfm.sock \
--timeout 120 \
--access-logfile /var/log/pfm/access.log \
--error-logfile /var/log/pfm/error.log \
wsgi:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
RuntimeDirectory=pfm
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
```
```bash
# Create log directory
sudo mkdir -p /var/log/pfm
sudo chown pfm:www-data /var/log/pfm
sudo systemctl daemon-reload
sudo systemctl enable pfm
sudo systemctl start pfm
sudo systemctl status pfm
```
---
## 9. Nginx Configuration
```bash
sudo nano /etc/nginx/sites-available/pfm
```
```nginx
server {
listen 80;
server_name pfm.ngodanguyen.tech;
# Certbot will update this block automatically
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name pfm.ngodanguyen.tech;
# Certbot will populate these after running certbot
# ssl_certificate /etc/letsencrypt/live/pfm.ngodanguyen.tech/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/pfm.ngodanguyen.tech/privkey.pem;
# include /etc/letsencrypt/options-ssl-nginx.conf;
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 10M;
location / {
proxy_pass http://unix:/run/pfm/pfm.sock;
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 https;
proxy_read_timeout 120s;
proxy_connect_timeout 10s;
}
# AI chat uses SSE — disable buffering
location /ai/stream {
proxy_pass http://unix:/run/pfm/pfm.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Cache-Control no-cache;
proxy_set_header X-Accel-Buffering no;
proxy_buffering off;
proxy_read_timeout 300s;
chunked_transfer_encoding on;
}
location /static/ {
alias /home/pfm/app/app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /home/pfm/app/uploads/;
internal;
}
access_log /var/log/nginx/pfm_access.log;
error_log /var/log/nginx/pfm_error.log;
}
```
```bash
sudo ln -s /etc/nginx/sites-available/pfm /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
---
## 9a. SSL — Certbot (Let's Encrypt)
```bash
# Install Certbot + Nginx plugin
sudo apt install -y certbot python3-certbot-nginx
# Issue certificate — auto-edits the Nginx config above
sudo certbot --nginx -d pfm.ngodanguyen.tech
# Follow prompts:
# - Enter email for renewal notices
# - Agree to TOS
# - Choose redirect HTTP → HTTPS (recommended)
# Verify cert
sudo certbot certificates
```
Certbot automatically:
- Populates the `ssl_certificate` lines in the Nginx config
- Sets up HTTP → HTTPS redirect
- Installs a systemd timer for auto-renewal
```bash
# Test auto-renewal
sudo certbot renew --dry-run
# Check renewal timer
sudo systemctl status certbot.timer
```
---
## 10. Groq API Setup (Free AI)
No local installation needed. Groq runs in the cloud — just get a free API key.
### 10.1 Get API Key
1. Sign up at **https://console.groq.com**
2. Go to **API Keys****Create API key**
3. Copy the key into `/home/pfm/app/.env` as `GROQ_API_KEY`
### 10.2 Free Tier Limits
| Metric | Limit |
|--------|-------|
| Requests/day | 14,400 |
| Tokens/minute | 500,000 |
| Tokens/day | 500,000 (varies by model) |
| Cost | Free |
More than enough for a personal finance assistant.
### 10.3 Recommended Models
| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `llama-3.3-70b-versatile` | Fast | High | Default — chat + insights |
| `llama-3.1-8b-instant` | Very fast | Good | Quick queries, daily summaries |
Set in `.env` via `GROQ_MODEL`. Switchable from Settings UI.
### 10.4 Verify Key Works
```bash
curl https://api.groq.com/openai/v1/chat/completions -H "Authorization: Bearer YOUR_GROQ_API_KEY" -H "Content-Type: application/json" -d '{
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 10
}'
```
Should return a JSON response with a greeting.
---
## 11. Scheduled Jobs (systemd timers)
### 11.1 Process Recurring Transactions (Daily 6AM)
```bash
sudo nano /etc/systemd/system/pfm-recurring.service
```
```ini
[Unit]
Description=PFMS — Process Recurring Transactions
After=mysql.service
[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/python scripts/process_recurring.py
```
```bash
sudo nano /etc/systemd/system/pfm-recurring.timer
```
```ini
[Unit]
Description=PFMS — Recurring Transactions Timer
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
### 11.2 Fetch Investment Prices (Daily 4PM weekdays)
```bash
sudo nano /etc/systemd/system/pfm-prices.service
```
```ini
[Unit]
Description=PFMS — Fetch Investment Prices
[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/python scripts/fetch_prices.py
```
```bash
sudo nano /etc/systemd/system/pfm-prices.timer
```
```ini
[Unit]
Description=PFMS — Price Fetch Timer
[Timer]
OnCalendar=Mon-Fri *-*-* 16:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
### 11.3 Net Worth Snapshot (1st of month)
```bash
sudo nano /etc/systemd/system/pfm-snapshot.service
```
```ini
[Unit]
Description=PFMS — Net Worth Snapshot
[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/python scripts/daily_snapshot.py
```
```bash
sudo nano /etc/systemd/system/pfm-snapshot.timer
```
```ini
[Unit]
Description=PFMS — Net Worth Snapshot Timer
[Timer]
OnCalendar=*-*-01 00:05:00
Persistent=true
[Install]
WantedBy=timers.target
```
### 11.4 AI Daily Insight (Midnight)
```bash
sudo nano /etc/systemd/system/pfm-aiinsight.service
```
```ini
[Unit]
Description=PFMS — AI Daily Insight
After=network-online.target
[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/python scripts/daily_ai_insight.py
```
```bash
sudo nano /etc/systemd/system/pfm-aiinsight.timer
```
```ini
[Unit]
Description=PFMS — AI Insight Timer
[Timer]
OnCalendar=*-*-* 00:01:00
Persistent=true
[Install]
WantedBy=timers.target
```
### 11.5 Fetch USD/VND Rate (Daily 8AM)
```bash
sudo nano /etc/systemd/system/pfm-fxrate.service
```
```ini
[Unit]
Description=PFMS — Fetch USD/VND Exchange Rate
[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/app
Environment="PATH=/home/pfm/app/venv/bin"
EnvironmentFile=/home/pfm/app/.env
ExecStart=/home/pfm/app/venv/bin/python scripts/fetch_fx_rate.py
```
```bash
sudo nano /etc/systemd/system/pfm-fxrate.timer
```
```ini
[Unit]
Description=PFMS — FX Rate Fetch Timer
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
### Enable All Timers
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now pfm-recurring.timer
sudo systemctl enable --now pfm-fxrate.timer
sudo systemctl enable --now pfm-prices.timer
sudo systemctl enable --now pfm-snapshot.timer
sudo systemctl enable --now pfm-aiinsight.timer
# Verify
sudo systemctl list-timers --all | grep pfm
```
---
## 12. Log Rotation
```bash
sudo nano /etc/logrotate.d/pfm
```
```
/var/log/pfm/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 pfm www-data
postrotate
systemctl reload pfm
endscript
}
```
---
## 13. Database Backup (Daily)
```bash
sudo nano /etc/systemd/system/pfm-backup.service
```
```ini
[Unit]
Description=PFMS — MySQL Backup
[Service]
Type=oneshot
User=pfm
ExecStart=/bin/bash -c 'mysqldump --defaults-file=/home/pfm/.my.cnf pfm_db | gzip > /home/pfm/app/backups/pfm_$(date +\%Y\%m\%d).sql.gz'
```
```bash
sudo nano /etc/systemd/system/pfm-backup.timer
```
```ini
[Unit]
Description=PFMS — Daily Backup Timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
```bash
sudo mkdir -p /home/pfm/app/backups
sudo chown pfm:pfm /home/pfm/app/backups
sudo systemctl enable --now pfm-backup.timer
```
> Store MySQL credentials securely for the backup job:
> ```bash
> sudo -u pfm bash -c 'cat > /home/pfm/.my.cnf << EOF
> [client]
> user=pfm_user
> password=STRONG_PASSWORD_HERE
> EOF
> chmod 600 /home/pfm/.my.cnf'
> ```
> Keep last 30 days. Add a cleanup cron to delete older files:
> `find /home/pfm/app/backups -name "*.sql.gz" -mtime +30 -delete`
---
## 14. Firewall (UFW)
```bash
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
```
> MySQL (3306) is NOT opened — internal only. Groq calls go outbound over HTTPS (port 443).
---
## 15. Deploy Update Workflow
```bash
# SSH into server
sudo -u pfm bash
cd /home/pfm/app
source venv/bin/activate
git pull origin main
pip install -r requirements.txt # if deps changed
flask db upgrade # if migrations added
sudo systemctl restart pfm
sudo systemctl status pfm
```
---
## 16. Quick Status Check
```bash
# All PFMS services
sudo systemctl status pfm
sudo systemctl status nginx
sudo systemctl status mysql
# Logs (live)
sudo journalctl -u pfm -f
sudo tail -f /var/log/pfm/error.log
sudo tail -f /var/log/nginx/pfm_error.log
# Groq API check
curl https://api.groq.com/openai/v1/models \
-H "Authorization: Bearer $(grep GROQ_API_KEY /home/pfm/app/.env | cut -d= -f2)"
```
---
## 17. Gitea Integration (your self-hosted)
```bash
# On server — set up deploy key
sudo -u pfm ssh-keygen -t ed25519 -f /home/pfm/.ssh/gitea_deploy -N ""
cat /home/pfm/.ssh/gitea_deploy.pub
# Add this public key to Gitea repo → Settings → Deploy Keys
# Clone with deploy key
sudo -u pfm GIT_SSH_COMMAND='ssh -i /home/pfm/.ssh/gitea_deploy' \
git clone git@gitea.yourdomain.com:youruser/pfm.git /home/pfm/app
```