# 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=America/New_York ``` ```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 ```