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 (default 3000) from server/.env or 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_ENABLED off 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)

1

Install Certbot

# Ubuntu/Debian
sudo apt update && sudo apt install -y certbot python3-certbot-nginx
2

Obtain a certificate

sudo certbot --nginx -d salon.example.com
3

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.log and error.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/.env set; 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 ci or npm install --production in server/
  • 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