Start

Provision script

provision/provision.sh turns a fresh Ubuntu 24.04 droplet into a host the panel can manage: web server, several PHP versions, MySQL, SSL tooling, process supervision, a deploy user and a basic firewall. Then it starts the panel.

Usage

$ sudo bash provision/provision.sh [options]

Run it as root from the repository. It detects the repository it lives in and uses that as the panel's install directory. Output is shown live and appended to /var/log/server-manager-provision.log.

# Typical
$ bash provision/provision.sh --php "8.2 8.3 8.4" --email you@example.com

# Host only: don't start the panel container
$ bash provision/provision.sh --no-panel

# Panel checkout somewhere else (cloned there if the folder is empty)
$ bash provision/provision.sh --install-dir /srv/server-manager

The repository (git@github.com:SCC-Pro/server-manager.git) is private, so root on the droplet needs a deploy key before cloning. See Installation → Quick start.

Options

Each flag can also be set as an environment variable (e.g. PHP_VERSIONS="8.3 8.4" bash provision.sh).

FlagEnv varDefaultDescription
--php "8.3 8.4"PHP_VERSIONS8.3 8.4Space-separated PHP versions to install side by side (from the ondrej/php PPA). The newest becomes the default CLI php.
--deploy-userDEPLOY_USERdeployLinux user that owns projects and runs PHP-FPM, workers and cron.
--portSM_PORT8090Panel port (also opened in UFW).
--emailADMIN_EMAILadmin@<hostname>Panel login email and default Let's Encrypt contact.
--nodeNODE_MAJOR22Node.js major version (NodeSource).
--swapSWAP_SIZE2GSwap file to create when the droplet has none. 0 skips it.
--timezoneTIMEZONEUTCSystem timezone (cron runs in this timezone).
--ftpINSTALL_FTP=1offAlso install the FTP server (pure-ftpd, TLS, passive ports 40000–40100, fail2ban jail). It can be installed later from the panel instead.
--no-redisINSTALL_REDIS=0installedSkip Redis.
--no-panelINSTALL_PANEL=0startedProvision the host only.
--install-dirINSTALL_DIRthis repo, else /opt/server-managerWhere the panel's docker-compose.yml and .env live.
--repoSM_REPOgit@github.com:SCC-Pro/server-manager.gitGit URL to clone the panel from when the install dir is empty.

What it does

  1. Base system

    Sets the timezone, runs apt upgrade, installs git, curl, unzip, acl, ufw, fail2ban, jq, rsync and cron. Creates a swap file if there is none (with vm.swappiness=10) so Composer and npm don't run out of memory on small droplets.

  2. Package repositories

    Adds ppa:ondrej/php (multiple PHP versions), NodeSource and Docker's official apt repository.

  3. Deploy user

    Creates deploy with home /home/deploy (mode 755 so nginx can traverse it), adds it to www-data, prepares ~/.ssh and pre-trusts the github.com, gitlab.com and bitbucket.org host keys so the first clone doesn't prompt.

  4. nginx

    Removes the stock default site and installs a catch-all sm-default.conf: unknown hostnames get the "parked domain" page from /var/www/sm-parked, and /.well-known/acme-challenge/ is served from /var/www/letsencrypt for every host. Adds conf.d/sm-tuning.conf (hide version, 64 MB uploads, gzip tuning).

  5. PHP

    For each version: FPM, CLI and the extensions Laravel needs (mysql, sqlite3, mbstring, xml, curl, zip, bcmath, intl, gd, readline, opcache, redis). The default FPM pool is switched to run as the deploy user (socket owned by www-data for nginx), and a 99-server-manager.ini sets memory, upload limits, timezone and OPcache.

  6. Composer & Node.js

    Composer from the official installer (checksum verified) into /usr/local/bin; Node.js from NodeSource.

  7. MySQL

    Installs MySQL 8, removes anonymous users and the test database, and creates sm_admin@localhost / @127.0.0.1 with a random 32-character password and ALL PRIVILEGES … WITH GRANT OPTION. That password goes straight into the panel's .env. MySQL stays bound to localhost.

  8. Redis (optional)

    Redis on 127.0.0.1:6379, supervised by systemd: handy for Laravel cache, queues and Horizon.

  9. certbot, Supervisor, cron

    Enables the certbot.timer auto-renewal, adds a deploy hook that reloads nginx after each renewal, and creates /var/log/sm-cron for cron output logs.

  10. Docker

    Docker Engine with the Compose and Buildx plugins.

  11. Firewall

    UFW allows SSH, 80, 443 and the panel port; everything else is denied inbound. fail2ban protects SSH.

  12. Panel

    Creates .env from .env.example (admin email, generated 20-character password, MySQL password, port, deploy user, app key, URL), runs docker compose up -d --build, waits for /up, then prints the URL and credentials (also saved to /root/server-manager-credentials.txt).

Resulting layout

These are the paths the panel reads and writes on the host afterwards:

PathContents
/opt/server-manager/.envPanel configuration and credentials (mode 600).
/home/deploy/<project>Project checkouts. nginx only ever serves the project's public (or chosen) sub-folder.
/home/deploy/.ssh/sm_<project>Per-project read-only deploy keys.
/etc/nginx/sites-available/sm-<domain>.confOne generated vhost per domain (symlinked into sites-enabled).
/etc/nginx/sites-available/sm-default.confCatch-all parked page + ACME challenge.
/var/www/sm-parked/index.htmlThe parked domain page (also shown for unknown hostnames). The panel rewrites it from resources/host/parked.html on every container start. Edit the template, not this file.
/var/www/sm-for-sale/index.htmlThe "domain for sale" page, written by the panel from resources/host/for-sale.html with the enquiry email from Settings, on every container start and when the email changes.
/var/www/letsencryptACME webroot used by certbot --webroot.
/etc/letsencrypt/live/<domain>/Certificates.
/etc/cron.d/sm-cron-<id>One file per cron job.
/etc/supervisor/conf.d/sm-<name>.confOne file per worker; logs in /var/log/supervisor/sm-<name>.log.
/etc/php/<ver>/fpm/pool.d/www.confFPM pool (runs as deploy). Socket: /run/php/php<ver>-fpm.sock.

Files prefixed sm- are generated by the panel and overwritten when you change things in the UI. Don't edit them by hand; change the setting in the panel instead.

Re-running safely

The script is idempotent. Run it again to add a PHP version (--php "8.3 8.4 8.5") or repair a package. Existing users, keys and the swap file are kept. The MySQL admin password is read back from the existing .env rather than regenerated. An existing .env is never overwritten, so no new admin password is generated on re-runs.

Troubleshooting

The script stopped with ✖

Read the last lines of /var/log/server-manager-provision.log, fix the cause, and run the script again: completed steps are skipped or harmlessly repeated.

"nginx config test failed"

Run nginx -t to see the offending file. On a non-fresh droplet another site may conflict with the catch-all default_server.

The panel didn't answer

$ cd /opt/server-manager && docker compose ps
$ docker compose logs --tail 100

Can't reach the panel from your browser

Check ufw status lists the panel port and, if you use a DigitalOcean Cloud Firewall, that it allows it too.