PPPAIMT-Crest/docs/DEPLOY.md

146 lines
5.5 KiB
Markdown

# 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 <repository-url> .
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).