Run Banimark in Docker (or put PHP on the server)

Run the Banimark AI support desk in Docker when your server has no PHP: a PHP-FPM image with the ionCube loader, nginx and MySQL, or install PHP and MySQL natively instead.

Docker About 20 minutes Banimark 0.32.1 Runs beside your app

What you are about to do

  1. Four small files in one folder
  2. docker compose up -d --build
  3. One block in the web server you already run
  4. Open it in a browser and follow the installer
  5. Paste one line into your templates

You do this once. From then on, new versions install themselves from a button in your own Banimark admin — no terminal, no developer, and a way back if you need one. How updating works.

Your server runs Node, Python, Go or Rails, and has never had PHP on it. You have two honest options, and neither one disturbs what is already there.

A — Docker

Three containers, nothing installed on the host, everything pinned. Best when you already deploy with Docker, or you would rather not touch the host at all.

Start here →

B — PHP on the host

Two apt install lines and a database. Fewer moving parts, one less thing to keep updated, and PHP-FPM is a well-behaved neighbour.

Skip to that →

Either way, this is what Banimark needs

NeedsWhy
PHP 8.2 or newerThe engine ships encoded for PHP 8.2, and an 8.2 build runs on 8.2 and later — it will not run on 8.1 at all. We test on 8.2 and 8.3.
ext-curl, ext-json, PDO
plus a driver: pdo_mysql, pdo_pgsql or pdo_sqlite
Calling your AI provider, and talking to your database.
The ionCube loaderThe engine ships encoded, so the free loader has to be active. Without it, Banimark shows a page saying exactly what to install — the rest of your site keeps working. How to install it.
A database
MySQL 5.7+/8, MariaDB 10.4+, PostgreSQL 12+, or SQLite
Banimark keeps its own banimark_* tables. It never writes to yours.
ComposerHow the package is installed. Banimark is not on Packagist; each guide shows the one line that tells Composer where it lives.
HTTPS (strongly recommended)Chat transcripts and the signed visitor token travel over it.

No queue worker, no Redis, no Node build step, no cron job required.

Option A — four files

Make a directory for the stack. Everything below lives in it.

terminal
mkdir -p banimark-stack/app && cd banimark-stack

The PHP image, with the ionCube loader baked in

This is the part people get wrong. The licensed build of Banimark is encoded, so the loader is not optional — and it has to match the image's PHP version exactly. The build derives both, and then asserts the loader really took, so a broken image fails at docker build rather than as a blank page in production.

