Installing on your own server
On your own server

Installing on your own server

One command in the terminal: the installer puts Docker in place, fetches the bundle, sets up nginx, issues the certificate and brings the product up.

Nothing is built on the spot and nothing is downloaded from the internet except the bundle itself. All you need is Debian, two domains and half an hour, most of which is the download. The license token comes before the install: how to get one.

What you will need

WhatMinimumRecommended
SystemDebian 13, amd64Debian 13, amd64
Dedicated cores24
Shared (burstable) cores instead46–8
Memory4 GB8 GB
Swap2 GB, required2 GB
Disk40 GB SSD100 GB SSD
Operatorsup to 5up to 25

Debian 13 or newer only. Ubuntu and other distributions are not supported, and that is a deliberate narrowing: one tested system instead of a zoo, so that this guide and our updates do not drift away from what you run. The architecture is amd64; an arm build is a separate request.

About shared cores. The numbers come from a server with dedicated cores and a full reservation. On an ordinary VPS the cores are shared and oversubscribed: the advertised “4 cores” turn out to be noticeably fewer, especially at busy hours. So for shared cores take about twice the nominal count — that compensates for oversubscription, it is not headroom for growth.

About steal time. This is the least obvious reason for slowness, and it is worth settling before you buy the server. On oversubscribed hosting the processor is formally there, but the hypervisor takes it away. If steal time is consistently above 5 per cent, background jobs and image processing suffer first: they get noticeably slower while the interface still looks alive. On heavily oversubscribed hosting we cannot promise responsiveness. The installer measures steal time as its first step and says what it saw; you can measure it yourself at any time with vmstat 1 10 — the st column is steal time as a percentage.

About the disk. Only attachments need counting. The database takes about 1.6 KB per message: even a hundred thousand messages a month come to less than 2 GB a year. On a live server attachments take fifty times more than the database, and they grow from what your customers send. Two levers are in the product: attachment retention and offloading to S3. With a heavy flow of images S3 is cheaper than buying disk.

No build happens on your server. We ship ready-made images, and that lowers the requirements noticeably: building the frontend alone asks for about 8 GB of memory, and if it ran on the spot the minimum profile would be twice as high.

What to prepare before the command

  1. Two domains, both A records pointing at this server: the main one for the interface (support.example.com) and a separate one for the API and webhooks (api.support.example.com). The records have to have propagated already — the certificate is issued by a domain check, and that does not pass on DNS that has not spread yet.
  2. The license token: a line starting with SHLIC1.. We send it on the sale. It also carries the address the installer fetches the bundle from, so nothing else has to be passed.
  3. Root on the server. The installer installs packages and writes to /opt, /etc/supporthub and /etc/nginx.

The one-command install

curl -fsSL https://get.supporthub.cc/install | bash -s -- \
    --key SHLIC1.… \
    --domain support.example.com \
    --api-domain api.support.example.com \
    --email admin@example.com
ArgumentWhat for
--keythe license token. Required: the address the bundle is fetched from comes out of it
--domainthe address of the interface
--api-domainthe address of the API and webhooks
--emailthe address for certbot: certificate expiry notices go there
--bundle <файл|url>take the bundle from here instead of downloading it from us. For air-gapped networks
--no-tlsleave nginx and certbot alone: you have your own proxy or load balancer
--app-dirwhere to install. /opt/supporthub by default
--dry-runprint the plan and change nothing

