Skip to content
Chatvia Docs

Installation

Deploy with Deployer

Get in touch: Ahmad Mayahi, creator of Chatvia, Spectra and Shakomako

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:

text
/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 records

nginx, 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:

bash
sudo -iu chatvia
cd deploy
vendor/bin/dep deploy --tag=v1.1.0

The 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 chatvia user. 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:

bash
cat ~/.ssh/id_ed25519.pub

On the server, add that line to the chatvia user's keys:

bash
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_keys

Back on your computer, check that it works:

bash
ssh [email protected] 'php -v'

2. Install Deployer

Make a folder for the deploy settings on your computer, and install Deployer in it:

bash
mkdir chatvia-deploy
cd chatvia-deploy
composer require --dev deployer/deployer
vendor/bin/dep --version

The 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
<?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:

  1. Checks that the analytics, Pulse and Spectra databases are there, and creates any that are missing.
  2. Migrates the database.
  3. Stops the deploy when the embedding settings no longer match the database.
  4. Rebuilds the index for search by meaning when EMBEDDING_INDEX changed. It does nothing when the index already matches.
  5. 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:

bash
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 .env

Use 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:

bash
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/chatvia

Deploy from your computer (step 5), then load the new paths on the server:

bash
sudo nginx -t
sudo systemctl reload nginx
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status

When everything works, delete the old folder: sudo rm -rf /var/www/chatvia-manual.

5. Deploy

From the chatvia-deploy folder on your computer:

bash
vendor/bin/dep deploy --tag=v1.0.0

Always 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:

bash
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

bash
vendor/bin/dep rollback
vendor/bin/dep artisan:reload

rollback 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:

text
/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:widget copies the new version's folder into shared/shakomako. It never changes a folder that is already there. The release keeps its own embed.js and version.json in .shakomako, next to public.
  • chatvia:widget:publish runs right after current points at the new release. It copies that release's embed.js and version.json into shared/shakomako. Until then, websites keep loading the old version: the new widget may need the new server code, so it must not load earlier.
  • rollback runs chatvia:widget:publish too, 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:

bash
cat /var/www/chatvia/shared/shakomako/version.json
ls -lt /var/www/chatvia/shared/shakomako
rm -rf /var/www/chatvia/shared/shakomako/1.0.0

Useful 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.atom lists 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:

bash
#!/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:

bash
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-check

The 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.

Search guides, settings and commands.