Install on a VPS (Ubuntu, no panel)
This guide installs Threadline on a plain Ubuntu server with no control panel and no Docker for the app: nginx, PHP-FPM, MySQL, Redis, Qdrant, a queue worker kept running by Supervisor, and one cron job. It takes about 30 minutes and every command is shown. You need to be comfortable typing commands over SSH, but you do not need to know how any of these programs work.
Prefer clicking to typing? Use Install: VPS (aaPanel). Prefer one command and Docker? Use Install: VPS (Docker).
What you need
- A new, empty Ubuntu 24.04 server with root SSH access, 2 CPU cores and 2 GB of RAM or more (4 GB is comfortable). Ubuntu 24.04 includes PHP 8.3, which Threadline needs. See the note below for 22.04.
-
A domain name with an A record pointing at the
server's IP address. This guide uses
desk.example.com; use your own everywhere you see it. -
The Threadline ZIP from CodeCanyon or ui-lib.com, for example
threadline-v1.2.0.zip. This guide calls itthreadline.zip.
Ubuntu 22.04 ships PHP 8.1, which is too old.
Before step 2 run
add-apt-repository -y ppa:ondrej/php && apt-get update
to get PHP 8.3. Debian 12 needs the same kind of extra
repository. This guide was tried on Ubuntu 24.04.
Step 1: Connect and open the firewall
Connect as root and allow SSH and web traffic:
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
SSH (port 22) is allowed first so you do not lock yourself out.
ufw enable warns that it may disrupt SSH; type
y, your connection stays open. If your hosting
provider has its own firewall, open ports 22,
80 and 443 there too.
Step 2: Install the software
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y nginx php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-gd php8.3-bcmath php8.3-zip php8.3-intl \
mysql-server redis-server supervisor docker.io unzip curl
Check the versions that matter:
php -v | head -1
redis-cli ping
The first line must say PHP 8.3 or newer and the second must say
PONG. Redis only listens on the server itself by
default. Leave it that way.
Step 3: Create the site's user and folder
The website runs as its own user, so it owns only its own files. The same user runs the queue worker and the cron job.
useradd -m -s /bin/bash threadline
mkdir -p /var/www/threadline
chown threadline:threadline /var/www/threadline
Step 4: Create the database
Choose your own long password and use it in place of
CHOOSE_A_LONG_PASSWORD. Keep it for step 11.
mysql -e "CREATE DATABASE threadline CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'threadline'@'localhost' IDENTIFIED BY 'CHOOSE_A_LONG_PASSWORD';
GRANT ALL ON threadline.* TO 'threadline'@'localhost';
FLUSH PRIVILEGES;"
On Ubuntu the root user can run mysql
without a password. Do not use MySQL's own root
account for Threadline.
Step 5: Run PHP as the site's user
Give the site its own PHP-FPM pool, running as
threadline, then retire the default pool:
cat > /etc/php/8.3/fpm/pool.d/threadline.conf <<'EOF'
[threadline]
user = threadline
group = threadline
listen = /run/php/threadline.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
EOF
mv /etc/php/8.3/fpm/pool.d/www.conf /etc/php/8.3/fpm/pool.d/www.conf.disabled
systemctl restart php8.3-fpm
listen.owner = www-data lets nginx talk to the
socket. Check that the pool is running as the right user:
ps -eo user:20,cmd | grep "php-fpm: pool" | grep -v grep
It should list threadline, not www-data.
Step 6: Set up nginx
cat > /etc/nginx/sites-available/threadline <<'EOF'
server {
listen 80;
server_name desk.example.com;
root /var/www/threadline/public;
index index.php;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/threadline.sock;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
EOF
rm -f /etc/nginx/sites-enabled/default
ln -sf /etc/nginx/sites-available/threadline /etc/nginx/sites-enabled/threadline
nginx -t && systemctl reload nginx
nginx -t must say the test is successful before it
reloads. The root line is the important one: it
points at the public folder, so the rest of the
files cannot be reached from the internet.
Step 7: Upload Threadline
-
Copy the Threadline ZIP to the server, as you downloaded it.
On your computer:
scp threadline.zip root@YOUR_SERVER_IP:/tmp/ -
On the server, extract it as the site's user:
The second command must print the file's name. If it says there is no such file, the files went into a sub-folder: move them up one level.sudo -u threadline unzip -q /tmp/threadline.zip -d /var/www/threadline ls /var/www/threadline/artisan
Step 8: Start Qdrant
Qdrant is the vector database. Run it with Docker, for this one service only. It listens on the server itself and starts again after a reboot:
docker run -d --name qdrant --restart unless-stopped \
-p 127.0.0.1:6333:6333 \
-v qdrant_data:/qdrant/storage \
qdrant/qdrant:v1.20.0
curl http://127.0.0.1:6333/readyz
The last command should answer all shards are ready
(give it a few seconds). Do not open port 6333 to the internet.
Step 9: Turn on HTTPS
Do this now, before the installer, so the address you enter there is final. The domain's A record must already point at the server.
apt-get install -y certbot python3-certbot-nginx
certbot --nginx -d desk.example.com
It asks three questions: an email address for expiry notices,
Y to accept the Let's Encrypt terms, and whether
to share your email with the EFF (either answer is fine). It then
says Congratulations! You have successfully enabled
HTTPS. Certbot edits the nginx site, sends plain
http:// visits to https://, and sets up
automatic renewal for you.
Step 10: Set up .env for a VPS
-
Open your domain in a browser once. Threadline gives its
.envfile its own secret key and shows the installer. Leave that page open. -
Edit the file as the site's user:
Change these four lines, then save (sudo -u threadline nano /var/www/threadline/.envCtrl+O,Enter,Ctrl+X):SERVER=vps SESSION_DRIVER=redis QUEUE_CONNECTION=redis CACHE_STORE=redis
The Redis and Qdrant lines in the same file
(REDIS_HOST=127.0.0.1, REDIS_PORT=6379,
REDIS_CLIENT=predis and
QDRANT_URL=http://127.0.0.1:6333) are already
correct; leave them.
All three of QUEUE_CONNECTION,
CACHE_STORE and SESSION_DRIVER must
say redis. The example file sets them to
database, and a value in .env wins
over what SERVER=vps would choose. Do not fill in
the database lines by hand; the installer asks for them.
Step 11: Run the installer
Reload your domain. The installer has five steps and is the same one as on shared hosting, described in Install: Shared Hosting.
- Requirements. Every row should be green. A red row names what is missing.
-
App. The final address of your site, for
example
https://desk.example.com, and your timezone. -
Database. Host
127.0.0.1, port3306, databasethreadline, userthreadlineand the password from step 4. Click Test connection, then Continue. - Owner. Your own name, email and password.
- Install. It builds the database. It does not create any sample accounts or plans.
Use an empty database. The installer drops any tables already in it.
When the installer has finished, make .env readable
by the site's user only. It holds the database password and the
app key, and by default any account on the server can read it:
chmod 600 /var/www/threadline/.env
ls -l /var/www/threadline/.env
The second command should start with -rw------- and
name threadline twice. Threadline can still change
the file itself, because that user owns it.
Step 12: Start the queue worker
On a VPS a worker must always be running, or no message is sent. Supervisor keeps it alive and starts it again after a reboot.
cat > /etc/supervisor/conf.d/threadline-worker.conf <<'EOF'
[program:threadline-worker]
command=/usr/bin/php /var/www/threadline/artisan queue:work redis --queue=default,mail,webhooks --sleep=3 --tries=3 --max-time=3600
directory=/var/www/threadline
user=threadline
autostart=true
autorestart=true
stopasgroup=true
redirect_stderr=true
stdout_logfile=/var/www/threadline/storage/logs/worker.log
stopwaitsecs=3600
EOF
supervisorctl reread
supervisorctl update
supervisorctl status
The status must say RUNNING.
Step 13: Add the cron job
The scheduler sends reminders, runs campaigns and cleans up. It needs one cron job, running every minute as the site's user:
crontab -u threadline -e
If it asks which editor to use, choose nano. Add this
line at the end, then save (Ctrl+O,
Enter, Ctrl+X):
* * * * * cd /var/www/threadline && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
Check it was saved with
crontab -u threadline -l. See also
Cron Setup.
Step 14: Check that everything works
Sign in as the Owner and open System → Cron & Health (details).
- Within a minute or two it should say The scheduler last ran a moment ago.
- The requirements list should be all green.
- Open System → Queue (details). The Worker card says Running within half a minute of the worker starting, even with no work yet.
Reboot the server once to be sure (reboot): nginx,
PHP, MySQL, Redis, Docker with Qdrant and the Supervisor worker
all start again by themselves, and the site is back within about
half a minute. Then connect an AI provider
(AI Provider Setup) and set
up email (Mail & Email Templates).
Optional: send public pages as finished HTML
Search engines read your public pages (home, pricing, about and
the others) best when the server sends them already built. With
SERVER=vps Threadline does that as soon as a small
Node.js program runs next to it. Without it, pages are built in
the visitor's browser as before, and nothing else changes.
Screens behind the sign-in are always built in the browser.
apt-get install -y nodejs
node --version
The version must be 18 or newer. Then let Supervisor keep it running:
cat > /etc/supervisor/conf.d/threadline-ssr.conf <<'EOF'
[program:threadline-ssr]
command=/usr/bin/node /var/www/threadline/bootstrap/ssr/app.js
directory=/var/www/threadline
user=threadline
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/www/threadline/storage/logs/ssr.log
EOF
supervisorctl reread
supervisorctl update
supervisorctl status
Both programs must say RUNNING. It uses about 100 MB
of memory. To check it, open your home page in a private window
and view the page source: the page's text is in it, not only an
empty <div id="app">. An update restarts it on
the new version by itself.
Updating
Because the files are on the server's disk, you can update from the browser: System → Maintenance → App Update, upload the new ZIP and click Install. It backs up the database first, replaces the files, applies database changes and tells the worker to restart. Supervisor starts it again by itself.
Or over SSH, as the site's user (not as root):
cd /var/www/threadline
sudo -u threadline php artisan threadline:update /tmp/threadline-v1.2.0.zip
Copy the ZIP to the server first, for example with
scp threadline-v1.2.0.zip root@YOUR_SERVER_IP:/tmp/.
The command shows the installed and the new version and asks
Continue?; type yes. It backs up the
database, replaces the files with a progress bar, runs the
database changes and ends with Updated. The site is back
online. Supervisor restarts the worker on the new version
by itself. You can then delete the ZIP from /tmp.
Full details, including what the backup contains, are in Updating Threadline.
If something goes wrong
| What you see | Likely cause and fix |
|---|---|
| 502 Bad Gateway |
nginx cannot reach PHP. Check
systemctl status php8.3-fpm and that the socket
path in step 6 is /run/php/threadline.sock.
|
| The home page works, but every other page is a 404 |
The try_files rule is missing from the nginx
site (step 6).
|
| The browser shows nginx's welcome page |
The default site is still enabled. Run
rm /etc/nginx/sites-enabled/default and reload
nginx.
|
| Installer: storage/ or .env is writable is red |
The files are not owned by threadline. Run
chown -R threadline:threadline /var/www/threadline
and reload.
|
| Installer: a PHP extension is Missing |
Install it, for example
apt-get install php8.3-gd, then
systemctl restart php8.3-fpm.
|
| The scheduler never runs |
Check crontab -u threadline -l: the line must be
there, with the right folder. Run it by hand to see the
error:
sudo -u threadline sh -c 'cd /var/www/threadline && php artisan schedule:run'
|
| Pages load, but no message is ever sent |
The worker is not running. Run
supervisorctl status; if it is not
RUNNING, read
/var/www/threadline/storage/logs/worker.log and
check that QUEUE_CONNECTION is
redis (step 10).
|
| Knowledge-base answers are empty after you add documents |
Qdrant is not reachable. Run
curl http://127.0.0.1:6333/readyz and check
docker ps (step 8).
|
More in Troubleshooting & FAQ.