Skip to content

Deploy: Ubuntu Plesk

Oleg Ivanchenko edited this page Oct 23, 2026 · 11 revisions

Deploy: Ubuntu Plesk

Multi-instance oCMS deployment on Ubuntu with Plesk, one instance per domain/vhost.

Architecture

Core themes are embedded in the binary. Each site runs on its own port behind Nginx reverse proxy.

/opt/ocms/bin/ocms              # Shared binary (includes themes)
/opt/ocms/bin/ocmsctl           # CLI management tool
/etc/ocms/sites.conf            # Site registry
/etc/systemd/system/ocms@.service  # Systemd template

Per site:
/var/www/vhosts/example.com/ocms/
├── data/ocms.db                # SQLite database
├── uploads/                    # Media files
├── custom/                     # Custom themes/modules (optional)
├── backups/                    # Automated backups
├── logs/ocms.log               # App log (stdout + stderr); logs/error.log is the error-only log
├── .env                        # Environment config
└── ocms.pid                    # PID file (ocmsctl mode)

/etc/systemd/system/ocms@example_com.service.d/instance.conf   # Per-site systemd override

Quick Start

1. Install Binary and Tools

# Local machine:
mkdir -p bin
curl -L https://github.com/olegiv/ocms-go/releases/latest/download/ocms-linux-amd64.tar.gz | tar -xz ocms
mv ocms bin/ocms-linux-amd64
scp bin/ocms-linux-amd64 user@server:/tmp/ocms
ssh user@server mkdir -p /tmp/ocms-setup
scp scripts/deploy/* user@server:/tmp/ocms-setup/

# On the server:
sudo mkdir -p /opt/ocms/bin /etc/ocms
sudo cp /tmp/ocms /opt/ocms/bin/ocms
sudo chmod 755 /opt/ocms/bin/ocms
sudo cp /tmp/ocms-setup/ocmsctl /opt/ocms/bin/
sudo chmod 755 /opt/ocms/bin/ocmsctl
sudo cp /tmp/ocms-setup/ocms@.service /etc/systemd/system/
for script in setup-site.sh deploy-multi.sh backup-multi.sh healthcheck-multi.sh generate-logrotate.sh helper.sh; do
    sudo cp /tmp/ocms-setup/$script /opt/ocms/
    sudo chmod 755 /opt/ocms/$script
done
sudo systemctl daemon-reload

For development builds, replace the release download with make build-linux-amd64.

2. Provision a Site

sudo /opt/ocms/setup-site.sh example.com example_com

Arguments: domain, system-user, port (auto-assigned from 8081), group (default: psaserv). Option: --path PATH for a custom instance directory (absolute, under /var/www/vhosts/<domain>/, parent directory must exist).

The script creates directories, generates a session secret, an OCMS_MFA_KEY (production requires two-factor authentication for every account; back the key up with the database) and an embed proxy token, writes the .env, creates the systemd drop-in, registers the site in /etc/ocms/sites.conf, and prints the Nginx snippet for Plesk.

The generated .env sets OCMS_ENV=production, and in production the application refuses to start unless OCMS_TRUSTED_PROXIES (127.0.0.1 for the local nginx) and OCMS_API_ALLOWED_CIDRS are set, or the matching OCMS_REQUIRE_TRUSTED_PROXIES / OCMS_REQUIRE_API_ALLOWED_CIDRS is explicitly false. The script does not write them, so add them to the site's .env before starting it.

3. Configure SSL in Plesk

  1. Websites & Domains → your domain → SSL/TLS Certificates
  2. Click Install under "Let's Encrypt"
  3. Enable Permanent SEO-safe 301 redirect from HTTP to HTTPS

4. Configure Nginx in Plesk

Paste the snippet from setup-site.sh into Websites & Domains → example.com → Apache & nginx Settings → Additional nginx directives.

5. Bootstrap the Admin Account

Fresh binary/server installs need a one-time seeded bootstrap before the production service is enabled. Keep the generated .env production default OCMS_DO_SEED=false; override it only for this foreground bootstrap run:

cd /var/www/vhosts/example.com/ocms
sudo -u example_com env OCMS_ENV=development OCMS_DO_SEED=true /opt/ocms/bin/ocms

While the process is running, open https://example.com/admin/ and log in with admin@example.com / changeme1234. Change the admin email and password, then stop the foreground process with Ctrl+C.

6. Start the Site

# For production (systemd)
sudo systemctl enable --now ocms@example_com

# Check status
sudo journalctl -u ocms@example_com -f

ocmsctl Reference

ocmsctl <command> <site-id> [options]

Commands:
  start   <site-id> [--foreground]  Start instance
  stop    <site-id>                 Stop instance (SIGTERM, SIGKILL after 60 s)
  restart <site-id>                 Restart instance
  status  <site-id>                 Show status and health
  logs    <site-id> [--follow|-f]   View logs
  logs    <site-id> --errors [--follow]  View the error-only log
  list                              List all sites with status
  health  <site-id>                 Check health endpoint

Deploying Updates

Binary Only (Multiple Instances)

./scripts/deploy/deploy-binary.sh server.example.com site_a site_b site_c

Options: -u USER (SSH user), --skip-build, --dry-run.

The script backs up the running binary, /opt/ocms/bin/ocms, to ocms.backup (a failed backup stops the deploy), uploads the new binary to /opt/ocms/bin/ocms-linux-amd64, installs it as /opt/ocms/bin/ocms while the instances still run (copying it beside that file and renaming it over, so a failed install stops nothing), then restarts the listed instances and waits for each to report healthy (ocmsctl health; HEALTH_RETRIES and HEALTH_INTERVAL set how long). When one does not, the deploy exits 1 and names it. deploy.sh below does the same for one instance. All instances share /opt/ocms/bin/ocms, so instances not listed keep running the old binary until they restart.

With Custom Content

./scripts/deploy/deploy.sh server.example.com my_site \
  -v /var/www/vhosts/example.com/ocms -o hosting

Options: -v VHOST (instance directory), -o OWNER, -g GROUP, --skip-binary, --skip-build, --sync-custom, --dry-run.

Multi-Instance (On Server)

sudo /opt/ocms/deploy-multi.sh /tmp/ocms

Backs up the current binary to /opt/ocms/bin/ocms.backup, installs the new one as /opt/ocms/bin/ocms while the sites still run, stops every site in sites.conf with ocmsctl stop (through systemd or its PID file, whichever runs it), starts every site through systemd, and health-checks each one. A site that fails to stop or start is reported as failed and the others carry on; a site that did not stop is not started, because its old process may still be running.

Syncing Production to Development

./scripts/deploy/sync-prod-to-dev.sh server.example.com my_site \
  -v /var/www/vhosts/example.com/ocms

Options: --no-db, --no-uploads, --no-logs, --sync-custom, --dry-run.

The script stops the production instance while it copies the data, then starts it and waits for it to report healthy (ocmsctl health), exiting 1 when it does not. If a step fails or the sync is interrupted, it starts the instance again before exiting; if the stop fails, it copies nothing.

Backups

# Backup all sites
sudo /opt/ocms/backup-multi.sh

# Backup one site
sudo /opt/ocms/backup-multi.sh example_com

Backups include database, uploads, and custom content (if present). Stored in each site's backups/ directory with 30-day retention.

Health Checks

sudo /opt/ocms/healthcheck-multi.sh

Restarts sites that are running but unhealthy (at most 3 times per outage), reports crash-looping (activating) and failed units without restarting them, skips inactive ones, and exits with the number of sites that are down. Supports optional Slack and email alerting, once per outage. Run it as root: its state lives in /run/ocms-health/, which only root can write.

Cron Jobs

# /etc/cron.d/ocms-multi
SHELL=/bin/bash
0 3 * * * root /opt/ocms/backup-multi.sh >> /var/log/ocms-backup.log 2>&1
*/5 * * * * root /opt/ocms/healthcheck-multi.sh 2>&1 | grep -v "^$"

