Deployment
ChartChat is deployed on a Linux server using nginx as the web server and PM2 as the Node.js process manager. All deployments are handled by Jenkins (see CI/CD Pipeline).
Server Layout
All releases live under a single deploy base:
/var/www/html/chartchat-jenkins/
├── member-portal/
│ ├── releases/
│ │ ├── release-1/ ← built static files (Vite dist)
│ │ ├── release-2/
│ │ └── release-N/ ← latest release
│ └── current ← symlink → releases/release-N (nginx root)
│
├── admin-portal/
│ ├── releases/
│ │ └── release-N/
│ └── current ← symlink → releases/release-N
│
└── server/
├── releases/
│ └── release-N/ ← dist/ + .env + medplum.config.json + node_modules/
└── current ← symlink → releases/release-N (PM2 CWD)
Each deploy creates a new numbered release directory. The current symlink is atomically switched to the new release as the final step. This means zero-downtime cutover for the portals (nginx serves from the symlink) and near-zero downtime for the server (PM2 is restarted pointing at the new path).
The pipeline keeps the 5 most recent releases and deletes older ones automatically.
Permissions
Release directories are owned by jenkins:www-data with 755 permissions so nginx (www-data) can serve the static files.
chown -R jenkins:www-data /var/www/html/chartchat-jenkins/
chmod -R 755 /var/www/html/chartchat-jenkins/
nginx
nginx serves the two frontend portals as static files. Each has its own vhost pointing at the current symlink.
| Domain | nginx root |
|---|---|
dev.chartchathealth.com | /var/www/html/chartchat-jenkins/member-portal/current |
| Admin domain | /var/www/html/chartchat-jenkins/admin-portal/current |
Both portals are single-page applications (React + Vite). nginx must be configured to fall back to index.html for all routes:
location / {
try_files $uri $uri/ /index.html;
}
PM2
The server runs as a Node.js process managed by PM2 under the ubuntu user.
Start command
pm2 start node --name chartchat-server --cwd /var/www/html/chartchat-jenkins/server/current -- dist/index.js aws:us-east-2:/chartchat/uat/
The aws:us-east-2:/chartchat/uat/ argument tells the server to load its configuration from AWS SSM Parameter Store instead of a local medplum.config.json.
PM2 is controlled by Jenkins via sudo
Jenkins runs as the jenkins user. To allow it to manage PM2 without a password, add this to sudoers:
jenkins ALL=(ubuntu) NOPASSWD: /home/ubuntu/.nvm/versions/node/v24.11.1/bin/pm2
File location: /etc/sudoers.d/jenkins-pm2
Why delete + start instead of restart
PM2 resolves the --cwd path at start time. If you use pm2 restart, PM2 keeps using the path from the original start command — it will not follow the updated current symlink. Every deploy therefore does:
pm2 delete chartchat-server # remove old process entry
pm2 start node --name chartchat-server --cwd "${SERVER_CURRENT}" -- dist/index.js aws:us-east-2:/chartchat/uat/
pm2 save
Server Runtime Files
The following files must be present in the release directory when the server starts:
| File | Source | Notes |
|---|---|---|
dist/ | npm run build output | Compiled TypeScript → JavaScript |
.env | Written from Jenkins credentials | Never committed to git |
medplum.config.json | Not used | Server reads config from AWS SSM at startup |
node_modules/ | npm install inside release dir | Installed after copying dist |
node_modules/@medplum/definitions/dist/ | Local build override | See note below |
Why @medplum/definitions is overridden
The version of @medplum/definitions published to the npm registry does not include ChartChat's custom FHIR resource types (PatientEhrConnection, SSOIntegration, HospitalIntegration, etc.). After running npm install in the release directory, the pipeline overwrites the npm-installed copy with the locally built one:
cp -r "${WORKSPACE}/packages/definitions/dist/." \
"${RELEASE_DIR}/node_modules/@medplum/definitions/dist/"
Health Check & Rollback
After PM2 is started, the pipeline polls http://localhost:8103/healthcheck every 10 seconds for up to 4 minutes (24 attempts).
- Success (HTTP 2xx/3xx): Deploy is marked complete.
- Failure: The pipeline:
- Finds the previous release directory
- Repoints the
currentsymlink back to it - Runs
pm2 restart chartchat-server - Fails the Jenkins build so the team is alerted
The Member Portal and Admin Portal have similar rollback logic — if copying or symlinking fails, the current symlink is reverted to the previous release.
Manual Deployment Reference
The original manual setup lives at /var/www/html/chartchat-health/chartchat. Do not touch this directory — Jenkins deploys exclusively to /var/www/html/chartchat-jenkins/.