Skip to content

Bare Metal Install

Blake Foley edited this page Sep 17, 2026 · 1 revision

Bare Metal Install

deploy/install.sh stands up a brand-new, standalone ITFlow instance on a fresh (or already-running) Ubuntu/Debian box, or adds another company's independent instance (own vhost, own database, own database user) alongside one or more instances already running on the same box. This is not a multi-tenant installer — every company gets its own separate database and its own separate config.php.

For local evaluation instead, see Docker Compose.

Usage

git clone https://github.com/TheTractorHacker/itflow.git
cd itflow
sudo deploy/install.sh --domain=<fqdn> [options]
sudo deploy/install.sh --help

All scripts under deploy/ must run as root — they touch /etc, install packages, and manage services. Every one of them logs what it's about to do before doing anything invasive (a service restart, ufw enable, a destructive file write), and none of them disable SSH access as a side effect.

What it does, in order

  1. Packages — nginx, MariaDB, PHP 8.4 (via the ondrej/php PPA if your distro's default repos don't carry it), Redis, certbot, ufw, fail2ban, git, composer. Anything already installed (e.g. because another instance already runs on this box) is left alone.

  2. Application code — if run from inside an existing checkout, that checkout is copied into the new instance's app directory; otherwise it's cloned fresh from GitHub. composer install --no-dev runs if composer.json is present.

  3. uploads//backups/ + permissions — creates every uploads/* subdirectory with its directory-listing-denial placeholder, then sets the whole app directory to www-data:www-data, 750 directories / 640 files, widening only uploads/ and backups/ back to writable.

  4. Database — generates a random 32-character password, creates a MariaDB database and a same-named user bound to localhost only, with exactly the grants the app needs (SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES, LOCK TABLES, CREATE TEMPORARY TABLES — nothing global). Those same grants are what let Backup and Restore's restore.sh later DROP DATABASE/CREATE DATABASE on a restore.

  5. Redis — installed and, if not already listening there, reconfigured to 127.0.0.1:6380 (matching includes/redis_functions.php's hardcoded expectation). It's a box-level shared service: a second company's install on the same box detects it's already correctly configured and leaves it alone. The app degrades gracefully without it — only live ticket/chat push is affected.

  6. Certificate + nginx vhost — bootstraps a self-signed certificate, deploys a shared itflow-locations.conf snippet once per box, renders the vhost for your domain, and either runs certbot (direct-TLS mode) or leaves the self-signed cert in place long-term (--proxy-mode or --skip-tls).

  7. PHP-FPM + MariaDB hardening — applies the same hardening templates [[Updating and Hardening|Updating-and-Hardening]]'s harden.sh does, validating each with a config test before restarting the service, rolling back automatically if the test fails.

  8. Firewall + fail2ban — allows SSH (auto-detected port) and HTTP/HTTPS through ufw before enabling it, then installs the sshd and itflow-auth fail2ban jails.

  9. Cron — installs a system cron entry invoking cron/cron.php every 5 minutes. Inert until you turn on Enable Cron in the app's own Settings (see "After install" below).

  10. Application setup — runs scripts/setup_cli.php as www-data to write config.php, import db.sql, and create the first admin user (or, with --restore-from, a throwaway placeholder instance immediately overwritten by restore.sh). Skipped automatically if config.php already exists, so re-running install.sh against an already-set-up instance is safe.

    Success is verified by checking config.php for its trailing $config_enable_setup = 0; line, not setup_cli.php's exit code. Every die(...) in that script is PHP's die()/exit() with a string argument, which always exits 0 — a bad email, a DB error, a malformed db.sql statement all look, at the shell level, identical to success. That trailing line is the same flag includes/redirect_if_setup_enabled.php gates the whole app on, so it's the one signal that's actually reliable.

Key flags

Full reference: sudo deploy/install.sh --help.

  • --domain=<fqdn> — required. Public hostname for this instance.
  • --proxy-mode — this box sits behind an external reverse proxy that already terminates public TLS. Skips certbot, serves a self-signed backend cert on :8443, keeps nginx's redirects relative.
  • --skip-tls — no public DNS yet / TLS configured later by hand.
  • --non-interactive — fail instead of prompting for anything missing.
  • --admin-password=... — avoid on an interactive terminal. Forwarded to setup_cli.php via an environment variable, never on setup_cli.php's own argv (so ps can't see it there) — but it IS visible on install.sh's own command line/shell history. Leave it out and answer the interactive prompt instead whenever you have a terminal, which touches neither.
  • --restore-from=<path> / --restore-passphrase-file=<path> — stand up this new box from an existing backup instead of a fresh company setup. See Backup and Restore.

Worked examples

Fresh dedicated box:

sudo deploy/install.sh \
  --domain=itflow.example.com \
  --email=admin@example.com \
  --company-name="Example Co" --country="United States" \
  --locale=en_US --timezone=America/New_York --currency=USD
# (admin name/email/password prompted interactively)

Adding a second company to a box that already runs one instance:

sudo deploy/install.sh \
  --domain=itflow2.example.com \
  --db-name=itflow2 \
  --email=admin@example.com \
  --company-name="Second Co" --country="United States" \
  --locale=en_US --timezone=America/New_York --currency=USD

Nginx, PHP-FPM, MariaDB, and Redis are already installed and running — install.sh detects that and skips reinstalling them. It provisions a brand-new app directory, database/user, and nginx vhost, side by side with the first. Shared ufw/fail2ban state and the shared nginx rate-limit zone are reused, not duplicated.

A new box from an existing backup (disaster recovery):

sudo deploy/install.sh \
  --domain=itflow.example.com \
  --email=admin@example.com \
  --restore-from=/root/backup-itflow-20260101T023000Z.tar.gz.enc \
  --restore-passphrase-file=/etc/itflow/backup-passphrase

Every company/localization/admin-user flag is ignored in this mode. Packages, TLS, hardening, firewall, and cron are provisioned exactly as in a fresh install; the setup step writes a throwaway placeholder config.php/schema (never actually used) and hands off to deploy/restore.sh, which overwrites the database and uploads/ with the real backup's contents.

After install

  1. Rotate the generated database password if it was ever displayed on a screen someone else could see.
  2. Enable "Enable Cron" in the app's own Settings — the system cron entry fires every 5 minutes regardless, but cron/cron.php does nothing until this is turned on.
  3. Set up Backup and Restore's passphrase file and scheduled timer.
  4. Consider also running sudo deploy/harden.sh — see Updating and Hardening for the extra steps it applies beyond what install.sh does inline.
  5. Enable MFA on the admin account — TOTP and WebAuthn/passkeys are both real, working features, not turned on by default for the account setup_cli.php creates.

Clone this wiki locally