Log Rotation

sudo /opt/ocms/generate-logrotate.sh

Generates /etc/logrotate.d/ocms from sites.conf (re-run after adding or removing sites). Each site's logs/*.log is rotated daily, 30 days kept, compressed with gzip, using copytruncate.

File Permissions

Path Owner Mode
/opt/ocms/bin/ocms root:root 755
{vhost}/ocms/ {user}:psaserv 750
{vhost}/ocms/.env {user}:psaserv 600
{vhost}/ocms/data/ {user}:psaserv 755
{vhost}/ocms/uploads/ {user}:psaserv 755

Site Registry

Sites are tracked in /etc/ocms/sites.conf:

# SITE_ID INSTANCE_DIR SYSTEM_USER PORT
example_com /var/www/vhosts/example.com/ocms example_com 8081
blog_example_com /var/www/vhosts/blog.example.com/ocms bloguser 8082

Comment out a line to disable a site without removing data.

Removing a Site

sudo systemctl disable --now ocms@example_com
sudo rm -rf /etc/systemd/system/ocms@example_com.service.d
sudo systemctl daemon-reload
sudo rm -rf /var/www/vhosts/example.com/ocms
# Edit /etc/ocms/sites.conf to remove the line

Security Checklist

  • Changed default admin password
  • Strong OCMS_SESSION_SECRET (32+ bytes)
  • OCMS_MFA_KEY set and backed up with the database
  • OCMS_ENV=production
  • SSL/TLS enabled
  • HTTP redirects to HTTPS
  • .env permissions set to 600
  • Firewall configured (only 80/443 open)
  • Backups scheduled
  • Health monitoring enabled

