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).
| Flag | Env var | Default | Description |
|---|---|---|---|
--php "8.3 8.4" | PHP_VERSIONS | 8.3 8.4 | Space-separated PHP versions to install side by side (from the ondrej/php PPA). The newest becomes the default CLI php. |
--deploy-user | DEPLOY_USER | deploy | Linux user that owns projects and runs PHP-FPM, workers and cron. |
--port | SM_PORT | 8090 | Panel port (also opened in UFW). |
--email | ADMIN_EMAIL | admin@<hostname> | Panel login email and default Let's Encrypt contact. |
--node | NODE_MAJOR | 22 | Node.js major version (NodeSource). |
--swap | SWAP_SIZE | 2G | Swap file to create when the droplet has none. 0 skips it. |
--timezone | TIMEZONE | UTC | System timezone (cron runs in this timezone). |
--ftp | INSTALL_FTP=1 | off | Also install the FTP server (pure-ftpd, TLS, passive ports 40000–40100, fail2ban jail). It can be installed later from the panel instead. |
--no-redis | INSTALL_REDIS=0 | installed | Skip Redis. |
--no-panel | INSTALL_PANEL=0 | started | Provision the host only. |
--install-dir | INSTALL_DIR | this repo, else /opt/server-manager | Where the panel's docker-compose.yml and .env live. |
--repo | SM_REPO | git@github.com:SCC-Pro/server-manager.git | Git URL to clone the panel from when the install dir is empty. |
What it does
- 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 (withvm.swappiness=10) so Composer and npm don't run out of memory on small droplets. - Package repositories
Adds
ppa:ondrej/php(multiple PHP versions), NodeSource and Docker's official apt repository. - Deploy user
Creates
deploywith home/home/deploy(mode 755 so nginx can traverse it), adds it towww-data, prepares~/.sshand pre-trusts the github.com, gitlab.com and bitbucket.org host keys so the first clone doesn't prompt. - 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/letsencryptfor every host. Addsconf.d/sm-tuning.conf(hide version, 64 MB uploads, gzip tuning). - 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-datafor nginx), and a99-server-manager.inisets memory, upload limits, timezone and OPcache. - Composer & Node.js
Composer from the official installer (checksum verified) into
/usr/local/bin; Node.js from NodeSource. - MySQL
Installs MySQL 8, removes anonymous users and the test database, and creates
sm_admin@localhost/@127.0.0.1with a random 32-character password andALL PRIVILEGES … WITH GRANT OPTION. That password goes straight into the panel's.env. MySQL stays bound to localhost. - Redis (optional)
Redis on
127.0.0.1:6379, supervised by systemd: handy for Laravel cache, queues and Horizon. - certbot, Supervisor, cron
Enables the
certbot.timerauto-renewal, adds a deploy hook that reloads nginx after each renewal, and creates/var/log/sm-cronfor cron output logs. - Docker
Docker Engine with the Compose and Buildx plugins.
- Firewall
UFW allows SSH, 80, 443 and the panel port; everything else is denied inbound. fail2ban protects SSH.
- Panel
Creates
.envfrom.env.example(admin email, generated 20-character password, MySQL password, port, deploy user, app key, URL), runsdocker 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:
| Path | Contents |
|---|---|
/opt/server-manager/.env | Panel 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>.conf | One generated vhost per domain (symlinked into sites-enabled). |
/etc/nginx/sites-available/sm-default.conf | Catch-all parked page + ACME challenge. |
/var/www/sm-parked/index.html | The 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.html | The "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/letsencrypt | ACME 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>.conf | One file per worker; logs in /var/log/supervisor/sm-<name>.log. |
/etc/php/<ver>/fpm/pool.d/www.conf | FPM 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.