Install on a VM
The VM install puts every piece of Tidal PTC on the server as a regular system package: Nginx and PHP-FPM serve the app, PostgreSQL and Redis run locally, systemd keeps Reverb (websockets) and the queue worker running, and cron runs the scheduler. One script, install-vm.sh, does all of it. The manual steps below are the same install done by hand.
Who this is for
Section titled “Who this is for”District IT staff who want to self-host without containers. If you’re happy with Docker, the Docker install has fewer moving parts.
Before you start
Section titled “Before you start”- Check the requirements: a Debian 12/13 or Ubuntu 24.04/26.04 server, a DNS
Arecord pointing at it, and ports80and443open. - Point your domain’s DNS at the server before running the script, so the certificate request succeeds.
Need a server? A DigitalOcean Droplet works well:
Quick install
Section titled “Quick install”-
Run the install script with your domain and email:
Terminal window curl -fsSL https://raw.githubusercontent.com/archboard/tidal-ptc/main/install-vm.sh \| sudo bash -s -- --domain ptc.example.org --email you@example.org -
Wait for it to finish. It installs PHP 8.5, PostgreSQL 18, Redis, Nginx, Certbot, Node.js and Composer; clones the code into
/var/www/tidal-ptcas atidaluser; creates the database; gets a Let’s Encrypt certificate; writes.env; builds and migrates the app; and sets up the systemd services and the scheduler cron entry. At the end it checks thathttps://<domain>/upreturns200. -
Visit
https://ptc.example.org/installto finish the installation. See First run.
| Option | Description |
|---|---|
--domain DOMAIN |
Public hostname (required; prompted for if omitted). |
--email EMAIL |
Let’s Encrypt expiry notices (optional). |
--version REF |
Git branch or tag to deploy (default: main). |
--no-tls |
Skip Let’s Encrypt and serve plain HTTP, for example behind a load balancer that terminates TLS. |
--dir PATH |
Install directory (default: /var/www/tidal-ptc). |
-h, --help |
Show the options. |
The script is safe to re-run: it keeps existing secrets and .env values, and pulls, rebuilds, migrates and restarts everything. Re-run it to update, after editing .env, or after fixing DNS if the certificate request failed.
Manual install
Section titled “Manual install”Run the commands as a normal user with sudo. Replace ptc.example.org with your domain throughout.
1. Install PHP 8.5
Section titled “1. Install PHP 8.5”Ubuntu 26.04 ships PHP 8.5 in universe. Older Debian and Ubuntu releases need Ondřej Surý’s repository.
sudo apt-get updatesudo apt-get install -y ca-certificates curl git unzip lsb-release
# Debian 12 / 13, Ubuntu 24.04 (skip on Ubuntu 26.04)curl -sSL https://packages.sury.org/php/README.txt | sudo bash -x
sudo apt-get updatesudo apt-get install -y php8.5-fpm php8.5-cli php8.5-pgsql php8.5-redis php8.5-intl \ php8.5-zip php8.5-mbstring php8.5-xml php8.5-curl php8.5-bcmath2. Install Composer
Section titled “2. Install Composer”curl -sS https://getcomposer.org/installer | phpsudo mv composer.phar /usr/local/bin/composerDon’t use the distro composer package; it pulls in the distro’s older PHP.
3. Install Node.js
Section titled “3. Install Node.js”Node.js is only needed to build the front-end assets.
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -sudo apt-get install -y nodejs4. Install PostgreSQL 18
Section titled “4. Install PostgreSQL 18”From the official PostgreSQL apt repository:
sudo apt-get install -y postgresql-commonsudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -ysudo apt-get install -y postgresql-18Create the database and its user. Keep the password for .env.
DB_PASSWORD=$(openssl rand -hex 24); echo "$DB_PASSWORD"sudo -u postgres psql -c "CREATE ROLE tidal LOGIN PASSWORD '$DB_PASSWORD';"sudo -u postgres psql -c "CREATE DATABASE tidal_ptc OWNER tidal;"PostgreSQL only listens on localhost by default, which is what you want.
5. Install Redis
Section titled “5. Install Redis”sudo apt-get install -y redis-serversudo systemctl enable --now redis-serverThe packaged config binds to 127.0.0.1 only, so no password is needed for a single-server install.
6. Install Nginx and Certbot
Section titled “6. Install Nginx and Certbot”sudo apt-get install -y nginx certbot python3-certbot-nginxsudo systemctl enable --now nginx7. Get the code
Section titled “7. Get the code”The app, PHP-FPM, the queue worker and Reverb all run as one dedicated tidal user, so files written by any of them (logs, cache) are writable by the others.
sudo useradd --create-home --shell /bin/bash tidalsudo mkdir -p /var/www/tidal-ptcsudo chown tidal:tidal /var/www/tidal-ptcsudo -u tidal git clone https://github.com/archboard/tidal-ptc.git /var/www/tidal-ptcmain is the latest code. To run a release instead, run sudo -u tidal git -C /var/www/tidal-ptc checkout v1.2.3 (tags match the GitHub releases).
8. Configure .env
Section titled “8. Configure .env”sudo -iu tidalcd /var/www/tidal-ptccp .env.example .envsed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" .envEdit .env and set the values listed in Configuration. Leave TRUSTED_PROXIES unset: Nginx talks to PHP-FPM directly over FastCGI, so the app already sees the real client IP and HTTPS.
9. Build and migrate
Section titled “9. Build and migrate”Still as the tidal user in /var/www/tidal-ptc:
composer install --no-dev --optimize-autoloader --no-interactionnpm ci && npm run buildphp artisan migrate --forcephp artisan optimizeexitphp artisan optimize caches config and routes, so rerun it after every .env change.
10. PHP-FPM pool
Section titled “10. PHP-FPM pool”Give the app its own pool running as tidal. Create /etc/php/8.5/fpm/pool.d/tidal-ptc.conf:
[tidal-ptc]user = tidalgroup = tidallisten = /run/php/tidal-ptc.socklisten.owner = www-datalisten.group = www-data
pm = dynamicpm.max_children = 10pm.start_servers = 2pm.min_spare_servers = 1pm.max_spare_servers = 3pm.max_requests = 500sudo systemctl restart php8.5-fpmRaise pm.max_children if you have the RAM (roughly 50 MB per child).
11. Nginx
Section titled “11. Nginx”Create /etc/nginx/sites-available/tidal-ptc. Start with plain HTTP; Certbot adds TLS in the next step.
map $http_upgrade $connection_upgrade { default upgrade; '' close;}
server { listen 80; server_name ptc.example.org; root /var/www/tidal-ptc/public; index index.php;
charset utf-8;
# Reverb websockets location ~ ^/apps?(/|$) { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 86400; }
location / { try_files $uri $uri/ /index.php?$query_string; }
location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; }
location ~ ^/index\.php(/|$) { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/tidal-ptc.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_hide_header X-Powered-By; }
location ~ /\.(?!well-known).* { deny all; }}Only index.php is executed; any other .php file under public/ is served as a 404.
sudo ln -s /etc/nginx/sites-available/tidal-ptc /etc/nginx/sites-enabled/sudo rm -f /etc/nginx/sites-enabled/defaultsudo nginx -t && sudo systemctl reload nginxsudo certbot --nginx -d ptc.example.org --redirectCertbot rewrites the server block to listen on 443 with the new certificate, adds the HTTP to HTTPS redirect, and installs a systemd timer that renews automatically. sudo certbot renew --dry-run confirms renewal works.
12. Reverb and the queue worker
Section titled “12. Reverb and the queue worker”Create /etc/systemd/system/tidal-ptc-reverb.service:
[Unit]Description=Tidal PTC Reverb websocket serverAfter=network.target redis-server.service
[Service]User=tidalGroup=tidalWorkingDirectory=/var/www/tidal-ptcExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080Restart=alwaysRestartSec=3LimitNOFILE=10000
[Install]WantedBy=multi-user.targetAnd /etc/systemd/system/tidal-ptc-queue.service:
[Unit]Description=Tidal PTC queue workerAfter=network.target redis-server.service postgresql.service
[Service]User=tidalGroup=tidalWorkingDirectory=/var/www/tidal-ptcExecStart=/usr/bin/php artisan queue:work redis --queue=default,sis_sync --tries=3 --max-time=3600Restart=alwaysRestartSec=3
[Install]WantedBy=multi-user.target--queue=default,sis_sync processes mail and notifications before PowerSchool syncs. --max-time=3600 makes the worker exit hourly to release memory; systemd starts a fresh one.
sudo systemctl daemon-reloadsudo systemctl enable --now tidal-ptc-reverb tidal-ptc-queue13. Scheduler
Section titled “13. Scheduler”Reminders and activity-log pruning run from the Laravel scheduler. Add it to the tidal user’s crontab:
echo '* * * * * cd /var/www/tidal-ptc && php artisan schedule:run >> /dev/null 2>&1' | sudo crontab -u tidal -Verify
Section titled “Verify”https://ptc.example.org/upreturns200.systemctl status tidal-ptc-reverb tidal-ptc-queueshows bothactive (running).- Open the browser dev tools on any page: there should be a websocket connection to
wss://ptc.example.org/app/<REVERB_APP_KEY>with status101. If it fails, see Real-time updates. - Errors land in
/var/www/tidal-ptc/storage/logs/and/var/log/nginx/error.log.
Then visit https://ptc.example.org/install to finish the installation.
Day-to-day operations
Section titled “Day-to-day operations”Run app commands as the tidal user: sudo -iu tidal, then cd /var/www/tidal-ptc.
| Task | Command |
|---|---|
| Run Artisan | sudo -u tidal php /var/www/tidal-ptc/artisan <command> |
| Tail app logs | tail -f /var/www/tidal-ptc/storage/logs/laravel-*.log |
| Tail service logs | journalctl -fu tidal-ptc-queue -u tidal-ptc-reverb |
After changing .env, re-cache the config and restart the services:
sudo -u tidal php /var/www/tidal-ptc/artisan optimizesudo systemctl reload php8.5-fpm && sudo systemctl restart tidal-ptc-queue tidal-ptc-reverb