Finance-Accounts/ar-aging-app/deploy/DEPLOY.md

6.1 KiB
Raw Blame History

Production deployment (AWS, one server)

One 8 GB server runs everything via docker-compose.prod.yml: caddy (automatic HTTPS) → web (nginx: React build + /api proxy) → backend (FastAPI, single worker) + mysql (data on the instance disk), with nightly backups to S3.

8 GB RAM is not optional: uploaded Amazon exports are 300500 MB and expand to multi-GB while parsing. Validate with your largest real file before buying anything smaller.

Monthly cost: ≈ $5055 — Lightsail 8 GB $44 (or EC2 t4g.large ≈ $61 with EBS + IPv4), S3 backups $1.503, weekly snapshots $24, Route 53 $0.50, Frankfurter FX API $0.


1. Provision

  1. Lightsail: 8 GB / 2 vCPU / 160 GB SSD instance, Ubuntu 24.04. Attach the included static IP. (EC2 route: t4g.large + 100 GB gp3 EBS + Elastic IP.)
  2. Firewall: allow 22 (your office IPs only), 80, 443. Everything else closed.
  3. Install Docker + AWS CLI:
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER   # re-login after this
    sudo apt-get install -y awscli  # or the AWS CLI v2 bundle
    
  4. S3 bucket for backups: create company-ar-backups, enable versioning, add a lifecycle rule (transition to Glacier/IA after 90 days). Attach an IAM role to the instance allowing s3:PutObject, s3:GetObject, s3:ListBucket on that bucket — no access keys on disk.
  5. DNS: A record ar.<company>.com → the static IP. Caddy then issues and renews the TLS certificate automatically — there is no certbot step.

2. Configure & start

sudo mkdir -p /opt/ar-aging && sudo chown $USER /opt/ar-aging
cd /opt/ar-aging && git clone <repo-url> . && cd ar-aging-app

cp .env.example .env.production
nano .env.production        # fill the PRODUCTION section: AR_DOMAIN, passwords,
                            # AR_SECRET_KEY (openssl rand -hex 32), backup bucket
# (a pre-filled .env.production with generated credentials already exists on the
#  dev machine — copy it to the server instead of re-generating)

docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build
curl -s https://ar.<company>.com/api/health     # {"status":"ok",...}

Every docker compose ... -f docker-compose.prod.yml command below also needs --env-file .env.production — set an alias once and forget it: alias dcp='docker compose --env-file .env.production -f docker-compose.prod.yml'

3. Create the users (5 logins)

docker compose --env-file .env.production -f docker-compose.prod.yml exec backend \
  python manage.py add-user talha --name "Talha Ahmed"
# repeat per user; passwords are prompted, never stored in shell history
docker compose --env-file .env.production -f docker-compose.prod.yml exec backend python manage.py list-users

AR_AUTH=on means the API refuses everything except login/health until users exist. Password resets: manage.py set-password <username>. Leavers: manage.py deactivate-user.

4. Migrate the existing SQLite data (one-time)

The current data lives in backend/data/ar_aging.db on the dev machine. Do a timed dry run first — January alone is ~3.4M transaction rows.

# copy the SQLite file to the server first (scp), then from ar-aging-app/:
docker compose --env-file .env.production -f docker-compose.prod.yml cp ./ar_aging.db backend:/tmp/ar_aging.db
docker compose --env-file .env.production -f docker-compose.prod.yml exec backend \
  python migrate_sqlite_to_mysql.py --sqlite /tmp/ar_aging.db --dry-run
docker compose --env-file .env.production -f docker-compose.prod.yml exec backend \
  python migrate_sqlite_to_mysql.py --sqlite /tmp/ar_aging.db

Verify before anyone uses it: per-table row counts printed by the script must match, and a spot check to the cent — open the January closing and compare /api/sessions/{id}/reconciliation final_receivable_usd against the dev machine. Copy backend/data/uploads/ into the ar_data volume the same way (compose cp ./uploads backend:/data/), then archive the SQLite file to S3 and retire the dev copy.

One-time cleanup for the historical double-count bug (duplicate upload rows):

docker compose --env-file .env.production -f docker-compose.prod.yml exec backend python manage.py dedupe-files          # dry run
docker compose --env-file .env.production -f docker-compose.prod.yml exec backend python manage.py dedupe-files --apply
# then re-process the closings it flagged

5. Backups

chmod +x deploy/backup.sh
crontab -e
# 30 2 * * * /opt/ar-aging/ar-aging-app/deploy/backup.sh >> /var/log/ar-backup.log 2>&1

Three layers: nightly mysqldump + uploads/exports → versioned S3 (the script), weekly instance snapshots (Lightsail console → enable automatic snapshots), and MySQL's own volume on the instance disk. Run the restore drill quarterly — commands are at the bottom of backup.sh.

6. Deploying updates

cd /opt/ar-aging/ar-aging-app
git pull
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build

Deploy outside a processing run when possible. If a restart does land mid-run, the closing is auto-marked as interrupted at startup (never stuck on "processing") — just re-run it.

7. Operating notes

  • Single backend worker, single instance — by design. Jobs and their progress live in-process. Do not add --workers or replicas.
  • Logs: docker compose --env-file .env.production -f docker-compose.prod.yml logs -f backend (requests, jobs, FX fetches, logins). Add the CloudWatch agent if you want them off-box.
  • Health: GET /api/health checks the DB and data-dir and is unauthenticated — point Lightsail/CloudWatch monitoring at it.
  • Exchange rates: Frankfurter (free, keyless). The only outbound call the app makes; currency codes and dates only. Fetched rates still require in-app confirmation (C5).
  • Upgrade path (not needed at this scale): move MySQL to RDS db.t4g.small (+~$30/mo, point-in-time restore) by setting MYSQL_HOST to the RDS endpoint and removing the mysql service; move exports to S3-primary with presigned URLs if the disk ever tightens.