Files
Personal-Finance-Management/deploy.md
T
2026-05-31 17:28:50 -04:00

13 KiB

PFM — Server Deployment Guide

Ubuntu Server · Nginx · Gunicorn · MySQL · Groq API · Certbot SSL


0. Assumptions

Item Value
OS Ubuntu 22.04 LTS
App user pfm
Home dir /home/pfm
App directory /home/pfm/web
Venv /home/pfm/venv
Uploads /home/pfm/web/uploads
Backups /home/pfm/web/backups
Domain pfm.ngodanguyen.tech
SSL Let's Encrypt via Certbot
MySQL 8.0
Python 3.11+
Gitea gitea.ngodanguyen.tech

1. System Prep

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 unzip \
  libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b \
  libjpeg-dev libopenjp2-7-dev

libpango* libs required by WeasyPrint for PDF generation.


2. Create App User

# Skip if pfm already exists
id pfm &>/dev/null || sudo useradd -m -s /bin/bash pfm
sudo usermod -aG www-data pfm

3. MySQL Setup

sudo mysql_secure_installation
# Set root password, remove anonymous users, disable remote root
sudo mysql -u root -p
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

sudo mkdir -p /home/pfm/web
sudo chown pfm:www-data /home/pfm/web
sudo chmod 750 /home/pfm/web

sudo -u pfm bash
cd /home/pfm

# Create venv at /home/pfm/venv
python3 -m venv venv
source venv/bin/activate

# Clone from Gitea
cd /home/pfm/web
GIT_SSH_COMMAND='ssh -i /home/pfm/.ssh/gitea_deploy' \
  git clone git@gitea.ngodanguyen.tech:youruser/pfm.git .

pip install --upgrade pip
pip install -r requirements.txt

5. Environment File

sudo -u pfm nano /home/pfm/web/.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/web/uploads
MAX_CONTENT_LENGTH=10485760
FLASK_ENV=production
FLASK_APP=wsgi:app
APP_CURRENCY=USD
APP_CURRENCY_SYMBOL=$
APP_TIMEZONE=Asia/Ho_Chi_Minh
chmod 600 /home/pfm/web/.env

Generate secret key:

python3 -c "import secrets; print(secrets.token_hex(32))"

6. Uploads & Backups Directories

sudo mkdir -p /home/pfm/web/uploads
sudo mkdir -p /home/pfm/web/backups
sudo chown pfm:www-data /home/pfm/web/uploads
sudo chown pfm:pfm /home/pfm/web/backups
sudo chmod 750 /home/pfm/web/uploads

7. Initialize Database

cd /home/pfm/web
source /home/pfm/venv/bin/activate

# Remove stray __init__.py from project root if present
rm -f /home/pfm/web/__init__.py

flask db init
flask db migrate -m "initial schema"
flask db upgrade

# Seed categories + create admin user (interactive)
python scripts/init_db.py

8. Gunicorn Setup (systemd service)

sudo nano /etc/systemd/system/pfm.service
[Unit]
Description=PFM — Personal Finance Management System
After=network.target mysql.service

[Service]
User=pfm
Group=www-data
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/gunicorn \
    --chdir /home/pfm/web \
    --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
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

sudo nano /etc/nginx/sites-available/pfm
server {
    listen 80;
    server_name pfm.ngodanguyen.tech;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name pfm.ngodanguyen.tech;

    # Certbot populates these:
    # 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 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/web/app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Receipts — served via Flask send_from_directory (internal)
    location /uploads/ {
        alias /home/pfm/web/uploads/;
        internal;
    }

    access_log /var/log/nginx/pfm_access.log;
    error_log  /var/log/nginx/pfm_error.log;
}
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)

sudo apt install -y certbot python3-certbot-nginx

# Issue cert — auto-edits Nginx config
sudo certbot --nginx -d pfm.ngodanguyen.tech

# Verify
sudo certbot certificates

# Test auto-renewal
sudo certbot renew --dry-run
sudo systemctl status certbot.timer

10. Groq API Setup

  1. Sign up at https://console.groq.com
  2. API Keys → Create API key
  3. Add to /home/pfm/web/.env as GROQ_API_KEY

Models in Use

Model Used For
llama-3.3-70b-versatile Chat + daily insights (default)
llama-3.1-8b-instant Fast queries (user-selectable)
meta-llama/llama-4-scout-17b-16e-instruct Receipt OCR (vision) — hardcoded in ocr_service.py

Free Tier Limits

14,400 requests/day · 500,000 tokens/minute · Free

Verify Key

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}'

11. Scheduled Jobs (systemd timers)

11.1 Process Recurring Transactions (Daily 6AM)

sudo nano /etc/systemd/system/pfm-recurring.service
[Unit]
Description=PFM — Process Recurring Transactions
After=mysql.service

