Server configuration & deployment
Production deployment, reverse proxy, and security for Salon Management
Architecture overview
Salon Management ships as a single Node.js application under server/:
- Express renders EJS views for the browser and exposes HTTP routes for dashboards, appointments, POS, settings, and the rest of the product.
- Static files (CSS, JS, images) and uploads (for example profile photos
and salon logos) are served from
server/public, including paths under/uploads/. - The process listens on
PORT(default3000) fromserver/.envor the environment.
In production, a reverse proxy (nginx or Apache) typically terminates TLS on ports 80/443 and forwards all requests to the Node process. There is no separate Vite or SPA build—you do not deploy a second static bundle for the UI.
Server requirements
- CPU: 2+ cores (more for higher concurrency)
- RAM: 4GB minimum; 8GB+ recommended with MySQL and Node
- Storage: SSD recommended; room for logs, uploads, and database
- OS: Ubuntu LTS, Debian, RHEL-family Linux, or similar
- Network: DNS pointing to the server; ports 80/443 open for the reverse proxy
Environment configuration
Production server/.env
Create or update the server .env on the host (never commit real secrets). Start from
.env-example and adjust:
Example production server/.env
NODE_ENV=production PORT=3000 DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=salon_management DB_USER=salon_prod DB_PASSWORD=use_a_long_random_password JWT_SECRET=use_a_long_random_secret_different_from_db_password JWT_EXPIRES_IN=7d # Demo re-seed cron — leave disabled in production # DEMO_SEED_CRON_ENABLED=false
Security
Do not commit .env. Use strong, unique values for JWT_SECRET and database
credentials. Rotate secrets if they are ever exposed.
Production checklist
- Set
NODE_ENV=production - Restrict the MySQL user to the Salon Management database only
- Keep
DEMO_SEED_CRON_ENABLEDoff unless you intentionally run a public demo that resets data
Process management (PM2)
Run the application under PM2 (or systemd below). Example ecosystem.config.js in
server/:
module.exports = {
apps: [{
name: 'salon-management',
cwd: __dirname,
script: './bin/www',
instances: 1,
exec_mode: 'fork',
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
time: true
}]
};
Create server/logs if needed, then:
cd /var/www/pdma_envato_salon_management/server pm2 start ecosystem.config.js pm2 save pm2 startup
Cluster mode with multiple Node workers is possible but test sessions and file uploads carefully before enabling.
systemd (optional)
Example unit if you prefer systemd over PM2 startup hooks. Adjust paths and user:
[Unit] Description=Salon Management After=network.target mysql.service [Service] Type=simple User=www-data WorkingDirectory=/var/www/pdma_envato_salon_management/server Environment=NODE_ENV=production ExecStart=/usr/bin/node ./bin/www Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target
sudo systemctl daemon-reload sudo systemctl enable salon-management sudo systemctl start salon-management
Web server (reverse proxy)
Forward all HTTP(S) traffic to Node so EJS routes, APIs, and static files under
/uploads stay on one origin. Increase client_max_body_size (nginx) or
LimitRequestBody (Apache) if you expect large logo or profile uploads.
Nginx example
Adjust server_name and paths. Node listens on 127.0.0.1:3000.
# Optional: rate limiting — define once in http { } in nginx.conf:
# limit_req_zone $binary_remote_addr zone=salon_app:10m rate=10r/s;
server {
listen 80;
server_name salon.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name salon.example.com;
ssl_certificate /etc/letsencrypt/live/salon.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/salon.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 25M;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
# limit_req zone=salon_app burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -sf /etc/nginx/sites-available/salon-management /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Apache example
Enable proxy, proxy_http, ssl, headers. Forward
everything to Node:
<VirtualHost *:443>
ServerName salon.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/salon.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/salon.example.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
Add HTTP → HTTPS redirect and enable the site with a2ensite per your distribution.
SSL (Let’s Encrypt)
Install Certbot
# Ubuntu/Debian sudo apt update && sudo apt install -y certbot python3-certbot-nginx
Obtain a certificate
sudo certbot --nginx -d salon.example.com
Renewal test
sudo certbot renew --dry-run
Backups
Back up MySQL and uploaded files under server/public/uploads (profile images, salon logos,
etc.).
#!/bin/bash set -euo pipefail DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/var/backups/salon-management" DB_NAME="salon_management" DB_USER="salon_prod" APP_ROOT="/var/www/pdma_envato_salon_management" mkdir -p "$BACKUP_DIR" mysqldump -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" | gzip > "$BACKUP_DIR/db_$DATE.sql.gz" tar -czf "$BACKUP_DIR/uploads_$DATE.tar.gz" -C "$APP_ROOT/server/public" uploads # Retention example: delete backups older than 30 days find "$BACKUP_DIR" -name "db_*.sql.gz" -mtime +30 -delete find "$BACKUP_DIR" -name "uploads_*.tar.gz" -mtime +30 -delete
Store DB_PASSWORD securely (environment or a .cnf with mode 0600). Do
not embed production passwords in scripts checked into git.
Monitoring & logs
- PM2:
pm2 logs salon-management,pm2 monit - nginx:
/var/log/nginx/access.loganderror.log - systemd:
journalctl -u salon-management -f
Example logrotate for PM2 log directory (adjust paths):
/var/www/pdma_envato_salon_management/server/logs/*.log {
daily
rotate 30
compress
missingok
notifempty
}
Security hardening
- Firewall: allow SSH, 80, and 443; avoid exposing MySQL (3306) publicly unless required
- SSH: key-based auth where policy allows
- Keep the OS and npm dependencies updated (
npm audit, patch Sequelize/MySQL) - HTTPS in production; JWT and cookies rely on a correctly configured app URL behind the proxy
Performance notes
| Topic | Notes |
|---|---|
NODE_ENV=production |
Enables production behavior in Express and typical Node libraries |
| MySQL | Index foreign keys and frequently filtered columns; monitor slow queries |
| Static assets | Express serves public/; nginx can optionally cache static extensions in front of Node if
you split locations (advanced) |
Deployment checklist
Before go-live
- Staging tested: migrations; no demo passwords in production
- Production
server/.envset; demo seed cron disabled - TLS configured; HTTP redirects to HTTPS
- Reverse proxy forwards the full site to Node
- Backups and restore procedure documented
Deploy
- Deploy code; run
npm ciornpm install --productioninserver/ - Run
npm run db:migrate(use seeders only when appropriate—never run demo seeders on a live database without review) - Restart PM2 or systemd; reload nginx/Apache
- Smoke-test login, uploads, and critical flows from the public URL
Related docs
Windows setup, Linux setup, macOS setup, Troubleshooting, Home.