Repository navigation
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.
git clone https://github.com/TheTractorHacker/itflow.git
cd itflow
sudo deploy/install.sh --domain=<fqdn> [options]
sudo deploy/install.sh --helpAll 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.
-
Packages — nginx, MariaDB, PHP 8.4 (via the
ondrej/phpPPA 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. -
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-devruns ifcomposer.jsonis present. -
uploads//backups/+ permissions — creates everyuploads/*subdirectory with its directory-listing-denial placeholder, then sets the whole app directory towww-data:www-data,750directories /640files, widening onlyuploads/andbackups/back to writable. -
Database — generates a random 32-character password, creates a MariaDB database and a same-named user bound to
localhostonly, 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'srestore.shlaterDROP DATABASE/CREATE DATABASEon a restore. -
Redis — installed and, if not already listening there, reconfigured to
127.0.0.1:6380(matchingincludes/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. -
Certificate + nginx vhost — bootstraps a self-signed certificate, deploys a shared
itflow-locations.confsnippet 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-modeor--skip-tls). -
PHP-FPM + MariaDB hardening — applies the same hardening templates [[Updating and Hardening|Updating-and-Hardening]]'s
harden.shdoes, validating each with a config test before restarting the service, rolling back automatically if the test fails. -
Firewall + fail2ban — allows SSH (auto-detected port) and HTTP/HTTPS through
ufwbefore enabling it, then installs thesshdanditflow-authfail2ban jails. -
Cron — installs a system cron entry invoking
cron/cron.phpevery 5 minutes. Inert until you turn on Enable Cron in the app's own Settings (see "After install" below). -
Application setup — runs
scripts/setup_cli.phpaswww-datato writeconfig.php, importdb.sql, and create the first admin user (or, with--restore-from, a throwaway placeholder instance immediately overwritten byrestore.sh). Skipped automatically ifconfig.phpalready exists, so re-runninginstall.shagainst an already-set-up instance is safe.Success is verified by checking
config.phpfor its trailing$config_enable_setup = 0;line, notsetup_cli.php's exit code. Everydie(...)in that script is PHP'sdie()/exit()with a string argument, which always exits0— a bad email, a DB error, a malformeddb.sqlstatement all look, at the shell level, identical to success. That trailing line is the same flagincludes/redirect_if_setup_enabled.phpgates the whole app on, so it's the one signal that's actually reliable.
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 tosetup_cli.phpvia an environment variable, never onsetup_cli.php's own argv (sopscan't see it there) — but it IS visible oninstall.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.
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=USDNginx, 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-passphraseEvery 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.
- Rotate the generated database password if it was ever displayed on a screen someone else could see.
-
Enable "Enable Cron" in the app's own Settings — the system cron entry fires every 5 minutes
regardless, but
cron/cron.phpdoes nothing until this is turned on. - Set up Backup and Restore's passphrase file and scheduled timer.
- Consider also running
sudo deploy/harden.sh— see Updating and Hardening for the extra steps it applies beyond whatinstall.shdoes inline. -
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.phpcreates.