146 lines
5.5 KiB
Markdown
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).
|