Skip to content

Self-Hosting Guide

Recogito Studio is ready for self-hosting and these instructions detail the steps necessary for deployment on Ubuntu Linux, v24 (although many earlier versions will also work as well). The deployment strategy utilizes Docker, Docker Compose, and Nginx.

Because everything runs in Docker behind Nginx, you can host Recogito Studio on any server that meets the requirements below, including a cloud VM (DigitalOcean, AWS EC2, Hetzner, Linode, etc.) or your own hardware. The steps assume a Debian-based distribution; the examples were written against Ubuntu 24.

Self-hosted Recogito Studio runs as a set of Docker containers: the Astro client, a Supabase/Postgres backend (with MinIO object storage and a Cantaloupe IIIF server), and Portainer for container management. See the architecture overview for the high-level model, or the reference docs for detail.

System Diagram

  • Trigger.dev - Background job system. Required for project import/export, and also used by plugin-ner. Use managed Trigger.dev Cloud or self-host it; see Set Up Trigger.dev.
  • Stanford CoreNLP - Natural Language Processor. Optional; required only for plugin-ner.

Point the hostnames you use at your server’s public IP with DNS A records (or a single wildcard record). Two services are used by your end users’ browsers and must be reachable at a public hostname:

  • client.example.com — Client application
  • server.example.com — Supabase API, which the client calls directly

The other three are operator-only management UIs. Give them hostnames if you want browser access over HTTPS, or skip public DNS and reach them privately (over an SSH tunnel, or by IP and port) to reduce your attack surface:

  • portainer.example.com — Container management
  • pgadmin.example.com — Database administration
  • minio.example.com — Object storage console

If you self-host Trigger.dev and expose it over the web, add one more, such as trigger.example.com.

The subdomain names are conventions, not requirements. You choose the base domain and the hostnames—they are wired up through your .env (SITE_URL, API_EXTERNAL_URL) and the Nginx site configs, so nothing is hardcoded.

  • Operating System: Ubuntu Linux v24 recommended; other Debian-based distributions should work
  • Memory: Minimum 4GB
  • Storage: 80GB+ recommended
  • Network: Public IP address with DNS access

Provision an Ubuntu 24 server that meets the system requirements above—at least 4GB RAM, 2 CPUs, and 80GB storage—with SSH access and a public IP. Note its public IP address, and enable automatic backups for production use.

New to cloud servers? DigitalOcean’s droplet creation guide is a good starting point, but any provider or on-premises machine works.

These steps follow DigitalOcean’s initial server setup guide. Connect to your server:

Terminal window
ssh root@your_server_ip

Update the system:

Terminal window
apt update
apt upgrade

Create a new recogito user with sudo privileges:

Terminal window
adduser recogito
usermod -aG sudo recogito

Configure the UFW firewall:

Terminal window
ufw app list
ufw allow OpenSSH
ufw enable
ufw status

If your provider has its own firewall or security groups (AWS, GCP, etc.), apply the equivalent rules there as well—some clouds block ports upstream regardless of UFW.

Best practice is to not utilize the root login except in special circumstances. Go ahead and exit the instance and SSH in with the new recogito user.

Terminal window
exit
ssh recogito@your_server_ip

Install and configure the web server:

Terminal window
sudo apt update
sudo apt install nginx

Adjust firewall for web traffic:

Terminal window
sudo ufw allow 'Nginx Full'
sudo ufw status

Verify Nginx is running:

Terminal window
systemctl status nginx

Test by navigating to http://your_server_ip - you should see the Nginx welcome page.

Nginx welcome

Verify Git is installed:

Terminal window
git --version

Install Docker Engine and the Compose plugin by following Docker’s official install instructions for your distribution. On Ubuntu, the quickest option is the convenience script:

Terminal window
curl -fsSL https://get.docker.com | sudo sh

Install Node Package Manager:

Terminal window
sudo apt install npm

Clone the repository:

Terminal window
git clone --depth 1 https://github.com/recogito/recogito-studio.git
cd ./recogito-studio

Copy and edit environment variables:

Terminal window
cp ./docker/.env.example ./docker/.env
nano ./docker/.env

Update these required values in your .env file:

Some values are passwords you choose (you’ll log in with them); the rest are random secrets you generate with a command and never type by hand.

Passwords you choose:

  • DASHBOARD_PASSWORD required — Supabase Studio login (must include at least one letter)
  • DASHBOARD_USERNAME optional — username for the Supabase Studio login. Defaults to supabase.
  • ORG_ADMIN_PW required — password for the initial admin user (admin@example.com)

Random secrets (run the command and paste the output):

  • POSTGRES_PASSWORD requiredopenssl rand -hex 24 (hex avoids characters that break Postgres connection strings)
  • VAULT_ENC_KEY requiredopenssl rand -hex 16 (32 characters)
  • INVITE_CRYPTO_KEY requiredopenssl rand -base64 32

Identifier:

  • POOLER_TENANT_ID required — any unique string, e.g. recogito

