Deploy with Deployer
Deployer is a free deployment tool for PHP apps. You run it on your own computer. It signs in to your server over SSH and puts every version of Chatvia in a folder of its own:
/var/www/chatvia
├── current -> releases/3 the release that runs
├── releases/
│ ├── 1/
│ ├── 2/
│ └── 3/
├── shared/ the same in every release
│ ├── .env
│ ├── shakomako/ every version of the chat widget
│ └── storage/ files, logs, caches
└── .dep/ Deployer's recordsnginx, Supervisor and cron use current, so they always run the release it
points at.
A deploy builds the new release next to the one that runs: it installs the
packages, builds the frontend and migrates the database. Only when all of
that worked does it point the current link at the new folder. Visitors use
the old release until that moment, and going back is one command.
This page sets up Deployer for a server installed with Install on Ubuntu.
Deployer only brings the code to the server. Redis, Horizon, the WebSocket server and the scheduler still run on the server, as on that page. For now, Chatvia needs Redis and Laravel Horizon (see Redis and Horizon are required). That is why we recommend a VPS (a virtual private server that you control): see Where to install.
Installed with the installer?
The installer's complete installation
(Install with one command) already
set up Deployer on the server, with the deploy.php from this page. It runs
as chatvia, in /home/chatvia/deploy, and deploys to the server itself
(localhost() in place of host()). Running the installer again upgrades
with it, and automatic updates run it each
night to install new releases. To deploy by hand:
sudo -iu chatvia
cd deploy
vendor/bin/dep deploy --tag=v1.1.0The installer replaces that deploy.php on an upgrade only while you have
not changed it.
What you need
- A VPS or another server you control with steps 1 to 4 of
Install on Ubuntu done: the packages (Supervisor among
them, for Horizon), PostgreSQL, Redis and the
chatviauser. Or a server where you installed all of Chatvia by hand: see Move an install to Deployer. - On your computer: PHP 8.5 or newer, Composer and an SSH client.
1. Let Deployer sign in
Deployer signs in as chatvia, the user that owns the code. It needs no
sudo.
On your computer, show your public SSH key, or make a key first with
ssh-keygen -t ed25519:
cat ~/.ssh/id_ed25519.pubOn the server, add that line to the chatvia user's keys:
sudo install -d -m 700 -o chatvia -g chatvia /home/chatvia/.ssh
echo 'ssh-ed25519 AAAA... you@laptop' | sudo tee -a /home/chatvia/.ssh/authorized_keys
sudo chown chatvia:chatvia /home/chatvia/.ssh/authorized_keys
sudo chmod 600 /home/chatvia/.ssh/authorized_keysBack on your computer, check that it works:
ssh [email protected] 'php -v'2. Install Deployer
Make a folder for the deploy settings on your computer, and install Deployer in it:
mkdir chatvia-deploy
cd chatvia-deploy
composer require --dev deployer/deployer
vendor/bin/dep --versionThe folder holds only deploy.php: Deployer deploys Chatvia's repository as
it is, so you never change Chatvia's code. Keep the folder in a Git
repository of your own if your team deploys too.
3. Write deploy.php
Create deploy.php in that folder:
<?php
namespace Deployer;
require 'recipe/laravel.php';
set('repository', 'https://github.com/chatvia/chatvia.git');
set('keep_releases', 5);
// One user runs PHP-FPM, Horizon, Reverb and the scheduler, so a release
// needs no extra write permissions.
set('writable_mode', 'skip');
host('chat.example.com')
->set('remote_user', 'chatvia')
->set('deploy_path', '/var/www/chatvia');
desc('Creates the storage folders Laravel needs');
task('chatvia:storage', function () {
run('mkdir -p {{deploy_path}}/shared/storage/framework/cache/data {{deploy_path}}/shared/storage/framework/sessions {{deploy_path}}/shared/storage/framework/views');
});
desc('Builds the frontend and the chat widget');
task('chatvia:build', function () {
cd('{{release_path}}');
run('npm ci --no-audit --no-fund');
run('npm run build');
run('npm run shakomako:build');
});
desc('Keeps every chat widget version online, in a folder all releases share');
task('chatvia:widget', function () {
run('mkdir -p {{deploy_path}}/shared/shakomako');
// A page that loaded an older widget still loads files from that
// version's folder, so the versions of earlier releases stay. A folder
// that is already there holds the same build and is skipped.
run('for dir in {{current_path}}/public/shakomako/*/ {{release_path}}/public/shakomako/*/; do [ -d "$dir" ] || continue; version=$(basename "$dir"); [ -e "{{deploy_path}}/shared/shakomako/$version" ] || cp -R "${dir%/}" "{{deploy_path}}/shared/shakomako/$version"; done');
// embed.js and version.json pick the version that loads. They stay with
// the release until it goes live (chatvia:widget:publish).
run('mv {{release_path}}/public/shakomako {{release_path}}/.shakomako');
run('{{bin/symlink}} {{deploy_path}}/shared/shakomako {{release_path}}/public/shakomako');
});
desc('Makes the chat widget load the version of the release that runs');
task('chatvia:widget:publish', function () {
// A release from before chatvia:widget serves its own public/shakomako.
if (! test('[ -d {{current_path}}/.shakomako ]')) {
return;
}
// Copied, then renamed, so nginx never serves a half-written file.
run('cd {{current_path}}/.shakomako && for file in *; do [ -f "$file" ] || continue; cp "$file" "{{deploy_path}}/shared/shakomako/.$file.tmp" && mv -f "{{deploy_path}}/shared/shakomako/.$file.tmp" "{{deploy_path}}/shared/shakomako/$file"; done');
});
// The release the admin panel shows and compares with the newest one: the
// tag without its "v", or for a branch its name and commit (not a release,
// so the panel looks for no updates). The Docker image writes the same file.
desc('Saves the version of this release in its VERSION file');
task('chatvia:version', function () {
run('cd {{release_path}} && case {{target}} in v[0-9]*) version={{target}}; version=${version#v} ;; *) version="{{target}}@$(cut -c1-7 REVISION)" ;; esac && printf \'%s\\n\' "$version" > VERSION');
});
// In place of the recipe's plain migrate: chatvia:deploy:migrate creates the
// analytics, Pulse and Spectra databases, migrates, checks the embedding
// settings and adds the built-in tools, the same steps the Docker image runs.
desc('Migrates the database and runs the checks that go with it');
task('artisan:migrate', artisan('chatvia:deploy:migrate --no-interaction', ['skipIfNoEnv', 'showOutput']));
after('deploy:update_code', 'chatvia:version');
after('deploy:shared', 'chatvia:storage');
after('deploy:vendors', 'chatvia:build');
after('chatvia:build', 'chatvia:widget');
after('deploy:symlink', 'chatvia:widget:publish');
after('rollback', 'chatvia:widget:publish');
after('deploy:failed', 'deploy:unlock');Change chat.example.com to your server's address. Deployer's
Laravel recipe does most of
the work, and the file adds what Chatvia needs on top. vendor/bin/dep tree deploy shows every step of a deploy, in order:
| Step | What it does |
|---|---|
deploy:prepare |
Locks the deploy, makes the folder releases/N and fetches the code of the tag you deploy. |
chatvia:version |
Writes the version you deploy, like 1.4.2, to the release's VERSION file. The admin panel shows it, and compares it with the newest release. Without this step the admin panel shows dev and never says when an update is out. |
deploy:shared |
Links .env and storage/ in the release to shared/. |
chatvia:storage |
Creates the storage/framework folders the first time. |
deploy:vendors |
composer install --no-dev. |
chatvia:build |
Builds the frontend and the chat widget. |
chatvia:widget |
Copies the new widget version into shared/shakomako, and links the release's public/shakomako to that folder. See Keep older widget versions. |
artisan:storage:link |
Lets nginx serve asset files (profile photos, widget logos) from /storage. |
artisan:optimize |
Caches the configuration, routes, events and views. |
artisan:migrate |
Runs chatvia:deploy:migrate. See The database steps. |
deploy:publish |
Points current at the new release, unlocks, and deletes all but the newest 5 releases. |
chatvia:widget:publish |
Right after the switch, makes websites load the new widget version. |
artisan:reload |
Restarts Horizon, Reverb and Pulse. Supervisor starts them again on the new release. |
When a step fails, the deploy stops before current changes, so the running
release is not touched.
The database steps
deploy.php replaces the recipe's artisan:migrate with one Chatvia
command, chatvia:deploy:migrate. The Docker image runs the same command
each time it starts, so both kinds of install prepare the database in the
same way. It does these steps, in order:
- Checks that the analytics, Pulse and Spectra databases are there, and creates any that are missing.
- Migrates the database.
- Stops the deploy when the embedding settings no longer match the database.
- Rebuilds the index for search by meaning when
EMBEDDING_INDEXchanged. It does nothing when the index already matches. - Updates the built-in tools in the tool catalog. This step needs your embedding API key. Without it, the deploy only shows a warning and goes on.
4. Prepare the server
A new server
After steps 1 to 4 of Install on Ubuntu, create the shared
folder and the .env file:
sudo install -d -o chatvia -g chatvia /var/www/chatvia /var/www/chatvia/shared
sudo -iu chatvia
cd /var/www/chatvia/shared
curl -fsSL -o .env https://raw.githubusercontent.com/chatvia/chatvia/v1.0.0/.env.example
chmod 600 .env
php -r 'echo "base64:" . base64_encode(random_bytes(32)) . PHP_EOL;'
nano .envUse the tag you will deploy in the address. Set APP_KEY to the value the
php -r line printed, and change the other lines as in
step 5 of Install on Ubuntu.
Then leave the chatvia user with exit.
Deploy (step 5 below), then do steps 8 to 13 of Install on Ubuntu with one
change: everywhere it says /var/www/chatvia, write
/var/www/chatvia/current. These are the lines that differ:
| File | Line |
|---|---|
/etc/nginx/sites-available/chatvia |
root /var/www/chatvia/current/public; |
/etc/supervisor/conf.d/chatvia.conf |
command=/usr/bin/php /var/www/chatvia/current/artisan horizon, command=/usr/bin/php /var/www/chatvia/current/artisan reverb:start, command=/usr/bin/php /var/www/chatvia/current/artisan pulse:check and directory=/var/www/chatvia/current (three times) |
/etc/cron.d/chatvia |
* * * * * chatvia cd /var/www/chatvia/current && php artisan schedule:run >> /dev/null 2>&1 |
In step 7, only the last command is left to do: take the superuser right away from the database user after the first deploy.
Move an install to Deployer
On a server where Chatvia runs from /var/www/chatvia (installed with
Install on Ubuntu), move .env and storage/ into the shared
folder, and point nginx, Supervisor and cron at current. The site is down
from the first command until the first deploy is done, a few minutes:
sudo supervisorctl stop chatvia-horizon chatvia-reverb chatvia-pulse
sudo mv /var/www/chatvia /var/www/chatvia-manual
sudo install -d -o chatvia -g chatvia /var/www/chatvia /var/www/chatvia/shared
sudo mv /var/www/chatvia-manual/.env /var/www/chatvia-manual/storage /var/www/chatvia/shared/
sudo sed -i 's#/var/www/chatvia/public#/var/www/chatvia/current/public#' \
/etc/nginx/sites-available/chatvia
sudo sed -i 's#/var/www/chatvia/artisan#/var/www/chatvia/current/artisan#; s#^directory=/var/www/chatvia$#directory=/var/www/chatvia/current#' \
/etc/supervisor/conf.d/chatvia.conf
sudo sed -i 's#cd /var/www/chatvia #cd /var/www/chatvia/current #' /etc/cron.d/chatviaDeploy from your computer (step 5), then load the new paths on the server:
sudo nginx -t
sudo systemctl reload nginx
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl statusWhen everything works, delete the old folder:
sudo rm -rf /var/www/chatvia-manual.
5. Deploy
From the chatvia-deploy folder on your computer:
vendor/bin/dep deploy --tag=v1.0.0Always deploy a release tag: the newest one is on the
tags page, and the
changelog says what
changed. Without --tag, Deployer deploys the newest commit of the main
branch, which is not a release.
A deploy ends with successfully deployed!. On a new install it also warns
that the built-in tools were not added to the tool catalog. They need your
embedding API key: finish the setup in the admin panel (step 13 of Install
on Ubuntu), and it adds them. To add them by hand later, run:
vendor/bin/dep run 'php {{current_path}}/artisan chatvia:mcp:sync-builtin'To upgrade later, read the changelog, then deploy the new tag the same way.
The scheduler backs up the database twice a day. For a fresh copy right
before an upgrade, run sudo -u postgres pg_dump -Fc chatvia > chatvia.dump
and sudo -u postgres pg_dump -Fc analytics > analytics.dump on the server.
Roll back
vendor/bin/dep rollback
vendor/bin/dep artisan:reloadrollback points current at the previous release. artisan:reload
restarts Horizon, Reverb and Pulse, so they run that release too.
A rollback does not undo migrations. Roll back only when the previous release works with the database as it is now. When a release changed the database in a way the old code cannot read, restore the backup you made before the deploy instead.
Keep older widget versions
Websites load the chat widget in two steps. embed.js asks version.json
which version to use, then loads that version's files from a folder of its
own, for example public/shakomako/1.0.0/. Each release builds its own
copy of public/shakomako, which holds only its own widget version.
Without a shared folder, a deploy would remove the older version. Say a visitor opens a website while Chatvia runs widget 1.0.0, and you then deploy a release with widget 2.0.0. The visitor's page still asks for the 1.0.0 files, but the new release no longer has them, so the widget breaks until the page is loaded again.
So public/shakomako in each release is a link to shared/shakomako, which
keeps every version:
/var/www/chatvia/shared/shakomako
├── embed.js the loader, from the release that runs
├── version.json the version that loads, from the release that runs
├── 1.0.0/ kept for pages that still use it
└── 2.0.0/chatvia:widgetcopies the new version's folder intoshared/shakomako. It never changes a folder that is already there. The release keeps its ownembed.jsandversion.jsonin.shakomako, next topublic.chatvia:widget:publishruns right aftercurrentpoints at the new release. It copies that release'sembed.jsandversion.jsonintoshared/shakomako. Until then, websites keep loading the old version: the new widget may need the new server code, so it must not load earlier.rollbackrunschatvia:widget:publishtoo, so websites load the widget of the release you went back to.
The folders are small, so they can stay. To free space, delete the folders
of versions that are more than a few days old. Never delete the folder that
version.json names:
cat /var/www/chatvia/shared/shakomako/version.json
ls -lt /var/www/chatvia/shared/shakomako
rm -rf /var/www/chatvia/shared/shakomako/1.0.0Useful commands
| Command | What it does |
|---|---|
vendor/bin/dep releases |
Lists the releases, with the tag and commit of each. |
vendor/bin/dep ssh |
Opens a shell on the server, in the current release. |
vendor/bin/dep run 'cd {{current_path}} && php artisan about' |
Runs a command in the current release. |
vendor/bin/dep deploy:unlock |
Removes the lock that an interrupted deploy left behind. |
vendor/bin/dep artisan:reload |
Restarts Horizon, Reverb and Pulse. |
{{current_path}} is /var/www/chatvia/current. Use single quotes, so
your own shell leaves it alone.
Questions
Does PHP-FPM need a reload after a deploy? No. The nginx site passes the
real path of the release to PHP ($realpath_root), so the next request after
the switch runs the new release. The old one finishes the requests it
already had.
What if a deploy fails after the migrations? current still points at
the old release, but the database already has the new tables and columns.
Fix the cause and deploy the same tag again. Migrations that already ran are
skipped.
Where do I change a setting? In /var/www/chatvia/shared/.env on the
server. Deploy again afterwards, or run
vendor/bin/dep artisan:optimize and vendor/bin/dep artisan:reload.
Know when a new release is out
A Chatvia release is a new vX.Y.Z tag in the
GitHub repository. The admin panel
checks once a day and shows a notice on its dashboard when a newer release is
out (see Updates). To hear
about it outside the admin panel, for example in a chat app, use one of
these. GitHub sends
webhooks only to the people who run a repository, so you cannot add one for a
repository you do not own. These work for everyone:
- The release feed:
https://github.com/chatvia/chatvia/releases.atomlists every release. Add it to a feed reader or to Slack's RSS app, or let an automation tool (n8n, Zapier, Make, IFTTT) call your own webhook when a new item appears. - A small script on your server that asks GitHub for the newest tag every hour, and calls a webhook when it changed.
The script needs only git and curl. Save it as
/usr/local/bin/chatvia-release-check, and set WEBHOOK_URL to your own
webhook:
#!/bin/sh
# Posts to a webhook when Chatvia has a new release (a new vX.Y.Z tag).
set -eu
REPO=https://github.com/chatvia/chatvia
WEBHOOK_URL=https://hooks.example.com/your-webhook
STATE="$HOME/.chatvia-release"
latest=$(git ls-remote --tags --refs "$REPO.git" 'v*' \
| sed 's#.*refs/tags/##' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -n 1)
[ -n "$latest" ] || exit 0
[ "$latest" = "$(cat "$STATE" 2>/dev/null || true)" ] && exit 0
curl -fsS -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"Chatvia $latest is out: $REPO/blob/$latest/CHANGELOG.md\"}" \
"$WEBHOOK_URL" > /dev/null
echo "$latest" > "$STATE"Make it executable and run it every hour as chatvia:
sudo chmod 755 /usr/local/bin/chatvia-release-check
echo '0 * * * * chatvia /usr/local/bin/chatvia-release-check' \
| sudo tee /etc/cron.d/chatvia-release-checkThe first run reports the newest release. After that, it reports only new ones. The
message is {"text": "..."}, which Slack, Mattermost and Rocket.Chat accept.
Discord wants {"content": "..."} instead.
Deploy with a backup
The script only tells you. Don't add dep deploy to it: a deploy needs a
fresh backup of the database first. Installed with the installer? Its
automatic updates already back up the
database, then deploy each new release of your major version. Otherwise,
read the changelog of each release and back up before you deploy.