{brand:short_name} · Deployment manual

Putting {brand:short_name} on its cPanel account, from an empty account to a working console at api.tutelora.com, then every release after.

Kept with the code · checked against develop on 9 November 2026

{brand:short_name} is one Laravel app answering on several names. On cPanel it lives in one folder, ~/atrium, set up as the sub-domain api.tutelora.com (the console and the products' API). id.tutelora.com (where people sign in, and where every e-mail's link goes) points at the same folder. The app works out from the name which part of it answers.

Three names point at one folder on the cPanel account api.tutelora.comthe console, the products’ API id.tutelora.comsign-in, accounts, organisations tutel.inkshort links · later, when its DNS is ready THE CPANEL ACCOUNT ~/atrium/public one document root for every name · PostgreSQL the scheduler each minute works the queue

Work top to bottom. Tick each step as you finish it: the ticks stay in this browser, so you can stop and come back. Everything typed into a terminal happens over SSH on the cPanel account unless a block says on your Mac. Where a command says <you>, put the cPanel account's user name.

0 of 0 steps

00Before day one

Three things decide whether the account can run {brand:short_name} at all. Ask the host before paying for anything else.

The host must give ask first

  • PostgreSQL 15 or later, with the pg_trgm extension made in the database (a cPanel user can't make it: the host does, once).
  • PHP 8.4 or later (8.5 preferred) with pdo_pgsql, intl, sodium, mbstring, zip, dom, gd.
  • SSH access (Terminal in cPanel will do), Git and Composer.
  • Cron jobs, every minute.

The Architect brings yours

  • The cPanel account and its server's IP address.
  • Access to tutelora.com's DNS.
  • Admin on the GitHub repository tutelora/atrium (to add a deploy key).
  • A password manager entry ready for the secrets made on the way.
  • An SMTP mailer for {brand:company}'s own e-mail (sign-in links, invitations).

Not on this account later

  • No virus scanner (ClamAV), so it runs as staging: uploads aren't scanned, and the readiness check says so.
  • No Redis or supervisor: the database queue, worked by the scheduler.
  • Those come with the VPS, the production home ({brand:short_name}'s plan AF, A8).
Why "staging" on real names. The app refuses to call itself production without a virus scanner (app:check fails), and cPanel has none. So this account runs APP_ENV=staging on the real names: everything works, uploads go unscanned, and payments stay on Paystack's test keys. Moving to the VPS changes the server, not the names: the products and the people never see the move.

01The account and its names

1

Check PHP and PostgreSQL

In cPanel, MultiPHP Manager sets the PHP version per domain; set 8.5 (or 8.4) once the domains exist (step 3). In the terminal, find the binary cron will use and check its extensions:

ls /usr/local/bin/ea-php8*                 # e.g. /usr/local/bin/ea-php85
/usr/local/bin/ea-php85 -v
/usr/local/bin/ea-php85 -m | grep -Ei 'pdo_pgsql|intl|sodium|mbstring|zip|dom|gd'

Every one of the seven must print. A missing one is a ticket to the host (or Select PHP Version → Extensions on CloudLinux).

2

Make the database

cPanel → PostgreSQL Databases: create a database atriumdb and a user atrium with a long generated password, then add the user to the database with all privileges. cPanel prefixes both with the account's name (<you>_atriumdb, <you>_atrium): use the prefixed names everywhere below.

Then ask the host, in a ticket, to run this as a superuser and to confirm the server's version:

the host runs it
\c <you>_atriumdb
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT version();                          -- 15 or later
Stop here if the answer is "no". A version before 15, or no pg_trgm, can't run {brand:short_name}: a migration needs PostgreSQL 15's SET NULL (column), and search needs trigrams. Nothing on cPanel stands in for either.
3

Make the two names, both on one folder

cPanel → Domains → Create A New Domain:

  1. api.tutelora.com, untick "Share document root", document root atrium/public.
  2. id.tutelora.com, untick "Share document root", document root the same atrium/public.

The folder doesn't exist yet; cPanel makes it, and step 5 fills it. Set both to PHP 8.5 in MultiPHP Manager.

4

Point the DNS, then the certificates

Where tutelora.com's DNS lives, add two records to the account's IP address (cPanel shows it as Shared IP Address on its home page):

NameTypeValue
apiAthe account's IP
idAthe account's IP

If the zone is in this cPanel's Zone Editor, step 3 already made them. Once both names resolve (dig +short api.tutelora.com), cPanel → SSL/TLS Status → tick both → Run AutoSSL. Both must show a valid certificate: the session cookie is __Host-, which browsers keep only over https.

02The code

5

Give the account a deploy key, and clone

The repository is private. Make a key on the account that can only read it:

ssh-keygen -t ed25519 -N '' -C 'atrium@cpanel' -f ~/.ssh/atrium_deploy
cat ~/.ssh/atrium_deploy.pub

On GitHub, tutelora/atrium → Settings → Deploy keys → Add deploy key: paste it, title "cPanel", leave write access off. Then tell SSH to use it, and clone develop into the folder cPanel made (empty it first):

cat >> ~/.ssh/config <<'EOF'
Host github-atrium
  HostName github.com
  User git
  IdentityFile ~/.ssh/atrium_deploy
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
rm -rf ~/atrium && git clone -b develop git@github-atrium:tutelora/atrium.git ~/atrium
6

Install what the app needs

cd ~/atrium
/usr/local/bin/ea-php85 $(which composer) install --no-dev --optimize-autoloader --no-interaction
chmod -R ug+rwX storage bootstrap/cache

If which composer finds nothing, cPanel keeps it at /opt/cpanel/composer/bin/composer.

The pages' assets are built by Node. If the account has it (node -v, 20 or later), build there. If not, build on your Mac and upload:

on the account, with Node
npm ci --no-audit --no-fund && npm run build
or on your Mac
cd ~/dev/Tutelora/tuteatrium && git checkout develop && git pull
npm ci && npm run build
rsync -az --delete public/build/ <you>@<the account's host>:atrium/public/build/

03The settings and the secrets

7

Make the secrets

Each prints once. Copy each into the password manager as it appears, then into .env (step 8). Never into git, chat or a ticket.

cd ~/atrium
cp deploy/production.env.example .env && chmod 600 .env
/usr/local/bin/ea-php85 artisan key:generate --show          # APP_KEY
/usr/local/bin/ea-php85 artisan vault:key                    # FILE_VAULT_KEY
/usr/local/bin/ea-php85 artisan backup:key                   # BACKUP_PUBLIC_KEY here; its private half: the password manager only
/usr/local/bin/ea-php85 -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'   # VOUCHER_KEY: set once, never changed
/usr/local/bin/ea-php85 artisan passport:keys               # sign-in keys for the products, written under storage/
Two keys must never change once used. FILE_VAULT_KEY opens every uploaded file, and VOUCHER_KEY finds every voucher code. Lose either and those are gone. The sign-in keys can be replaced: products fetch the new public key on their own.
8

Fill in .env

Open ~/atrium/.env (cPanel's File Manager editor, or nano). It holds the production example; change these lines to the values for this account, and fill every <…> left:

APP_ENV=staging
APP_DEBUG=false
APP_URL=https://id.tutelora.com
BRAND_APP_HOST=id.tutelora.com
HOST_ID=id.tutelora.com
HOST_API=api.tutelora.com
HOST_INK=tutel.ink                       # answers once tutel.ink's DNS points here
HOST_FILES=                              # empty until a files host has its DNS

APP_KEY=<from step 7>
DB_DATABASE=<you>_atriumdb
DB_USERNAME=<you>_atrium
DB_PASSWORD=<the database user's password>

QUEUE_CONNECTION=database                # no Redis here
QUEUE_VIA_SCHEDULER=true                 # the scheduler works the queue each minute
FILE_VAULT_SCANNER=fake                  # no ClamAV here: uploads aren't scanned
FILE_VAULT_KEY=<from step 7>
BACKUP_PATH=/home/<you>/backups          # outside the app's folder
BACKUP_PUBLIC_KEY=<from step 7>
BACKUP_REMOTE=                           # empty: backups stay on the account until rclone is set
VOUCHER_KEY=<from step 7>

MAIL_MAILER=smtp
MAIL_HOST=<the mailer's SMTP host>
MAIL_USERNAME=<its user>
MAIL_PASSWORD=<its password>
MAIL_FROM_ADDRESS=<the sender, e.g. no-reply@tutelora.com>
TRUSTED_PROXIES=                         # empty, unless the host says a proxy sits in front: then its address
CSP_MODE=report                          # a week of reports first, then enforce
SCHEDULE_HEARTBEAT_URL=                  # empty unless an uptime check gives one

Leave REDIS_*, the SES lines, PASSPORT_* (the key files under storage/ serve), Google, Microsoft and Turnstile as they are until each is set up: the app runs without them and says so. SERVICE_IDENTITY is written for you in step 10.

04The first run

9

Run the deploy steps

One command does what every release does, in order, and stops at the first that fails: the migrations, the Input Fields lists, the roles, the e-mails and their checks, the mail log's partitions, the brand's check, and the caches.

cd ~/atrium && mkdir -p ~/backups
/usr/local/bin/ea-php85 artisan app:deploy

Each step prints a ✓ and its time, then "Deployed: N steps". A failure names its step and stops; fix it and run the command again: each step is safe to repeat.

10

Make the Service, the platform's own account

/usr/local/bin/ea-php85 artisan service:create <the Service's e-mail> --first="<first name>" --last="<family name>"
/usr/local/bin/ea-php85 artisan optimize      # service:create wrote SERVICE_IDENTITY into .env: cache it again

It shows a generated password once: into the password manager. Then sign in at https://api.tutelora.com/login and set up two-factor at once. The Service can't be deleted or have its e-mail changed from the web; service:whoami says who it is.

11

Start the clock: one cron line

cPanel → Cron Jobs → Common settings "Once Per Minute" → command:

cd /home/<you>/atrium && /usr/local/bin/ea-php85 artisan schedule:run >> /dev/null 2>&1

It runs everything timed: the queue (e-mails, webhooks to products, background work), the checks of the products' servers, backups, purges and the readiness record. Without it nothing is sent.

12

Check it's ready

/usr/local/bin/ea-php85 artisan app:check
/usr/local/bin/ea-php85 artisan backup:run      # the first backup, into ~/backups, encrypted

Expect passes, and these, which are right for this account: the scanner off (fake), the page policy reporting, no remote for backups, and the sign-in providers not set. Any fail must be fixed before anyone else signs in. The console's Operations page shows the same lines.

13

See it answer

OpenExpect
https://api.tutelora.com/up200, Laravel's health page: it answers only when the database and the queue do
https://id.tutelora.com{brand:company}'s sign-in page
https://api.tutelora.com/consolethe console's dashboard, signed in as the Service, with {brand:short_name} at its centre
https://api.tutelora.com/v1/ping401 as JSON, "Not signed in": the API answers, and refuses a call without a key
Any other name on the account404, on purpose

05Every release after

cPanel has no zero-downtime deployer, so a release updates the folder in place and takes a few seconds of maintenance. Only develop is deployed.

cd ~/atrium
P=/usr/local/bin/ea-php85
$P artisan down --retry=30                                   # "back in a moment" for anyone who calls meanwhile
git pull --ff-only origin develop
$P $(which composer) install --no-dev --optimize-autoloader --no-interaction
# the assets: npm ci && npm run build here, or rsync public/build from your Mac (step 6)
$P artisan backup:run --kind=predeploy                       # the database as it was, before this release
$P artisan app:deploy
$P artisan queue:restart
$P artisan up
$P artisan app:check

If app:deploy stops on a step, the site stays in maintenance: fix the cause, run it again, then up. A release that must be undone goes back to the previous commit (git log --oneline -5, git checkout <hash>, then the same steps). Migrations only go forward: undoing one that changed data means restoring the predeploy backup (backup:list, backup:restore <name>; reji/docs/BACKUP.md).

06When something's wrong

You seeLook atUsually
500 on every pagestorage/logs/app.json-<date>.log, the last linesA missing .env value, the wrong PHP for the domain, or storage/ not writable
404 on a name that should workHOST_ID, HOST_API in .env, then artisan optimizeA name spelt differently from the one the browser uses
Signed in, then signed out again at onceThe certificate; whether the host puts a proxy in front (then its address in TRUSTED_PROXIES)The __Host- cookie needs https, and the app must see it
No e-mail arrivesThe cron line; app:check's mail line; failed_jobsThe scheduler isn't running, or the SMTP login is wrong
"419" or a form that silently resetsSESSION_DOMAINIt must stay empty: each name keeps its own session
A migration fails on SET NULLSELECT version();PostgreSQL older than 15: step 2's stop
Search finds nothing\dx in the databasepg_trgm wasn't made
A change to .env does nothing—The config is cached: artisan optimize after every .env change

07The weeks after

WhenWhatWhere it's written
After a week of useRead the csp.violation lines, fix any that are {brand:short_name}'s own, then CSP_MODE=enforcereji/docs/DEPLOYMENT.md
Before a text is sentThe company's Sailup key on the console (Messages → Channels), and a sender namereji/docs/reference/notify.md
Before a product sends e-mailSES: each product's notify.<domain> verified, its configuration set, the SNS topic in NOTIFY_SES_TOPICSreji/docs/DEPLOYMENT.md
Before a short link is senttutel.ink's DNS and certificate on the same folderreji/docs/reference/links.md
When the keys comeTurnstile, Google, Microsoft; Paystack's test keys and its webhook https://api.tutelora.com/hook/paystackreji/docs/DEPLOYMENT.md, step 6
The first productIts server on the console, server:sign-in <srv_…>, then Legatus in the product, from its own folderreji/tasks/other-apps.md, packages/legatus/README.md
The VPSRedis, Supervisor and ClamAV: APP_ENV=production, the Redis queue, the real scannerreji/docs/DEPLOYMENT.md, "A new server"

The Markdown guide (reji/docs/DEPLOYMENT.md) stays the full reference, VPS included; this manual is the cPanel path through it, step by step. A step that changes in the code changes here in the same commit.