Troubleshooting

Service Won't Start

sudo systemctl status ocms@example_com
sudo journalctl -u ocms@example_com -n 50

Common: missing OCMS_SESSION_SECRET, missing OCMS_TRUSTED_PROXIES / OCMS_API_ALLOWED_CIDRS in production, permission denied on data/, port in use, missing systemd drop-in (see next).

Systemd Drop-In Missing

If the journal says OCMS_SESSION_SECRET is not set but .env exists and contains it, the per-instance drop-in is gone. Confirm:

sudo systemctl cat ocms@example_com                              # no User=/EnvironmentFile= lines?
sudo ls /etc/systemd/system/ocms@example_com.service.d/ 2>&1     # "No such file or directory"?

Recreate it (copy from a working site; values come from /etc/ocms/sites.conf):

sudo mkdir -p /etc/systemd/system/ocms@example_com.service.d
sudo tee /etc/systemd/system/ocms@example_com.service.d/instance.conf > /dev/null <<'EOF'
[Service]
User=<system_user>
Group=psaserv
WorkingDirectory=/var/www/vhosts/example.com/ocms
EnvironmentFile=/var/www/vhosts/example.com/ocms/.env
ReadWritePaths=/var/www/vhosts/example.com/ocms
SyslogIdentifier=ocms-example_com
StandardOutput=append:/var/www/vhosts/example.com/ocms/logs/ocms.log
StandardError=append:/var/www/vhosts/example.com/ocms/logs/ocms.log
EOF
sudo systemctl daemon-reload
sudo systemctl start ocms@example_com

Sites Down After Reboot

Every domain 502s after a server restart; ocmsctl list shows all sites stopped. Cause: units were never systemctl enable-d, so they don't auto-start at boot.

sudo /opt/ocms/bin/ocmsctl list                          # all "stopped"?
sudo systemctl is-enabled 'ocms@*'                       # any "disabled"?
sudo awk '!/^#/ && NF>0 {print $1}' /etc/ocms/sites.conf \
  | xargs -I{} sudo systemctl enable --now ocms@{}

healthcheck-multi.sh does not revive this — it restarts only units that are active but unhealthy, reports crash-looping (activating) and failed ones, and skips inactive ones. systemctl enable is the only reboot-safe fix.

502 Bad Gateway

curl http://127.0.0.1:8081/health
sudo nginx -t

Duplicate Location "/" Error

Plesk generates its own location / block. Use regex pattern, with no URI after the port:

location ~ ^/(.*)$ {
    proxy_pass http://127.0.0.1:8081;
}

Images or URLs With Spaces Return 400

A proxy_pass with a URI, such as proxy_pass http://127.0.0.1:8081/$1$is_args$args;, makes nginx rebuild the upstream request from the decoded path. A request for /uploads/large/x/My%20Photo.jpg then reaches the app with a literal space in the request line, which is malformed, so the app answers 400. Dropping the URI makes nginx forward the original request URI unchanged.

Tell the two cases apart without reading config:

curl -s -o /dev/null -w '%{http_code}\n' "https://example.com/uploads/large/x/a%20b.jpg"
# 404 = encoding handled correctly; 400 = proxy_pass still has a URI

This stays hidden until content has filenames with spaces or other percent-encoded characters, so a site can run a long time before hitting it.

Database Corruption

sqlite3 {vhost}/ocms/data/ocms.db "PRAGMA integrity_check;"
# Restore from backup if needed

Clone this wiki locally