Install on a VPS with aaPanel
aaPanel is a free control panel that installs and manages the web server, PHP and database on a Linux server for you. This guide takes a brand-new server to a working Threadline, with Redis, Qdrant, the background queue worker and the scheduler all running, using aaPanel's screens and a few short commands. It takes about 30 minutes, most of it waiting for installs.
Every step was tried on a fresh Ubuntu 24.04 server with aaPanel 8.0.7, nginx 1.30, PHP 8.4 and MariaDB 10.11. If your aaPanel looks slightly different, the names of the buttons are the same.
Want the simplest setup? On aaPanel you can also run
Threadline in shared mode, with no Redis, Qdrant or worker.
Follow steps 1 to 4, 7 to 9 and 11 below, leave
SERVER=shared in .env (skip step
10) and add only the cron job from step 13. See
Do you need this guide?.
What you need
- A new, empty server running Ubuntu 22.04 or 24.04 (aaPanel also supports Debian 11 and 12), with root SSH access. 2 CPU cores and 4 GB of RAM is comfortable: with everything below running, about 1 GB is in use.
-
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.
Start from an empty server. aaPanel installs its own web server, PHP and database, so do not add it to a server that already runs nginx, Apache or MySQL for something else.
Step 1: Install aaPanel
Connect to the server over SSH as root and run:
wget -O install_panel_en.sh https://www.aapanel.com/script/install_panel_en.sh
bash install_panel_en.sh -y
It takes about a minute. At the end it prints the panel address,
a user name and a password, for example
https://203.0.113.10:23456/a1b2c3d4. The port and
the part after it are different on every server.
Copy all three. If you lose them, run
bt default on the server to see them again.
The panel listens on its own port (23456 in the example). If the address does not open, allow that port in your hosting provider's firewall or security group, along with ports 80 and 443 for your website.
aaPanel also switches on the server's own firewall
(ufw) and opens SSH, ports 80 and 443 and the
panel port for you, so there is nothing to set up on the
server itself. The firewall at your hosting provider is a
separate one, and you set that yourself.
Step 2: Install nginx, PHP and the database
- Open the panel address in your browser and sign in with the user name and password from step 1. If a window says The installation was successful and offers a Finish button, do not click it: it only takes you to aapanel.com. Use your browser's Back button to return to the panel.
- On the first sign-in aaPanel opens a Software install window. Keep Method: Quick install, keep nginx, PHP 8.4 and MySQL (MariaDB) selected, and click One-click. Threadline needs PHP 8.3 or newer, so 8.3 works as well.
- Wait for it to finish, about 8 minutes. aaPanel shows the progress in a Messages Box window; when its task list reads Currently no tasks!, everything is installed. The suite also installs Apache, OpenLiteSpeed, phpMyAdmin and an FTP server. You do not need them and can remove them later under App Store → Installed to save memory.
The FTP server and phpMyAdmin each ask the firewall to open a
port to the internet (21 and 888). If you remove them, run
ufw status afterwards and delete any rule that
is left, for example ufw delete allow 21/tcp.
Threadline itself needs only SSH, 80, 443 and the panel port.
If the window did not open, go to App Store and install nginx, PHP 8.4 and MySQL from the list.
Step 3: Add the Fileinfo PHP extension
aaPanel's PHP is missing one extension Threadline needs. Without it the installer shows Fileinfo extension: Missing.
- Go to App Store → Installed. On the PHP-8.4 row click Setting.
- Open Install extensions, find fileInfo (it is near the top of the list), click Install and confirm. It takes less than a minute.
The first time you open App Store, aaPanel may show a Log in to aaPanel account window. You do not need an account: click Skip. It can come back later; skip it every time.
Step 4: Allow the PHP functions Threadline needs
For safety aaPanel switches off a long list of PHP functions. Four of them are needed. If you skip this step nothing warns you at first: the scheduler reports success while running nothing, and the queue worker stops the moment it starts, so no message is ever sent.
| Function | Why Threadline needs it |
|---|---|
proc_open |
Runs every scheduled task (reminders, campaigns, clean-up). |
pcntl_signal |
Lets the queue worker shut down cleanly and be restarted by an update. |
pcntl_alarm |
Lets the queue worker stop a job that takes too long. |
symlink |
Creates the link that shows uploaded logos and images. |
- On the same PHP-8.4 Setting window, open Disabled functions.
-
Click Del next to
proc_open. aaPanel shows Setup successfully! and the list jumps back to the top. -
Scroll down and do the same for
symlink,pcntl_alarmandpcntl_signal, one at a time. Leave every other function disabled; Threadline does not use them.
Right under pcntl_signal the list has
pcntl_signal_dispatch, a different function.
Check the name before you click Del and leave
that one disabled.
The change applies straight away. If you use PHP 8.3, do this under PHP-8.3 instead.
Step 5: Install Redis and Supervisor
- In App Store, click ALL next to App Sort. (If the list is still on Installed from step 3, the search finds nothing.) Search for redis, click Install on Redis (7.2) and then Install Now. aaPanel builds it on your server, which takes about 3 minutes.
- Search for supervisor and install Supervisor (3.1) the same way. It takes a few seconds. It keeps the queue worker running.
If the Log in to aaPanel account window shows up again, click Skip. Redis listens on the server only, with no password, which is what Threadline expects. Do not open port 6379 to the internet.
Step 6: Install Qdrant
Qdrant is the vector database. The simplest way to run it is Docker, for this one service only. Back in your SSH session:
curl -fsSL https://get.docker.com | sh
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
Check that it is ready (it can take a few seconds to start):
curl http://127.0.0.1:6333/readyz
It should answer all shards are ready. Qdrant only
listens on the server itself. Do not open port 6333.
Step 7: Add the site and its database
- Go to Website and click Add site.
-
Domain name: your domain, for example
desk.example.com. - Apply for SSL: untick it. You turn HTTPS on in step 9, where you can see whether it worked. The form rearranges itself when you type the domain, and the box comes ticked, so untick it after typing the domain, and click only once (a second click ticks it again).
- FTP: Not create. Database: choose MySQL. aaPanel fills in a database user and a password. PHP version: PHP-84.
-
Click Confirm. A window then shows the
database user and password. Copy both now.
The user is
sql_followed by your domain with the dots turned into underscores, for examplesql_desk_example_com, and the database has the same name. You can find them again under Databases.
Step 8: Upload Threadline
-
In the panel open Files and go to
/www/wwwroot/desk.example.com. Click Upload and choose the Threadline ZIP, as you downloaded it. -
Right-click the uploaded ZIP and choose
Extract. Leave Extract path
as it is (the site's own folder) and
Dir permission at
755, then click Confirm. The files are created owned bywww, the user the website runs as, which is what Threadline needs. -
Check that the file
artisansits directly inside/www/wwwroot/desk.example.com, not inside another folder. If it is one folder deeper, select its contents and move them up one level. You can then delete the ZIP.
If the browser upload is slow or stops, copy the ZIP from your
computer with
scp threadline.zip root@YOUR_SERVER_IP:/www/wwwroot/desk.example.com/
and then do the Extract step above. The
files still come out owned by www.
If you extract over SSH instead (for example
unzip -o /tmp/threadline.zip -d /www/wwwroot/desk.example.com
as root), give the files to the website user afterwards:
chown -R www:www /www/wwwroot/desk.example.com.
One line about .user.ini saying Operation not
permitted is normal, because the panel locks that file.
Step 9: Point the site at the public folder, and turn on HTTPS
The web server must serve only the public folder.
The rest of the files must not be reachable from the internet.
- In Website, click Conf on the right of your site's row.
- Open Directory. Next to Running directory open the list and scroll to the bottom: /public is the last entry. Choose it and click that row's Save (the one on the Running directory line, not the one above it). Leave Anti-XSS attack switched on; Threadline works with it.
-
Open URL rewrite. Open the list at the top
left (it shows 0.Current), scroll down about
halfway to laravel5, choose it and click
Save. The box fills with
location / { try_files $uri $uri/ /index.php$is_args$query_string; }. -
Turn on HTTPS. Do this now, before the
installer, so the address you enter there is final. Your
domain must already point at the server, and port 443 must be
open (step 1).
- Still in the Conf window, open SSL, then the Let's Encrypt tab.
- Leave Application Method on File Verification. Under Domain name tick your domain and click Apply. It takes about half a minute.
- When it finishes, the tab reads Current Certs – [Deployed]. Open another tab and come back to SSL: a Force HTTPS switch now shows under the certificate details. Switch it on. aaPanel answers Setup successfully!
http://are now sent tohttps://. The certificate renews itself: aaPanel already has a daily task for it under Cron. A.well-knownfolder owned by root now sits in your site'spublicfolder; that is normal, it is where Let's Encrypt checks your domain.
If the page stays on Loading… for more
than two minutes after you click Apply, the panel
itself is stuck. This is a rare fault inside aaPanel, where
two of its background tasks wait for each other, and it is
not caused by your domain or server. Run
/etc/init.d/bt restart over SSH, reload the
panel page (you stay signed in) and apply again.
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. -
In Files, go to
/www/wwwroot/desk.example.comand double-click.env. It opens in an Online Text Editor. Change these four lines (select the whole line, then type the new one) and click Save at the top left: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. While you type, the editor may show a list
of suggestions: press Esc to close it, and do not
press Enter or Tab while it is open.
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 in the browser. The installer has five steps. It is the same one as on shared hosting, described in Install: Shared Hosting.
- Requirements. Every row should be green, including Fileinfo extension and proc_open() function. If one is red, see the table at the bottom of this page, fix it and reload.
-
App. The Site URL shows the
address you opened the installer with. Check that it is the
final address of your site, starting with
https://, for examplehttps://desk.example.com, and pick your timezone. Leave the Encryption key as it is; it is created for your site alone. -
Database. Host
127.0.0.1and port3306are already right. Type the database name, the user and the password from step 7. The name and the user are the same. Click Test connection, then Continue. - Owner. The name, email and password of your own Owner account.
- Install. Wait while 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.
The user is not root. Before you
click Test connection, check that the name
and user boxes hold the sql_… values from
step 7. A browser or password manager can fill in a saved
login, and then the test fails with Access denied —
check the username and password even though the password
is right.
Do steps 9 to 11 in one sitting. From step 9 the installer is reachable by anyone on the internet, and scanners find a new domain within minutes. Whoever finishes the installer first becomes the Owner. If your address shows a sign-in page instead of the installer and you did not finish it yourself, someone else did: delete the site and its database in the panel and start again from step 7.
When the installer has finished, make .env readable
by the site only. It holds the database password and the app
key, and by default any account on the server can read it. Over
SSH, or in the panel's Terminal (left menu):
chmod 600 /www/wwwroot/desk.example.com/.env
ls -l /www/wwwroot/desk.example.com/.env
The second command should start with -rw------- and
name www twice. Threadline can still change the file
itself, because www owns it.
Step 12: Start the queue worker
On a VPS a worker must always be running, or no message is sent. aaPanel's Supervisor keeps it alive and starts it again after a restart.
- Go to App Store → Installed, and click Setting on the Supervisor row, then Add Daemon.
-
Fill in the form:
Field Value Name threadline-workerRun User wwwProcess directory /www/wwwroot/desk.example.comStart Command /www/server/php/84/bin/php artisan queue:work redis --queue=default,mail,webhooks --sleep=3 --tries=3 --max-time=3600Processes 1 - Click Confirm. The daemon appears in the list and its Status shows RUNNING. The Log link next to it shows what the worker prints.
The PHP in the command is the one of your site. For PHP 8.3 it is
/www/server/php/83/bin/php. The command to use for
each PHP version is also shown on that version's
Service tab.
Step 13: Add the cron job
The scheduler sends reminders, runs campaigns and cleans up. It needs one cron job that runs every minute.
- Go to Cron. Under Add Task leave Task type: Shell Script.
-
Fill in the form:
Field Value Task name Threadline schedulerExecute cycle N Minutes, then change the number from 30to1. The page then says executed every 1 minutes.Execute user www (the list starts at root;wwwis near the bottom, just abovelsadm,mysqlandredis)Script content cd /www/wwwroot/desk.example.com && /www/server/php/84/bin/php artisan schedule:run >> /dev/null 2>&1 - Click Add Task.
Do not leave Execute user on
root. Files the scheduler creates would belong to
root, and the website could no longer write to them.
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. If it says The scheduler has never run, see The scheduler never runs below.
- The requirements list should be all green, including PHP function: proc_open and PHP functions: pcntl_signal, pcntl_alarm.
- Open System → Queue (details). The Worker card says Running within half a minute of the worker starting, even with no work yet. Last job under it says No job handled yet until you send a test message or an email; that is normal. If the card says Not seen yet for more than a minute, check the worker's Status in Supervisor (step 12).
To be sure it survives a restart, reboot the server once. aaPanel,
nginx, PHP, the database, Redis, Docker with Qdrant and the
Supervisor worker all start again by themselves, within about a
minute, and Cron & Health shows the
scheduler running again a minute after that. Nothing needs to be
started by hand. The panel itself asks you to sign in again
after a reboot: use the address from step 1
(bt default prints it).
Next, 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.
-
Install Node.js 18 or newer. Over SSH:
The second command prints its full path, usuallyapt-get install -y nodejs which node/usr/bin/node. -
In Supervisor, click Add Daemon
again, as in step 12, with Name
threadline-ssr, Run Userwww, Process directory/www/wwwroot/desk.example.com, Processes1and this Start Command:
Its Status must show RUNNING./usr/bin/node bootstrap/ssr/app.js -
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">.
It uses about 100 MB of memory. 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 www user (not as root):
cd /www/wwwroot/desk.example.com
sudo -u www /www/server/php/84/bin/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 |
|---|---|
| The panel address does not open |
The panel's port is closed in your provider's firewall
(step 1). Run bt default on the server to see
the address, user name and password again.
|
| A panel page stays on Loading… for minutes |
The panel is stuck (a rare aaPanel fault, seen while
applying a Let's Encrypt certificate). Run
/etc/init.d/bt restart on the server, reload
the page and try again. You stay signed in.
|
| App Store search finds nothing for redis or supervisor | The list is still on Installed. Click ALL next to App Sort and search again (step 5). |
| Installer: Access denied — check the username and password |
The user or password is not the one from step 7. Check that
Database name and Username
both hold the sql_… value (they are the
same), not root or a login your browser filled
in, then test again.
|
| A red banner in the panel says PHP has a security vulnerability | It is aaPanel's standing notice. Go to App Store → Update App List; if PHP-8.4 shows no update, you are current. After any PHP update, open Cron & Health once: a red row means a function from step 3 or 4 has to be switched back on. |
| Installer: Fileinfo extension: Missing | Install the extension (step 3), then reload. |
| Installer: proc_open() function or pcntl_signal(), pcntl_alarm() is red | Remove the function from Disabled functions (step 4), then reload. |
| Installer: .env is writable or storage/ is writable is red |
The files are not owned by www, which
happens when they were extracted over SSH as root. Run the
chown command from the note in step 8.
|
| Let's Encrypt says the certificate could not be issued |
The domain does not point at this server yet, or port 80 is
closed. Check the A record, and that the website opens on
http://your-domain. Wait a few minutes for DNS
to spread and apply again.
|
| The home page works, but every other page is a 404 | The laravel5 rewrite is missing or not saved (step 9). |
| The browser shows Site is created successfully! instead of Threadline | That is aaPanel's placeholder page. The running directory is still /, not /public (step 9). Never serve the project's main folder. |
| 502 Bad Gateway | PHP is not running. In App Store → Installed open PHP-8.4's Setting and press Restart on the Service tab. |
| The scheduler never runs: The scheduler has never run |
Check the cron task in Cron: the cycle must
be every 1 minute, the user must be www, and
the path must be your site's folder. The task's
Log link shows each run.
|
| Pages load, but no message is ever sent |
The worker is not running. In Supervisor
the Status must say
RUNNING. If it does not, open its
Log, check step 4 (a missing
pcntl_signal stops it at once) and that
QUEUE_CONNECTION is redis
(step 10).
|
| An uploaded logo does not show, or saving settings gives a 500 error |
The storage link is missing because symlink is
still disabled (step 4). After fixing that, open
System → Maintenance → App Core
and click Create Symlink. Over SSH the
same is
sudo -u www /www/server/php/84/bin/php artisan storage:link
in the site's folder.
|
| 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 6).
|
More in Troubleshooting & FAQ.