[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/python scripts/process_recurring.py
sudo nano /etc/systemd/system/pfm-recurring.timer
[Unit]
Description=PFM — Recurring Transactions Timer

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target

11.2 Fetch USD/VND Rate (Daily 8AM)

sudo nano /etc/systemd/system/pfm-fxrate.service
[Unit]
Description=PFM — Fetch USD/VND Exchange Rate

[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/python scripts/fetch_fx_rate.py
sudo nano /etc/systemd/system/pfm-fxrate.timer
[Unit]
Description=PFM — FX Rate Timer

[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target

11.3 Fetch Investment Prices (Mon-Fri 4PM)

sudo nano /etc/systemd/system/pfm-prices.service
[Unit]
Description=PFM — Fetch Investment Prices

[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/python scripts/fetch_prices.py
sudo nano /etc/systemd/system/pfm-prices.timer
[Unit]
Description=PFM — Price Fetch Timer

[Timer]
OnCalendar=Mon-Fri *-*-* 16:00:00
Persistent=true

[Install]
WantedBy=timers.target

11.4 Net Worth Snapshot (1st of month)

sudo nano /etc/systemd/system/pfm-snapshot.service
[Unit]
Description=PFM — Net Worth Snapshot

[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/python scripts/daily_snapshot.py
sudo nano /etc/systemd/system/pfm-snapshot.timer
[Unit]
Description=PFM — Snapshot Timer

[Timer]
OnCalendar=*-*-01 00:05:00
Persistent=true

[Install]
WantedBy=timers.target

11.5 AI Daily Insight (Midnight)

sudo nano /etc/systemd/system/pfm-aiinsight.service
[Unit]
Description=PFM — AI Daily Insight
After=network-online.target

[Service]
Type=oneshot
User=pfm
WorkingDirectory=/home/pfm/web
Environment="PATH=/home/pfm/venv/bin"
EnvironmentFile=/home/pfm/web/.env
ExecStart=/home/pfm/venv/bin/python scripts/daily_ai_insight.py
sudo nano /etc/systemd/system/pfm-aiinsight.timer
[Unit]
Description=PFM — AI Insight Timer

[Timer]
OnCalendar=*-*-* 00:01:00
Persistent=true

[Install]
WantedBy=timers.target

Enable All Timers

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. Database Backup (Daily 2AM)

sudo nano /etc/systemd/system/pfm-backup.service
[Unit]
Description=PFM — MySQL Backup

[Service]
Type=oneshot
User=pfm
ExecStart=/bin/bash -c 'mysqldump --defaults-file=/home/pfm/.my.cnf pfm_db | gzip > /home/pfm/web/backups/pfm_$(date +\%Y\%m\%d).sql.gz && find /home/pfm/web/backups -name "*.sql.gz" -mtime +30 -delete'
sudo nano /etc/systemd/system/pfm-backup.timer
[Unit]
Description=PFM — Backup Timer

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now pfm-backup.timer

Store MySQL credentials for backup job:

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'

13. Log Rotation

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
}

14. Firewall (UFW)

sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

MySQL (3306) — internal only. Groq/yfinance — outbound HTTPS (443).


15. Gitea Deploy Key

sudo -u pfm ssh-keygen -t ed25519 -f /home/pfm/.ssh/gitea_deploy -N ""
cat /home/pfm/.ssh/gitea_deploy.pub
# Add to Gitea: repo → Settings → Deploy Keys

16. Deploy Update Workflow

sudo -u pfm bash
cd /home/pfm/web
source /home/pfm/venv/bin/activate

git pull origin main
pip install -r requirements.txt   # only if requirements changed
flask db upgrade                   # only if new migrations

sudo systemctl restart pfm
sudo systemctl status pfm

17. First-Run Checklist

# 1. Force fresh FX rate
source /home/pfm/venv/bin/activate
cd /home/pfm/web
python scripts/fetch_fx_rate.py
# Should print: 1 USD = ₫26,xxx VND [yfinance]

# 2. Verify all timers
sudo systemctl list-timers --all | grep pfm

# 3. Test AI
curl https://api.groq.com/openai/v1/models \
  -H "Authorization: Bearer $(grep GROQ_API_KEY /home/pfm/web/.env | cut -d= -f2)"

# 4. Check app logs
sudo journalctl -u pfm -f

# 5. Full service status
sudo systemctl status pfm nginx mysql

18. Quick Status Reference

# Services
sudo systemctl status pfm
sudo systemctl status nginx
sudo systemctl status mysql

# Live logs
sudo journalctl -u pfm -f
sudo tail -f /var/log/pfm/error.log
sudo tail -f /var/log/nginx/pfm_error.log

# Timers
sudo systemctl list-timers --all | grep pfm

# Run a cron manually
source /home/pfm/venv/bin/activate && cd /home/pfm/web
python scripts/fetch_fx_rate.py
python scripts/process_recurring.py
python scripts/daily_ai_insight.py