What happens, step by step — you see them in the output:

  1. The server. System, architecture, memory, swap, disk, steal time, the way out to our server. Blockers stop the install; remarks are printed and do not get in the way.
  2. Docker. If it is missing, it is installed from the Docker repository for Debian, together with docker-compose-plugin.
  3. The bundle. Downloaded from the address carried in the token into /var/tmp. A broken download is not a problem: running it again continues from where it stopped.
  4. Bundle verification. Checksums and, if we gave you the public key, the signature. Then the images are loaded and the files laid out.
  5. Settings. /opt/supporthub/.env fills itself in: the database password and the session signing key are generated on the spot, the addresses come from the arguments, and the number of worker processes is chosen by the amount of memory.
  6. The maintenance agent. The supporthub-agent service is installed on the host — the update button and support access in the License section work through it. A supporthub command for the same actions from the terminal comes with it.
  7. The license. The token is placed in /etc/supporthub/license.key — as a file, not an environment variable, so it never shows up in docker inspect or in shell history.
  8. nginx and the certificate. Two server blocks and certbot. If nginx on this server is already configured for your domains, the step is skipped entirely — we do not overwrite your config.
  9. The setup code. The installer issues it itself and prints it at the end.
  10. Start-up. docker compose up -d, then waiting until the backend answers on /api/health.

To see what would be done without changing anything:

curl -fsSL https://get.supporthub.cc/install | bash -s -- --key SHLIC1.… \
    --domain support.example.com --api-domain api.support.example.com \
    --email admin@example.com --dry-run

Verifying the bundle signature. If we gave you the release public key, put it on the server and point a variable at it — then the signature is actually verified, not just noted:

SH_RELEASE_PUBKEY=/root/supporthub-release.pem curl -fsSL https://get.supporthub.cc/install | bash -s -- --key …

Air-gapped networks. If the server has no way out to the internet, ask us for the bundle as a file and hand it to the installer through --bundle: it then downloads nothing at all. The installer script has to come with you too in that case — it is inside the bundle under the name bootstrap.bash:

tar -xzf supporthub-dedicated-2026.09.30.tar.gz bootstrap.bash
bash bootstrap.bash --key SHLIC1.… --domain support.example.com \
    --api-domain api.support.example.com --email admin@example.com \
    --bundle /root/supporthub-dedicated-2026.09.30.tar.gz

certbot will not issue a certificate in an air-gapped network: install with --no-tls and set up your own proxy, or put the certificate in place yourself.

Opening the page for the first time

At the end the installer prints the address and the setup code — twelve characters in the form ABCD-EFGH-JKLM. Open the address in a browser: the first-run wizard asks for the code, and then creates the owner and the first project.

The code exists because a fresh server is on the internet before its owner gets to it: without one, whoever opened the page first would become the owner. It sits in /etc/supporthub/setup-code and is asked for once — once the owner exists, the wizard closes for good and the /setup page stops existing.

The wizard does not ask about the license: the installer has already placed the token. The key field in the wizard is there for an install that ran without a token, and a key typed there takes precedence over the file.

Everything after that happens inside the product: operator invitations, connecting channels, settings. The one thing worth adding to /opt/supporthub/.env by hand is your own SMTP (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM), followed by docker compose up -d: without it operator invitations and notifications do not go out. The installer reminds you about this too.

Activating the license

The install needs at least one way out to our license server. It is not a permanent connection: the check runs at start-up and then every six hours, and what goes out is the license id, the server fingerprint, the build version, the number of active operators and a timestamp. Message contents and personal data are not sent and are not available to us.

A fresh install runs for no longer than seven days without activation. At least one successful check has to happen in that time. After it, the install survives a loss of connection for as many days as the license says: an outage on our side should not become one on yours. The first check usually happens by itself while you fill in the wizard — the license state is visible in the License section.

If the server is air-gapped, it never reaches out at all. Tell us up front: you send the server fingerprint, we issue a receipt offline, and you place it as a file. The fingerprint is shown in the License section, and this command prints the same thing:

docker compose exec backend python -c "from app.licensing import fingerprint; print(fingerprint.current())"

Profiles: the bot and WhatsApp

By default only what everybody needs comes up. Optional parts are switched on with profiles:

cd /opt/supporthub
docker compose --profile bot up -d          # your own Telegram bot
docker compose --profile whatsapp up -d     # the WhatsApp gateway

Without WhatsApp you save a container and its memory. Only switch on what you actually use.

Backups

They are your responsibility, but the recipe is ready. Two things are needed: the database and the attachments directory.

/usr/local/bin/supporthub-backupbash
#!/bin/bash
set -euo pipefail
DEST=/var/backups/supporthub
DAY=$(date +%F)
mkdir -p "$DEST"
cd /opt/supporthub