banimark-stack/Dockerfile
FROM php:8.3-fpm

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates unzip git \
 && docker-php-ext-install pdo_mysql \
 && rm -rf /var/lib/apt/lists/*

# ionCube. Use IONCUBE_ARCH=aarch64 on ARM hosts (Apple silicon, AWS Graviton).
ARG IONCUBE_ARCH=x86-64
RUN set -eux; \
    cd /tmp; \
    curl -fsSLO "https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_${IONCUBE_ARCH}.tar.gz"; \
    tar xzf "ioncube_loaders_lin_${IONCUBE_ARCH}.tar.gz"; \
    PHPMM="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')"; \
    cp "ioncube/ioncube_loader_lin_${PHPMM}.so" "$(php -r 'echo ini_get("extension_dir");')/"; \
    printf 'zend_extension=ioncube_loader_lin_%s.so\n' "$PHPMM" \
        > /usr/local/etc/php/conf.d/00-ioncube.ini; \
    rm -rf /tmp/ioncube*; \
    php -v | grep -q ionCube      # fail the build if the loader did not take

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/banimark

The stack

banimark-stack/docker-compose.yml
services:
  php:
    build:
      context: .
      # args: { IONCUBE_ARCH: aarch64 }    # uncomment on ARM
    restart: unless-stopped
    volumes:
      - ./app:/var/www/banimark          # writable: the installer writes its config here
    depends_on:
      db:
        condition: service_healthy

  web:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"              # loopback only; your real web server fronts it
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./app:/var/www/banimark:ro
    depends_on:
      - php

  db:
    image: mysql:8.4
    restart: unless-stopped
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    environment:
      MYSQL_DATABASE: banimark
      MYSQL_USER: banimark
      MYSQL_PASSWORD: change-me-to-something-long
      MYSQL_ROOT_PASSWORD: change-me-too
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 20
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:

nginx, inside the stack

banimark-stack/nginx.conf
server {
    listen 80;
    root /var/www/banimark;

    client_max_body_size 25m;

    location ~ ^/banimark(/.*)?$ {
        include       fastcgi_params;
        fastcgi_pass  php:9000;

        fastcgi_param SCRIPT_FILENAME /var/www/banimark/banimark.php;
        fastcgi_param SCRIPT_NAME     /banimark;
        fastcgi_param PATH_INFO       $1;
        fastcgi_param DOCUMENT_ROOT   /var/www/banimark;

        fastcgi_read_timeout 120s;      # AI answers are not instant
    }

    location / { return 404; }
}

The entry file

banimark-stack/app/banimark.php
<?php
require __DIR__.'/vendor/autoload.php';

\Banimark\Standalone\App::run(__DIR__.'/banimark.config.php');

Build and start it

terminal
docker compose up -d --build

# install the package inside the container, so the vendor tree matches its PHP
docker compose exec php composer require "banimark/banimark:^0.32"

# prove the loader is live before you go any further
docker compose exec php php -v | grep ionCube
terminal
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/banimark/install   # 200

Put it on the origin your visitors use

Add one location block to the web server that already serves your site — above whatever proxies to your app:

the host's existing server block
location ^~ /banimark {
    proxy_pass http://127.0.0.1:8080;          # no trailing slash: keep the path
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_read_timeout 120s;
    client_max_body_size 25m;
}

Run the browser installer

Open https://example.com/banimark/install. Use db as the database host — that is the service name inside the compose network — with the database, user and password you set in the compose file. Then your admin account, then optionally your first AI provider. It writes the config into ./app/banimark.config.php and locks itself.

The panel is at /banimark/admin. Embed the widget on your pages:

your layout
<script src="/banimark/widget.js" defer></script>

Back up ./app/banimark.config.php and the dbdata volume; that is the whole of your state.

Updating

The short version: two buttons, no terminal

Open /banimark/admin, go to Changelog, and when a new release exists you get a button that says Update to …. Press it. When it finishes, press Update the database. That is the whole thing, and nobody needs a command line, SSH or a developer to do it.

What happens when you press it:

  1. Banimark downloads the release from us, using your licence key.
  2. It checks the download is byte-for-byte the file we signed. If the signature or the hash does not match, it is thrown away and nothing on your server changes.
  3. It unpacks the new version beside the current one — never over it.
  4. It renames your current version aside as a backup, and moves the new one into place. If anything goes wrong at that step, the backup goes straight back.
  5. Your previous version stays on the server — just the one, so updates never pile copies onto your disk — and Previous versions offers a Restore this one button for it.

The same page also has, whenever they apply:

  • Check for updates now — on every state of the page. Banimark remembers our answer for a few hours; this asks again straight away.
  • Reinstall — downloads the version you are on and puts a fresh copy in place, for when files have been damaged or edited by mistake.

If the button is not there

The Changelog page will say which of these it is, in plain words:

  • Your licence has lapsed. You can still see what is new — downloading the file is what the licence pays for. Renew and the button comes back.
  • The server will not let PHP write to its own folder. Common on shared hosting. The page names the exact directory to give the web server write access to; your host can do it in a minute. Until then, use the command line below.
  • This release has to be installed by hand. Some releases need a step we will not do behind your back — a newer PHP, say. We hold those back from the button deliberately.

The command line, when you want it

Nothing about the button replaces this, and a developer updating from a terminal changes nothing about how the panel behaves. Name the release line you are moving to:

terminal
docker compose exec php composer require "banimark/banimark:^0.32"

Why not composer update? Banimark is still on 0.x, where Composer treats every minor release as a breaking one: ^0.25 will never install 0.26. composer update picks up fixes within your line; composer require with the new line is how you move up. The newest line right now is ^0.32.

The database changes, and what applies them

In Laravel, a release that needs new columns ships a migration and php artisan migrate applies it. You have no migration runner, so Banimark carries the same job itself: one function, Schema::ensureCurrent(), which brings an existing database up to whatever the installed version expects. It is what the panel button calls, what the Laravel migration calls, and what you can call yourself.

It is version-gated. It compares the schema_version row in banimark_settings with the version of the code now on disk, does the work once if they differ, and is a no-op every time after that. The chat endpoint never runs it, so your visitors never pay for it.

If you updated from the command line instead of the panel, signing in to /banimark/admin applies it too.

The deploy-script way: do it yourself

If you deploy from CI, or you would rather not have the first staff visit of the day be the migration, call it directly. Put this beside banimark.config.php:

banimark-upgrade.php
<?php
require __DIR__.'/vendor/autoload.php';

$cfg = require __DIR__.'/banimark.config.php';
$pdo = new PDO($cfg['dsn'], $cfg['db_user'], $cfg['db_pass'], [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$changed = \Banimark\Storage\Schema::ensureCurrent(
    $pdo,
    \Banimark\Licensing\Master::PACKAGE_VERSION
);

echo $changed
    ? "Banimark schema brought up to date.\n"
    : "Nothing to do.\n";
terminal
docker compose exec php composer require "banimark/banimark:^0.32"
docker compose exec php php banimark-upgrade.php

Safe to run on every deploy, in any order relative to your own migrations. On a database that was never installed it does nothing at all and says so — it upgrades an install, it does not create one. Run the browser installer for that.

What it actually does to your database

  • CREATE TABLE IF NOT EXISTS for any table a new release added.
  • ALTER TABLE … ADD COLUMN for any column a new release added, swallowing the duplicate error so it is safe to re-run.
  • Creates any missing index.
  • Converts Banimark's own tables to utf8mb4 if they are not already — the reason emoji work.

It is additive. Nothing is dropped, nothing is rewritten, and it only ever touches tables with Banimark's own prefix. Your application's schema is not involved.

Check that it worked

This matters more than it sounds, because of the next section. Ask the database what version it thinks it is on:

mysql -u banimark -p banimark
SELECT `value` FROM banimark_settings WHERE `key` = 'schema_version';

It must match the version shown on the panel's Changelog page. If it lags behind, the upgrade has not run.

If the version never moves

Almost always one thing: the database user Banimark connects as can read and write rows but cannot change the schema. ensureCurrent() is deliberately built never to throw — a panel that cannot write a settings row must still open — so a missing privilege looks exactly like nothing happening. No error, no warning, just an old version number.

mysql -u root -p
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX
    ON banimark.* TO 'banimark'@'localhost';
FLUSH PRIVILEGES;

Then run the upgrade again. Those seven are enough for both the first install and every upgrade after it; Banimark needs no DROP and no REFERENCES.


Option B — put PHP and MySQL on the host

Often the simpler answer. PHP-FPM is a small, boring service; it will not interfere with what is already running, and there is no image to rebuild when a patch lands.

Put PHP and MySQL on the server

Banimark is a PHP application. If your box has never run PHP, this is the whole of it — about five minutes of copy-and-paste, and it changes nothing about how your app runs. PHP-FPM is a small, well-behaved service; it will not interfere with what is already there.

The commands, for your operating system

Ubuntu and Debian

terminal
sudo apt update
# Ubuntu 24.04 has php8.3 in its own repositories. On older releases, or for a
# newer PHP, add Ondrej's archive first:
#   sudo add-apt-repository ppa:ondrej/php && sudo apt update

sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-mbstring php8.3-xml
sudo apt install -y mysql-server
sudo systemctl enable --now php8.3-fpm mysql

RHEL, Rocky and AlmaLinux

terminal
sudo dnf install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
sudo dnf module reset php -y && sudo dnf module install php:remi-8.3 -y
sudo dnf install -y php-fpm php-cli php-mysqlnd php-curl php-mbstring php-xml mysql-server
sudo systemctl enable --now php-fpm mysqld

Composer

terminal
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version

A database for Banimark

Give Banimark its own database and its own user. It only ever touches its banimark_* tables, so it can live beside anything else you have.

mysql -u root -p
CREATE DATABASE banimark CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'banimark'@'localhost' IDENTIFIED BY 'choose-a-long-random-password';
GRANT ALL PRIVILEGES ON banimark.* TO 'banimark'@'localhost';
FLUSH PRIVILEGES;

Install the ionCube loader

Your licensed build of Banimark ships encoded, so the ionCube PHP loader has to be active before anything will run. Most commercial hosts already have it — check first:

terminal
php -v
# ...with the ionCube PHP Loader   <- you are done, skip this section

If that line is there, skip the rest of this section entirely. If it is missing, the setup script below does the whole job for you.

The quick way: run the setup script

One command checks everything Banimark needs — PHP version, extensions, the ionCube loader, Composer, whether this server can reach us — and installs the loader if it is missing. It works out your operating system, processor, PHP version and control panel by itself, and asks before it changes anything.

Look before you leap. Run it with --check first: it reports exactly what it finds and what it would do, and changes nothing at all. Safe on a live server.

check only - changes nothing
curl -fsSL https://banimark.com/setup.sh | bash -s -- --check

Happy with what it says? Run it for real:

Linux, macOS, cPanel / Plesk servers
curl -fsSL https://banimark.com/setup.sh | bash
Windows (PowerShell)
irm https://banimark.com/setup.ps1 | iex

What it does, in order:

  1. Finds every PHP on the server and asks which one your website uses — on cPanel, Plesk, XAMPP or a Mac with MAMP there is usually more than one.
  2. Checks PHP 8.2+, the json, curl, pdo, mbstring and sodium extensions, a database driver, and zip (for one-click updates). Where it can, it offers to install or switch on what is missing.
  3. Downloads the official loader from ionCube for your exact PHP, copies it in, and adds it to PHP's configuration so it loads first — backing up any file it changes. It then starts PHP to prove it works, and puts everything back if it does not.
  4. Offers to restart PHP-FPM / Apache / LiteSpeed, and to check the PHP your website actually runs (a tiny, randomly named file in your public folder, read once and deleted).

The script is plain text — read it (or the Windows one) before you run it. On shared hosting, where nobody can change PHP from a terminal, it tells you where the switch is in your panel instead.

Prefer to do it by hand, or need to send it to whoever runs your server? The manual steps:

How to install the ionCube loader

The loader is a single .so that must match your exact PHP minor version — the current archive carries builds for PHP 8.1 through 8.5.

On your own Linux server

terminal
# 1. which PHP, and where do its extensions live?
php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";'      # e.g. 8.3
php -r 'echo ini_get("extension_dir")."\n";'                     # e.g. /usr/lib/php/20230831

# 2. fetch the loaders (use ioncube_loaders_lin_aarch64.tar.gz on ARM servers)
cd /tmp
curl -fsSLO https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.tar.gz
tar xzf ioncube_loaders_lin_x86-64.tar.gz

# 3. copy the ONE that matches your PHP
PHPMM=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
sudo cp "ioncube/ioncube_loader_lin_${PHPMM}.so" "$(php -r 'echo ini_get("extension_dir");')/"

Now switch it on. On Debian and Ubuntu, PHP reads a directory of .ini files, and the loader has to be first — the 00- prefix is what guarantees it loads before OPcache:

terminal
PHPMM=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
echo "zend_extension=ioncube_loader_lin_${PHPMM}.so" | sudo tee /etc/php/${PHPMM}/mods-available/00-ioncube.ini

sudo ln -sf /etc/php/${PHPMM}/mods-available/00-ioncube.ini /etc/php/${PHPMM}/fpm/conf.d/00-ioncube.ini
sudo ln -sf /etc/php/${PHPMM}/mods-available/00-ioncube.ini /etc/php/${PHPMM}/cli/conf.d/00-ioncube.ini
sudo systemctl restart php${PHPMM}-fpm

php -v      # must now say "with the ionCube PHP Loader"

On RHEL, Rocky and AlmaLinux the equivalent file is /etc/php.d/00-ioncube.ini, then sudo systemctl restart php-fpm.

On cPanel, Plesk or shared hosting

  • cPanel — Select PHP Version → Extensions, tick ioncube_loader. From a WHM shell, find EasyApache's package for your PHP with dnf list available 'ea-php83-php-ioncube*' and install the newest one — Banimark needs loader 15 or newer. The setup script does this for you.
  • Plesk — the loader ships with Plesk's own PHP builds; enable it under Websites & Domains → PHP Settings.
  • Anything else — ask your host to “enable the ionCube PHP loader”. It is a standard request and almost every commercial host supports it.

How you will know

You do not have to guess. If the loader is missing, or PHP has not been restarted since you added it, every Banimark admin page shows “Banimark needs the ionCube Loader” with these steps, the chat widget politely says support is temporarily unavailable, and the rest of your site carries on as normal. On Laravel, php artisan banimark:doctor says the same thing in the terminal. Once it is fixed, reload — nothing needs reinstalling.

Then follow the Node guide from “Install Banimark” — from that point the steps are identical whatever language your app is written in: a directory, one entry file, an nginx location block, and the browser installer.

Your licence

A fresh install asks us for a free trial on its own — 7 days, one per site . When it ends, the admin panel locks until you enter a purchased key. Your chat widget is never affected: visitors carry on being served whatever your licence says.

Enter the key under License in the panel, or set it before first boot:

.env
BANIMARK_LICENSE_KEY=BM-XXXX-XXXX-XXXX-XXXX

Activation needs one call to our servers. After that the licence is good offline for 14 days at a time, so a network problem on your side never interrupts anything.

First five minutes in the panel

  1. AI Providers — add your own key. Gemini, OpenAI, Anthropic, DeepSeek, Groq, Mistral, OpenRouter, xAI, or a local Ollama / LM Studio through a base URL.
  2. Rules — plain sentences in folders, not prompt engineering. “Never promise a refund date.” Switch a folder off without deleting it.
  3. Tools — the read-only lookups the assistant may run against your database. Pick a table, tick the columns it may show, and scope every row to the signed-in visitor. SELECT only, always bound, never widened by the AI.
  4. Staff — invite your team. Banimark has its own logins, separate from your application's users, with per-person permissions and optional 2FA.
  5. Notifications — SMTP, so a handover reaches someone.

Check it over

Before you call it done:

  • Open the panel and confirm the licence page is green.
  • Send a message through the widget on a real page — not just curl. The widget is JavaScript; a curl check proves nothing about it.
  • Type an emoji. If it comes back, your database is utf8mb4 and you are fine.
  • Ask something that needs a tool, while signed in, and confirm it returns your rows.
  • Trigger a handover and confirm the email lands.

Not your stack?

Stuck on a step?

Send us what banimark:doctor prints — it tells us more in one paste than a paragraph ever does. Or hand the whole thing to us and we will finish it for you.