# Deploy on Ubuntu (EC2) The API listens on port 4040. Nginx serves the frontend build and proxies `/api` to that port. PM2 keeps the API running. Replace `example.com`, bucket names, and account IDs with the real values before you apply anything. ## 1. Server packages ```bash sudo apt update sudo apt install -y curl ca-certificates nginx weasyprint curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs sudo npm install -g pm2 node -v ``` Confirm Node is 20 or newer. Agreement PDFs are printed by the `weasyprint` command, which exits after each file. `WEASYPRINT_BIN` can point at another binary. ## 2. Application ```bash sudo mkdir -p /var/www/ppaimt-crest sudo chown "$USER":"$USER" /var/www/ppaimt-crest cd /var/www/ppaimt-crest git clone . cp backend/.env.example backend/.env ``` Edit `backend/.env` on the server. Set `NODE_ENV=production`, `PORT=4040`, `CORS_ORIGIN` to the public site origin, and the database, S3, mail, and token values. Do not commit that file. ```bash npm install npm run build ``` ## 3. PM2 `ecosystem.config.cjs` starts `backend/src/server.js` on port 4040 and restarts it if it exits. PM2 sets `PORT` itself, so that value wins over `backend/.env`. ```bash pm2 start ecosystem.config.cjs pm2 save pm2 startup ``` Run the command `pm2 startup` prints, then `pm2 save` again. Check the process: ```bash pm2 status curl -s http://127.0.0.1:4040/api/health ``` ## 4. Nginx Copy the example config and point the site name at the real domain. The file serves `frontend/dist`, gzip-compresses text responses, sends basic security headers, and proxies `/api/` to `127.0.0.1:4040`. ```bash sudo cp nginx.conf /etc/nginx/sites-available/ppaimt-crest sudo ln -s /etc/nginx/sites-available/ppaimt-crest /etc/nginx/sites-enabled/ppaimt-crest sudo nginx -t sudo systemctl reload nginx ``` Open the EC2 security group for ports 80 and 443. Leave port 4040 closed to the public. The API is only reached through Nginx. The in-app rate limit uses the address Hapi sees. Set `TRUST_PROXY=true` on the server so the API reads the first `X-Forwarded-For` hop from Nginx. Leave it false anywhere the process is reached directly, so a client cannot spoof the address. ## 5. HTTPS with Certbot ```bash sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com ``` Certbot edits the Nginx site and installs a renewal timer. Confirm renewal with: ```bash sudo certbot renew --dry-run ``` ## 6. RDS MySQL - Use Amazon RDS for MySQL 8. Do not enable public access. - Put the database in private subnets. Its security group should allow inbound TCP 3306 only from the EC2 instance security group. - Create a database user with rights on the application database only. - Set `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, and `DB_NAME` in `backend/.env`. - Set `DB_SSL=true`. The Knex connection then uses TLS and checks the server certificate. - Turn on automated backups and keep them for 7 to 35 days. The restore drill is in [RUNBOOK.md](RUNBOOK.md). `GET /api/health` checks the database. When `S3_BUCKET` is set it also checks the bucket, and returns 503 if either check fails. With no bucket, local disk storage is reported as skipped. ## 7. S3 Create a private bucket in the same region as the app (for example `ap-south-1`). - Block all public access. - Disable ACLs (bucket owner enforced). - Default encryption: SSE-S3 or SSE-KMS. - Enable versioning before go-live: `aws s3api put-bucket-versioning --bucket YOUR_BUCKET_NAME --versioning-configuration Status=Enabled`. The application should use an IAM role on the EC2 instance, not long-lived keys in the image. If you must use keys for a temporary setup, put them in `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` on the server only. Example bucket policy. It denies non-TLS requests and allows one application role to read and write objects. It does not grant public access. ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "DenyInsecureTransport", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:aws:s3:::YOUR_BUCKET_NAME", "arn:aws:s3:::YOUR_BUCKET_NAME/*"], "Condition": { "Bool": { "aws:SecureTransport": "false" } } }, { "Sid": "AllowAppRoleObjectAccess", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_APP_ROLE" }, "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*" } ] } ``` Presigned URLs are the only way objects should be shared. Do not add a public `s3:GetObject` statement. ## 8. Environment Copy `backend/.env.example` to `backend/.env` on the server. Set `NODE_ENV=production`, `PORT=4040`, `TRUST_PROXY=true`, and `CORS_ORIGIN` to the public site origin. `DATA_ENCRYPTION_KEY` is 32 bytes encoded as base64. Create it in Secrets Manager and copy the value into the server env. Do not commit it. `VITE_SUPPORT_EMAIL` in `frontend/.env` is the address shown in the public footer. Set it before `npm run build`. GitHub Actions runs lint, the backend tests against MySQL 8, `npm audit --audit-level=high`, and the frontend build. Deploy is a manual workflow that SSHs to the instance and runs `scripts/deploy.sh`. The host, user, and key are GitHub secrets `DEPLOY_HOST`, `DEPLOY_USER`, and `DEPLOY_KEY`. Day-to-day operations, secret rotation, and the production checklist are in [RUNBOOK.md](RUNBOOK.md). The CREST acceptance list is in [UAT.md](UAT.md).