# The database. Through pg_dump inside the container: the snapshot is
# consistent and nothing has to be stopped.
docker compose exec -T db pg_dump -U postgres supporthub | gzip -6 > "$DEST/db-$DAY.sql.gz"

# Attachments. This is a docker volume, so copy from inside the container.
docker compose exec -T backend tar -cf - -C /app media | gzip -6 > "$DEST/media-$DAY.tar.gz"

# Settings, license and setup code: small, but a restore without them
# is incomplete.
tar -czf "$DEST/conf-$DAY.tar.gz" /opt/supporthub/.env /etc/supporthub

find "$DEST" -name '*.gz' -mtime +14 -delete
chmod +x /usr/local/bin/supporthub-backup
echo '15 3 * * * /usr/local/bin/supporthub-backup' | crontab -

A copy on the same disk saves you from a mistake, not from losing the server: take it somewhere else.

Restoring:

cd /opt/supporthub
docker compose down
docker compose up -d db
gunzip -c /var/backups/supporthub/db-2026-09-30.sql.gz | docker compose exec -T db psql -U postgres supporthub
docker compose up -d

Updates

An update is the same install command with a newer bundle. The installer remembers the previous image tag, so a rollback takes a minute:

/opt/supporthub/rollback.bash

A database backup is taken before the version is switched — that is what makes the rollback unconditional: images roll back in a minute, a database schema does not. Previous images are not removed from the server, otherwise there would be nothing to roll back to. If space is tight, remove them by hand and only the tags you are sure about.

Update access. The settings have a switch with which you allow or forbid us to come to the server to install updates. While it is off we do not go to the server at all — not “we go but change nothing”. Updates are then handed to you as a bundle and you install them yourself. Revoking access affects neither the product nor the license term. The details are in maintaining the install.

Installing step by step

The installer does everything described below, and this section is for two cases: when something of yours already lives on the server and you want to control every step, or when you need to understand what exactly happened. If you installed with one command, you can skip it.

1. Check the server

./preinstall-check.bash

The script is inside the bundle, changes nothing and only looks: the system, architecture, memory, swap, disk, docker, the way out, nginx and steal time. The installer runs the same checks as its first step.

2. Install Docker

apt install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
apt update
apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

It has to be docker-compose-plugin (the docker compose command), not the old docker-compose.

3. Domain and certificate

nginx runs on the host, not in a container. That is more convenient for you: certbot is right there, the config is where you are used to it, and product updates do not touch your TLS. The containers listen on loopback only (127.0.0.1:3000 and 127.0.0.1:8000); your nginx is what lets them out.

The blocks are written for port 80, and certbot adds 443, the certificate and the redirect. The other way round does not work: a config with ssl_certificate before the certificate exists takes nginx down on the very first reload.

