Architecture & layout
tulixhost runs every site under its own system user. nginx and PHP-FPM are shared daemons, but each site has a dedicated FPM pool socket, MariaDB user, and Redis ACL. There is no global "web" user that owns everything.
Per-site directory tree
Every site lives under /data/web/<site>/. The layout is fixed
because all scripts and the demo dashboard rely on it.
/data/web/example.com/
├── public/ ← webroot, served by nginx
│ └── index.php
├── conf/
│ ├── .envtulix ← secrets — mode 0640, root:web_example_com
│ ├── php.ini ← soft PHP settings (editable by site owner)
│ ├── php-fpm.conf ← symlink to /etc/php/<ver>/fpm/pool.d/example.com.conf
│ ├── my.cnf ← client + session caps for MariaDB
│ ├── redis.conf ← db number, prefix, soft limits
│ ├── nginx-extra.conf ← per-site nginx directives (included from the vhost)
│ └── ssl/
│ ├── example.com.crt
│ └── example.com.key
├── logs/
│ ├── nginx-access.log
│ ├── nginx-error.log
│ ├── php-error.log
│ ├── php-fpm-slow.log
│ ├── php-fpm-access.log
│ └── app.log ← your application's log (optional)
├── tmp/ ← PHP sessions, FPM socket, upload tmp, sys_temp_dir
│ └── php-fpm.sock
├── cron/
│ └── crontab ← editable source for /etc/cron.d/tulixhost-example.com
└── backups/ ← reserved (site-local; actual backups go to /data/backups/)
Per-site system user
For a site named example.com, the system user is
web_example_com. Dots and dashes are replaced with underscores; the
web_ prefix is fixed. The user is created with:
useradd --system --no-create-home \
--home-dir /data/web/example.com \
--shell /usr/sbin/nologin web_example_com
www-data is added to the per-site group so nginx can read static
files. The site's PHP-FPM pool runs as the per-site user, so:
- PHP code from site A cannot
open()files in site B (filesystem ACLs +open_basedir) - A compromised site's processes can't
killorstraceanother site's workers - Crontab entries run as the per-site user, not root
Shared daemons
| daemon | config | per-site bit |
|---|---|---|
| nginx | /etc/nginx/ | sites-available/<site>.conf |
| PHP-FPM | /etc/php/<ver>/fpm/ | pool.d/<site>.conf (one pool per site) |
| MariaDB | /etc/mysql/ | one database + one user per site |
| Redis | /etc/redis/redis.conf | one ACL user + one DB number + one key prefix |
Where the toolkit itself lives
/etc/tulixhost/tulixhost.conf | Main config — PHP version, Redis admin pass, retention. |
/usr/local/sbin/tulixhost-* | Symlinks to the local *.sh scripts. |
/var/log/tulixhost/audit.log | Every privileged invocation. |
/var/log/tulixhost/backup.log | Output of the nightly backup_all cron. |
/etc/cron.d/tulixhost | The master nightly backup cron. |
/etc/cron.d/tulixhost-<site> | One file per site with cron entries. |
/etc/logrotate.d/tulixhost | Rotation for site logs. |
/etc/nginx/snippets/tulixhost-security.conf | Global security headers + dotfile deny. |
/etc/nginx/snippets/tulixhost-tls.conf | TLS defaults (Mozilla intermediate). |
/etc/nginx/conf.d/00-tulixhost-default.conf | Catch-all returning 444 to unknown hostnames. |
/etc/mysql/mariadb.conf.d/99-tulixhost.cnf | Server-wide MariaDB hardening. |
/etc/redis/users.acl | Redis ACL file — one line per site. |
Why the layout matters
Everything for one site is under one path. That means:
- Backup is one
tarcommand plus amysqldumpplus a Redis SCAN+DUMP. - Removing a site removes the path, plus a handful of well-known files outside it.
- Migrating a site is restoring a tarball on a new box — the
.envtulixcarries the DB and Redis passwords with it, andrestore_site.shrecreates the DB user with that same password. - You can grep any log without escalating:
tail -F /data/web/example.com/logs/*.logas the per-site user works.