15 KiB
Deployment Guide
This guide covers deploying Flyer Crawler to a production server.
Last verified: 2026-01-28
Related documentation:
- ADR-014: Containerization and Deployment Strategy
- ADR-015: Error Tracking and Observability
- Bare-Metal Setup Guide
- Monitoring Guide
Quick Reference
Command Reference Table
| Task | Command |
|---|---|
| Deploy to production | Gitea Actions workflow (manual trigger) |
| Deploy to test | Automatic on push to main |
| Check PM2 status | pm2 list |
| View logs | pm2 logs flyer-crawler-api --lines 100 |
| Restart all | pm2 restart all |
| Check NGINX | sudo nginx -t && sudo systemctl status nginx |
| Check health | curl -s https://flyer-crawler.projectium.com/api/health/ready | jq . |
Deployment URLs
| Environment | URL | API Port |
|---|---|---|
| Production | https://flyer-crawler.projectium.com |
3001 |
| Test | https://flyer-crawler-test.projectium.com |
3002 |
| Dev Container | https://localhost |
3001 |
Server Access Model
Important: Claude Code (and AI tools) have READ-ONLY access to production/test servers. The deployment workflow is:
| Actor | Capability |
|---|---|
| Gitea CI/CD | Automated deployments via workflows (has write access) |
| User (human) | Manual server access for troubleshooting and emergency fixes |
| Claude Code | Provides commands for user to execute; cannot run them directly |
When troubleshooting deployment issues:
- Claude provides diagnostic commands for the user to run
- User executes commands and reports output
- Claude analyzes results and provides fix commands (1-3 at a time)
- User executes fixes and reports results
- Claude provides verification commands to confirm success
Prerequisites
| Component | Version | Purpose |
|---|---|---|
| Ubuntu | 22.04 LTS | Operating system |
| PostgreSQL | 14+ | Database with PostGIS extension |
| Redis | 6+ | Caching and job queues |
| Node.js | 20.x LTS | Application runtime |
| NGINX | 1.18+ | Reverse proxy and static files |
| PM2 | Latest | Process manager |
Verify prerequisites:
node --version # Should be v20.x.x
psql --version # Should be 14+
redis-cli ping # Should return PONG
nginx -v # Should be 1.18+
pm2 --version # Any recent version
Dev Container Parity (ADR-014)
The development container now matches production architecture:
| Component | Production | Dev Container |
|---|---|---|
| Process Mgmt | PM2 | PM2 |
| API Server | PM2 cluster mode | PM2 fork + tsx watch |
| Worker | PM2 process | PM2 process + tsx watch |
| Logs | PM2 -> Logstash | PM2 -> Logstash -> Bugsink |
| HTTPS | Let's Encrypt | Self-signed (mkcert) |
This ensures issues caught in dev will behave the same in production.
Dev Container Guide: See docs/development/DEV-CONTAINER.md
Server Setup
Install Node.js
curl -sL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs
Install PM2
sudo npm install -g pm2
Application Deployment
Clone and Install
git clone <repository-url>
cd flyer-crawler.projectium.com
npm install
Build for Production
npm run build
Start with PM2
npm run start:prod
This starts three PM2 processes:
flyer-crawler-api- Main API serverflyer-crawler-worker- Background job workerflyer-crawler-analytics-worker- Analytics processing worker
Environment Variables (Gitea Secrets)
For deployments using Gitea CI/CD workflows, configure these as repository secrets:
| Secret | Description |
|---|---|
DB_HOST |
PostgreSQL server hostname |
DB_USER |
PostgreSQL username |
DB_PASSWORD |
PostgreSQL password |
DB_DATABASE_PROD |
Production database name |
REDIS_PASSWORD_PROD |
Production Redis password |
REDIS_PASSWORD_TEST |
Test Redis password |
JWT_SECRET |
Long, random string for signing auth tokens |
VITE_GOOGLE_GENAI_API_KEY |
Google Gemini API key |
GOOGLE_MAPS_API_KEY |
Google Maps Geocoding API key |
NGINX Configuration
Reference Configuration Files
The repository contains reference copies of the production NGINX configurations for documentation and version control purposes:
| File | Server Config |
|---|---|
etc-nginx-sites-available-flyer-crawler.projectium.com |
Production NGINX config |
etc-nginx-sites-available-flyer-crawler-test-projectium-com.txt |
Test/staging NGINX config |
Important Notes:
- These are reference copies only - they are not used directly by NGINX
- The actual live configurations reside on the server at
/etc/nginx/sites-available/ - When modifying server NGINX configs, update these reference files to keep them in sync
- Use the dev container config at
docker/nginx/dev.conffor local development
Reverse Proxy Setup
Create a site configuration at /etc/nginx/sites-available/flyer-crawler.projectium.com:
server {
listen 80;
server_name flyer-crawler.projectium.com;
location / {
proxy_pass http://localhost:5173;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Serve flyer images from static storage (7-day cache)
location /flyer-images/ {
alias /var/www/flyer-crawler.projectium.com/flyer-images/;
expires 7d;
add_header Cache-Control "public, immutable";
}
}
Static Flyer Images
Flyer images are served as static files from the /flyer-images/ path with browser caching enabled:
| Environment | Directory | URL Pattern |
|---|---|---|
| Production | /var/www/flyer-crawler.projectium.com/flyer-images/ |
https://flyer-crawler.projectium.com/flyer-images/ |
| Test | /var/www/flyer-crawler-test.projectium.com/flyer-images/ |
https://flyer-crawler-test.projectium.com/flyer-images/ |
| Dev Container | /app/public/flyer-images/ |
https://localhost/flyer-images/ |
Cache Settings: Files are served with expires 7d and Cache-Control: public, immutable headers for optimal browser caching.
Create the flyer images directory if it does not exist:
sudo mkdir -p /var/www/flyer-crawler.projectium.com/flyer-images
sudo chown www-data:www-data /var/www/flyer-crawler.projectium.com/flyer-images
Enable the site:
sudo ln -s /etc/nginx/sites-available/flyer-crawler.projectium.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
MIME Types Fix for .mjs Files
If JavaScript modules (.mjs files) aren't loading correctly, add the proper MIME type.
Option 1: Edit the site configuration file directly:
# Add inside the server block
types {
application/javascript js mjs;
}
Option 2: Edit /etc/nginx/mime.types globally:
# Change this line:
application/javascript js;
# To:
application/javascript js mjs;
After changes:
sudo nginx -t
sudo systemctl reload nginx
PM2 Log Management
Install and configure pm2-logrotate to manage log files:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 14
pm2 set pm2-logrotate:compress false
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
Rate Limiting
The application respects the Gemini AI service's rate limits. You can adjust the GEMINI_RPM (requests per minute) environment variable in production as needed without changing the code.
CI/CD Pipeline
The project includes Gitea workflows at .gitea/workflows/deploy.yml that:
- Run tests against a test database
- Build the application
- Deploy to production on successful builds
The workflow automatically:
- Sets up the test database schema before tests
- Tears down test data after tests complete
- Deploys to the production server
Monitoring
Check PM2 Status
pm2 status
pm2 logs
pm2 logs flyer-crawler-api --lines 100
Restart Services
pm2 restart all
pm2 restart flyer-crawler-api
Error Tracking with Bugsink (ADR-015)
Bugsink is a self-hosted Sentry-compatible error tracking system. See docs/adr/0015-application-performance-monitoring-and-error-tracking.md for the full architecture decision.
Creating Bugsink Projects and DSNs
After Bugsink is installed and running, you need to create projects and obtain DSNs:
-
Access Bugsink UI: Navigate to
http://localhost:8000 -
Log in with your admin credentials
-
Create Backend Project:
- Click "Create Project"
- Name:
flyer-crawler-backend - Platform: Node.js
- Copy the generated DSN (format:
http://<key>@localhost:8000/<project_id>)
-
Create Frontend Project:
- Click "Create Project"
- Name:
flyer-crawler-frontend - Platform: React
- Copy the generated DSN
-
Configure Environment Variables:
# Backend (server-side) export SENTRY_DSN=http://<backend-key>@localhost:8000/<backend-project-id> # Frontend (client-side, exposed to browser) export VITE_SENTRY_DSN=http://<frontend-key>@localhost:8000/<frontend-project-id> # Shared settings export SENTRY_ENVIRONMENT=production export VITE_SENTRY_ENVIRONMENT=production export SENTRY_ENABLED=true export VITE_SENTRY_ENABLED=true
Testing Error Tracking
Verify Bugsink is receiving events:
npx tsx scripts/test-bugsink.ts
This sends test error and info events. Check the Bugsink UI for:
BugsinkTestErrorin the backend project- Info message "Test info message from test-bugsink.ts"
Sentry SDK v10+ HTTP DSN Limitation
The Sentry SDK v10+ enforces HTTPS-only DSNs by default. Since Bugsink runs locally over HTTP, our implementation uses the Sentry Store API directly instead of the SDK's built-in transport. This is handled transparently by the sentry.server.ts and sentry.client.ts modules.
Deployment Troubleshooting
Decision Tree: Deployment Issues
Deployment failed?
|
+-- Build step failed?
| |
| +-- TypeScript errors --> Fix type issues, run `npm run type-check`
| +-- Missing dependencies --> Run `npm ci`
| +-- Out of memory --> Increase Node heap size
|
+-- Tests failed?
| |
| +-- Database connection --> Check DB_HOST, credentials
| +-- Redis connection --> Check REDIS_URL
| +-- Test isolation --> Check for race conditions
|
+-- SSH/Deploy failed?
|
+-- Permission denied --> Check SSH keys in Gitea secrets
+-- Host unreachable --> Check firewall, VPN
+-- PM2 error --> Check PM2 logs on server
Common Deployment Issues
| Symptom | Diagnosis | Solution |
|---|---|---|
| "Connection refused" on health check | API not started | Check pm2 logs flyer-crawler-api |
| 502 Bad Gateway | NGINX cannot reach API | Verify API port (3001), restart PM2 |
| CSS/JS not loading | Build artifacts missing | Re-run npm run build, check NGINX static paths |
| Database migrations failed | Schema mismatch | Run migrations manually, check DB connectivity |
| "ENOSPC" error | Disk full | Clear old logs: pm2 flush, clean npm cache |
| SSL certificate error | Cert expired/missing | Run certbot renew, check NGINX config |
Post-Deployment Verification Checklist
After every deployment, verify:
- Health check passes:
curl -s https://flyer-crawler.projectium.com/api/health/ready - PM2 processes running:
pm2 listshowsonlinestatus - No recent errors: Check Bugsink for new issues
- Frontend loads: Browser shows login page
- API responds:
curl https://flyer-crawler.projectium.com/api/health/ping
Rollback Procedure
If deployment causes issues:
# 1. Check current release
cd /var/www/flyer-crawler.projectium.com
git log --oneline -5
# 2. Revert to previous commit
git checkout HEAD~1
# 3. Rebuild and restart
npm ci && npm run build
pm2 restart all
# 4. Verify health
curl -s http://localhost:3001/api/health/ready | jq .
Related Documentation
- Database Setup - PostgreSQL and PostGIS configuration
- Monitoring Guide - Health checks and error tracking
- Logstash Quick Reference - Log aggregation
- Bare-Metal Server Setup - Manual server installation guide