Part of the PurelyManage series.
This is the end-to-end deployment guide for PurelyManage. The backend runs as a Node.js process managed by systemd. The frontend is a static React build served from S3 via CloudFront. nginx handles TLS termination and reverse proxying for the backend.
Both repos are on GitHub:
- Backend: github.com/sagarnayak/purelymanageBackend-public
- Frontend: github.com/sagarnayak/purelymanageFrontend-public
Prerequisites
- Ubuntu (or any Linux with systemd)
- Node.js 18 or later
- PostgreSQL (any recent version, local or remote)
- nginx
- certbot (for TLS)
- imapsync (only if you need the migration feature, see the previous post)
Backend Setup
Clone the repo and install dependencies:
git clone https://github.com/sagarnayak/purelymanageBackend-public.git
cd purelymanageBackend-public
npm install
Copy the example env file and fill in your values:
cp .env.example .env
Required environment variables:
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/purelymanage
JWT_SECRET=<long random string>
JWT_REFRESH_SECRET=<different long random string>
ENCRYPTION_KEY=<64-char hex string> # openssl rand -hex 32
PURELYMAIL_API_TOKEN=<your token>
PURELYMAIL_OWNERSHIP_TOKEN=<your token>
FRONTEND_URL=https://yourdomain.com
# SMTP - leave blank to disable email notifications
SMTP_HOST=
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
Generate a fresh ENCRYPTION_KEY:
openssl rand -hex 32
Run the database migration to create all tables:
npm run migrate
Build the TypeScript source:
npm run build
Test it runs before wiring up systemd:
node dist/index.js
You should see the server start on port 3000. Stop it and proceed to the service setup.
systemd Service
Create /etc/systemd/system/purelymanage.service:
[Unit]
Description=PurelyManage Backend
After=network.target
[Service]
User=youruser
WorkingDirectory=/path/to/purelymanageBackend-public
EnvironmentFile=/path/to/purelymanageBackend-public/.env
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Replace youruser and the paths with your actual values. The EnvironmentFile directive loads all variables from .env into the process environment, so secrets never need to be hardcoded in the service file.
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable purelymanage
sudo systemctl start purelymanage
Check it is running:
sudo systemctl status purelymanage
View logs:
journalctl -u purelymanage -f
nginx and TLS
The backend expects to sit behind a reverse proxy. nginx handles TLS and forwards requests to port 3000.
Install nginx and certbot if not already present:
sudo apt install nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/purelymanage:
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/purelymanage /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Get a TLS certificate with certbot:
sudo certbot --nginx -d api.yourdomain.com
certbot will modify your nginx config to add SSL, redirect HTTP to HTTPS, and set up auto-renewal. After this your backend is reachable at https://api.yourdomain.com.
Update FRONTEND_URL in your .env to match the domain you will use for the frontend, then restart the service:
sudo systemctl restart purelymanage
Frontend Deployment
The frontend is a Vite + React app that builds to static files. You can serve it from anywhere that handles single-page apps: nginx on the same server, S3 + CloudFront, Netlify, or any static host.
Option A: nginx on the same server
Clone the frontend repo:
git clone https://github.com/sagarnayak/purelymanageFrontend-public.git
cd purelymanageFrontend-public
npm install
Create a .env file:
VITE_API_URL=https://api.yourdomain.com
Build:
npm run build
This produces a dist/ folder. Add a nginx server block to serve it:
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
root /path/to/purelymanageFrontend-public/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
The try_files directive with /index.html fallback is required for React Router to handle client-side navigation.
Option B: S3 + CloudFront with GitHub Actions
The frontend repo includes a GitHub Actions workflow that builds and deploys to S3 automatically on push to main. The workflow trigger is set to manual by default on the public repo. To enable auto-deploy:
- Change the
onblock in.github/workflows/deploy.ymlfromworkflow_dispatchto:
on:
push:
branches:
- main
- Add these secrets in your GitHub repo settings:
| Secret | Value |
|---|---|
VITE_API_URL | https://api.yourdomain.com |
S3_BUCKET | your S3 bucket name |
AWS_ACCESS_KEY_ID | IAM credentials with S3 write access |
AWS_SECRET_ACCESS_KEY | IAM credentials |
CLOUDFRONT_DISTRIBUTION_ID | your CloudFront distribution ID |
The S3 bucket should be private with a CloudFront OAC. The CloudFront distribution needs a custom domain with an ACM certificate and 403/404 errors redirected to index.html with a 200 status for React Router to work.
First Launch
Open the frontend URL in a browser. On a fresh install with no users in the database, you are redirected to /setup to create the first owner account. After that, login is at /login.
If you see a CORS error in the browser console, check that FRONTEND_URL in your backend .env matches the exact origin of the frontend (protocol, domain, and port). Restart the backend after any .env change.
Start from the beginning: PurelyManage: Open-Source Admin Panel for PurelyMail