Recogito uses Supabase’s legacy JWT-based keys. First, generate a JWT_SECRET and set it in your .env:

Terminal window
openssl rand -base64 36

ANON_KEY and SERVICE_ROLE_KEY are JWTs signed with that secret. From your recogito-studio directory, with JWT_SECRET already set in docker/.env:

Terminal window
JWT_SECRET="$(grep '^JWT_SECRET=' docker/.env | cut -d= -f2-)" node -e '
const c = require("crypto");
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 60 * 60 * 24 * 365 * 10; // 10 years
const enc = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
const sign = (role) => {
const data = enc({ alg: "HS256", typ: "JWT" }) +
"." + enc({ role, iss: "supabase", iat, exp });
return data + "." + c.createHmac("sha256", process.env.JWT_SECRET).update(data).digest("base64url");
};
console.log("ANON_KEY=" + sign("anon"));
console.log("SERVICE_ROLE_KEY=" + sign("service_role"));
'

Copy the two printed values into ANON_KEY and SERVICE_ROLE_KEY in your .env.

  • SITE_URL required — your client domain (https://client.example.com)
  • API_EXTERNAL_URL required — your server domain (https://server.example.com)
  • ROOM_SECRET required — generate with openssl rand -base64 24
  • MINIO_ROOT_USER / MINIO_ROOT_PASSWORD required — your login for the MinIO console. The password must be at least 8 characters.
  • PGADMIN_ADMIN_EMAIL / PGADMIN_ADMIN_PASSWORD required — your login for pgAdmin. The email must be a valid address format, or the container won’t start.
  • MAIL_FROM_ADDRESS required — sender address for system emails

Project import/export requires Trigger.dev; see Set Up Trigger.dev for how to obtain these.

  • TRIGGER_PROJECT_ID required — project ref from the Trigger.dev dashboard
  • TRIGGER_SECRET_KEY required — secret key from the Trigger.dev dashboard
  • TRIGGER_SERVER_URL optional — your instance URL; only when self-hosting Trigger.dev

The stack does not include a mail server. The .env.example defaults (SMTP_HOST=supabase-mail) point at a placeholder that doesn’t exist, so user invites and password resets will not be delivered until you configure a real SMTP provider (e.g. Amazon SES, SendGrid, Mailgun, or your own mail server).

Set these to your provider’s credentials:

  • SMTP_HOST / SMTP_PORT required — your provider’s server and port
  • SMTP_USER / SMTP_PASS required — your provider’s credentials
  • SMTP_SENDER_NAME required — display name on outgoing mail
  • SMTP_ADMIN_EMAIL / MAIL_FROM_ADDRESS required — the “from” address (use one your provider is authorized to send from)
  • ENABLE_EMAIL_AUTOCONFIRM optional — set to true to activate new signups immediately without a confirmation email. Defaults to false, which emails each new user a confirmation link they must click (requires working SMTP).
  • DISABLE_SIGNUP optional — set to true for a closed instance where only invited users can create accounts.
  • PUBLIC_ENABLE_USER_INVITE optional — controls whether org admins can invite users from the User Management UI. Defaults to TRUE; set to FALSE to hide the invite feature.
  • INSTALLED_PLUGINS optional — plugins to install and compile into the client, as a comma- or space-separated list of npm package names. See the Plugins guide for the available packages.

Each hostname you expose (see above) needs its own Nginx site configuration. The repo provides a template for each one—copy and customize them as follows.

Terminal window
sudo cp ./nginx.client.example.com /etc/nginx/sites-available/<your client url>
sudo nano /etc/nginx/sites-available/<your client url>

Replace client.example.com with your actual client domain.

Terminal window
sudo cp ./nginx.server.example.com /etc/nginx/sites-available/<your server url>
sudo nano /etc/nginx/sites-available/<your server url>

Update:

  • server_name with your server URL
  • Access-Control-Allow-Origin headers with your client URL

Repeat for remaining services:

Terminal window
sudo cp ./nginx.minio.example.com /etc/nginx/sites-available/<your MinIO url>
sudo cp ./nginx.pgadmin.example.com /etc/nginx/sites-available/<your pgAdmin url>
sudo cp ./nginx.portainer.example.com /etc/nginx/sites-available/<your Portainer url>

Add WebSocket support to /etc/nginx/nginx.conf:

Terminal window
sudo nano /etc/nginx/nginx.conf

Add to http block:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

Create symbolic links:

Terminal window
sudo ln -s /etc/nginx/sites-available/<your server url> /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/<your client url> /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/<your pgAdmin url> /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/<your MinIO url> /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/<your Portainer url> /etc/nginx/sites-enabled/

Test and restart:

Terminal window
sudo nginx -t
sudo systemctl restart nginx

Install Certbot by following the official Certbot instructions for Nginx on Ubuntu, then issue a certificate for each of your five domains:

Terminal window
sudo certbot --nginx -d <your client url>
sudo certbot --nginx -d <your server url>
sudo certbot --nginx -d <your pgAdmin url>
sudo certbot --nginx -d <your MinIO url>
sudo certbot --nginx -d <your portainer url>

Certbot installs a timer to renew certificates automatically. Confirm it works with sudo certbot renew --dry-run.

Execute the installation script:

Terminal window
sudo bash ./install-self-hosted-docker.sh

Respond “Yes” to database push when prompted.

Recogito Studio runs project import and export as background jobs on Trigger.dev, so a Trigger.dev project is required for those features (annotation and the rest of the app work without it). You can use managed Trigger.dev Cloud or self-host your own instance. Complete this before running the install script so the client starts with the correct values, or restart the client container afterward.

  1. Get a Trigger.dev instance.

    • Managed: sign up for Trigger.dev Cloud.
    • Self-hosted: follow the Trigger.dev Docker guide (see the self-hosting overview for architecture). Run it on the same or a separate server; to reach it over the web, proxy it with Nginx + TLS at a hostname such as trigger.example.com. Pin the instance to a version compatible with the client’s @trigger.dev/sdk (see package.json).
  2. Create a project in the Trigger.dev dashboard and generate a secret key, then set the matching variables in docker/.env:

    Terminal window
    TRIGGER_PROJECT_ID=proj_... # project ref from the dashboard
    TRIGGER_SECRET_KEY=tr_... # secret key from the dashboard
    # Self-hosted only—the URL of your instance. Omit to use Trigger.dev Cloud.
    TRIGGER_SERVER_URL=https://trigger.example.com
  3. Deploy the import/export tasks to your project. From a checkout of recogito-client (note this is a different repo from recogito-studio which provisions the Docker containers), log in and deploy:

    Terminal window
    npx trigger.dev@latest login # self-hosted: add -a https://trigger.example.com
    npx trigger.dev@latest deploy

    Self-hosted deploys also require Docker registry access—see the Trigger.dev deployment docs for the docker login step.

Navigate to your client URL (https://client.example.com). You should see the Recogito Studio interface.

Sign in with:

  • Username: admin@example.com
  • Password: Value from ORG_ADMIN_PW in your .env file

Navigate to your server URL (https://server.example.com) for Supabase studio access:

  • Username: supabase
  • Password: Value from DASHBOARD_PASSWORD in your .env file

Branding (platform name, colors, logos, favicon) and sign-in methods are controlled by config.json. In the self-hosting repo this lives at docker/config/config.json, and the install script bakes it into the client image at build time.

To customize, edit docker/config/config.json before running the install script (Step 8). If you change it after installation, rebuild the client image (the upgrade flow does this). config.json also enables SSO methods (saml, keycloak) alongside the default username/password login.

See the Config Tool reference for the full schema (policies, roles, groups, branding, authentication).

The stack stores your data in Postgres and MinIO. For production, set up regular database backups. The self-hosting repo documents a full pg_dump/restore procedure (including the storage-schema RLS policies that Supabase’s dump omits) in backup-and-restore.md.

The recogito-studio repo ships an upgrade.sh script that performs the full upgrade for you: it pulls the latest client and server code, applies your config.json, rebuilds the client image, pushes database schema changes, and restarts the container.

From the root of your recogito-studio repo:

Terminal window
# Get the latest self-hosting repo (including the upgrade script)
git pull
# Add any newly required variables from .env.example to your .env
diff ./docker/.env.example ./docker/.env
# Run the upgrade
chmod +x upgrade.sh
./upgrade.sh

Building the new image and applying migrations usually takes a few minutes.

Manual upgrade (if you’d rather run each step yourself)

Run all commands from the base directory that contains your recogito-studio repo.

Terminal window
# 1. Pull the latest self-hosting, client, and server repos
cd recogito-studio && git pull && cd ..
git clone --single-branch --branch main --depth 1 https://github.com/recogito/recogito-client
git clone --single-branch --branch main --depth 1 https://github.com/recogito/recogito-server
# 2. Build the client with your config and env vars
cp ./recogito-studio/docker/config/config.json ./recogito-client/src/config.json
cp ./recogito-studio/docker/.env ./recogito-client/.env
cd ./recogito-client
set -a && source .env && set +a
docker build --no-cache -t recogito-studio-client:latest .
cd ..
# 3. Apply schema changes and sync roles/policies to the database
cp ./recogito-studio/docker/config/config.json ./recogito-server/config.json
cd ./recogito-server
npm install
PGSSLMODE=disable npx supabase db push --db-url postgresql://postgres:$POSTGRES_PASSWORD@localhost:$POSTGRES_PORT/postgres --include-all
node ./create-default-groups.js -f ./config.json
cd ..
# 4. Restart the client container with the new image, then clean up
cd ./recogito-studio/docker
docker compose -f ./docker-compose.yml -f ./docker-compose.client.yml up -d --force-recreate client
cd ../..
rm -rf ./recogito-client/ ./recogito-server/

If you’ve made manual configuration changes (such as branding) to your live config.json, propagate them before copying it over in steps 2 and 3.