# 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 ```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 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 ```bash # 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 ```bash sudo mysql_secure_installation # 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/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 ```bash sudo -u pfm nano /home/pfm/web/.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/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 ``` ```bash chmod 600 /home/pfm/web/.env ``` Generate secret key: ```bash python3 -c "import secrets; print(secrets.token_hex(32))" ``` --- ## 6. Uploads & Backups Directories ```bash 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 ```bash 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) ```bash sudo nano /etc/systemd/system/pfm.service ``` ```ini [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 ``` ```bash 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; 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; } ``` ```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 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 ```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}' ``` --- ## 11. Scheduled Jobs (systemd timers) ### 11.1 Process Recurring Transactions (Daily 6AM) ```bash sudo nano /etc/systemd/system/pfm-recurring.service ``` ```ini [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 ``` ```bash sudo nano /etc/systemd/system/pfm-recurring.timer ``` ```ini [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) ```bash sudo nano /etc/systemd/system/pfm-fxrate.service ``` ```ini [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 ``` ```bash sudo nano /etc/systemd/system/pfm-fxrate.timer ``` ```ini [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) ```bash sudo nano /etc/systemd/system/pfm-prices.service ``` ```ini [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 ``` ```bash sudo nano /etc/systemd/system/pfm-prices.timer ``` ```ini [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) ```bash sudo nano /etc/systemd/system/pfm-snapshot.service ``` ```ini [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 ``` ```bash sudo nano /etc/systemd/system/pfm-snapshot.timer ``` ```ini [Unit] Description=PFM — Snapshot Timer [Timer] OnCalendar=*-*-01 00:05:00 Persistent=true [Install] WantedBy=timers.target ``` ### 11.5 AI Daily Insight (Midnight) ```bash sudo nano /etc/systemd/system/pfm-aiinsight.service ``` ```ini [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 ``` ```bash sudo nano /etc/systemd/system/pfm-aiinsight.timer ``` ```ini [Unit] Description=PFM — AI Insight Timer [Timer] OnCalendar=*-*-* 00:01: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. Database Backup (Daily 2AM) ```bash sudo nano /etc/systemd/system/pfm-backup.service ``` ```ini [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' ``` ```bash sudo nano /etc/systemd/system/pfm-backup.timer ``` ```ini [Unit] Description=PFM — Backup Timer [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target ``` ```bash sudo systemctl enable --now pfm-backup.timer ``` Store MySQL credentials for 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' ``` --- ## 13. 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 } ``` --- ## 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) — internal only. Groq/yfinance — outbound HTTPS (443). --- ## 15. Gitea Deploy Key ```bash 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 ```bash 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 ```bash # 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 ```bash # 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 ```