nginx
server {
    listen 80;
    server_name support.example.com;

    client_max_body_size 50m;   # attachments

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

server {
    listen 80;
    server_name api.support.example.com;

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;

        # Live updates in the operator interface go over WebSocket.
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}
apt install certbot python3-certbot-nginx
certbot --nginx -d support.example.com -d api.support.example.com

4. Unpack the bundle

mkdir -p /root/supporthub-bundle && cd /root/supporthub-bundle
tar -xzf /path/to/supporthub-dedicated-*.tar.gz
./install.bash --check-only        # verify integrity, change nothing
./install.bash                     # install

--check-only checks the checksums and prints what is in the bundle. If we gave you the signing public key, verify that too:

./install.bash --pubkey /path/to/supporthub-release.pem --check-only

The first run lays the files out in /opt/supporthub, loads the images and stops: the settings have to be filled in next.

5. Fill in the settings

nano /opt/supporthub/.env

The required fields are marked in the file. In short:

  • DB_PASSWORD: the database password, invented once. It ends up in the WhatsApp gateway connection string, so letters and digits only: openssl rand -hex 24;
  • SECRET_KEY: openssl rand -base64 48; changing it signs every operator out;
  • FRONTEND_URL, BACKEND_PUBLIC_URL, BACKEND_PUBLIC_WS, WEBHOOK_BASE_URL: your addresses from step 3;
  • WEBAUTHN_RP_ID, WEBAUTHN_ORIGIN: for device sign-in;
  • SMTP_*: your SMTP. Without it operator invitations and notifications do not go out.

On the minimum profile (2 cores, 4 GB) set:

SH_WEB_CONCURRENCY=1
SH_CELERY_CONCURRENCY=2

Otherwise two backend processes together with Celery run into the memory ceiling, and a spike while processing attachments ends in a killed worker.

For the System section in the admin area to see the containers, fill in the docker group id:

getent group docker | cut -d: -f3

6. Place the license token

install -d -m 750 -g 1000 /etc/supporthub
install -m 640 -g 1000 /path/to/license.key /etc/supporthub/license.key

The token lies in a file rather than an environment variable: that way it shows up neither in docker inspect nor in shell history. Group 1000 is the one the backend runs as inside the container; with root:root ownership the file is unreadable from there, and the install silently drops to read-only.

Without a token the install still comes up, but in read-only mode: incoming messages are accepted and stored, and replies are not possible.

7. Issue the setup code

Without a code the first-run wizard lets nobody in, and that is right: the code closes the window between “the server appeared on the internet” and “the install has an owner”. Invent a string (a random one is fine) and put it next to the license:

openssl rand -hex 6 | install -m 640 -g 1000 /dev/stdin /etc/supporthub/setup-code
cat /etc/supporthub/setup-code

Case, hyphens and spaces mean nothing when it is checked: the code is typed by hand.

8. Start it and open the page

cd /opt/supporthub
docker compose up -d
docker compose ps

From here it is the same as the one-command install: open your address, the wizard asks for the setup code and creates the owner.

When something does not work

  1. 1
    The installer stopped at the server check
    It prints which check failed. The blockers are: a system that is not Debian 13, not amd64, less than 4 GB of memory, less than 40 GB of disk, missing swap on a 4 GB machine, and steal time above 5 per cent. All of that is fixed on the hosting side, and it has to be fixed before the install.
  2. 2
    The bundle did not download
    Check that the server can reach the internet and that the license token is valid. If there is no way out, ask for the bundle as a file and install with --bundle.
  3. 3
    The backend did not come up and the installer showed the log
    Most often it is the database that is not ready yet — compose waits for it by itself, but on a slow disk the first start takes a minute or two — or an empty field in .env. Settings are edited in /opt/supporthub/.env, then cd /opt/supporthub && docker compose up -d. Running the installer again does not overwrite the settings.
  4. 4
    certbot did not issue the certificate
    Almost always DNS: both names have to point at this server and the records have to have propagated. The installer does not stop in this case — the product comes up and works over http — and prints the command to issue the certificate later.
  5. 5
    The first-run wizard does not open and the page sends you to sign-in
    That means the install is already claimed: the owner exists. This is not a fault but a protection — the wizard does not open a second time.
  6. 6
    The wizard says the setup code is not configured
    The /etc/supporthub/setup-code file is missing or empty. Put the code in place following step 7 of the manual install and reload the page: the file is read on every check, and the containers do not need restarting.
  7. 7
    The interface opens but no data loads
    Almost always BACKEND_PUBLIC_URL does not match what nginx actually proxies. The address goes into the frontend build, so its value has to be right before we build your image: if the address changed, a new build is needed.
  8. 8
    The operator interface freezes
    The WebSocket settings are missing from nginx: proxy_http_version 1.1, the Upgrade/Connection headers and a large proxy_read_timeout.
  9. 9
    Attachments do not send
    client_max_body_size in nginx.
  10. 10
    A banner about the license showed up
    There is no connection to the license server. Check the way out; the product keeps working for as many days as the license says and recovers by itself once the connection is back.
  11. 11
    Everything is slow but the processor looks idle
    Steal time. Go back to “What you will need”.
Was this page helpful?