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

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

  1. 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.
  2. 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.
  3. 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.

  1. Go to App Store → Installed. On the PHP-8.4 row click Setting.
  2. 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.
  1. On the same PHP-8.4 Setting window, open Disabled functions.
  2. Click Del next to proc_open. aaPanel shows Setup successfully! and the list jumps back to the top.
  3. Scroll down and do the same for symlink, pcntl_alarm and pcntl_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

  1. 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.
  2. 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

  1. Go to Website and click Add site.
  2. Domain name: your domain, for example desk.example.com.
  3. 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).
  4. FTP: Not create. Database: choose MySQL. aaPanel fills in a database user and a password. PHP version: PHP-84.
  5. 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 example sql_desk_example_com, and the database has the same name. You can find them again under Databases.

Step 8: Upload Threadline

  1. In the panel open Files and go to /www/wwwroot/desk.example.com. Click Upload and choose the Threadline ZIP, as you downloaded it.
  2. 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 by www, the user the website runs as, which is what Threadline needs.
  3. Check that the file artisan sits 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.

  1. In Website, click Conf on the right of your site's row.
  2. 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.
  3. 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; }.
  4. 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).
    1. Still in the Conf window, open SSL, then the Let's Encrypt tab.
    2. Leave Application Method on File Verification. Under Domain name tick your domain and click Apply. It takes about half a minute.
    3. 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!
    Visitors who open http:// are now sent to https://. The certificate renews itself: aaPanel already has a daily task for it under Cron. A .well-known folder owned by root now sits in your site's public folder; 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

  1. Open your domain in a browser once. Threadline gives its .env file its own secret key and shows the installer. Leave that page open.
  2. In Files, go to /www/wwwroot/desk.example.com and 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.

  1. 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.
  2. 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 example https://desk.example.com, and pick your timezone. Leave the Encryption key as it is; it is created for your site alone.
  3. Database. Host 127.0.0.1 and port 3306 are 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.
  4. Owner. The name, email and password of your own Owner account.
  5. 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.

  1. Go to App Store → Installed, and click Setting on the Supervisor row, then Add Daemon.
  2. Fill in the form:
    Field Value
    Name threadline-worker
    Run User www
    Process directory /www/wwwroot/desk.example.com
    Start Command /www/server/php/84/bin/php artisan queue:work redis --queue=default,mail,webhooks --sleep=3 --tries=3 --max-time=3600
    Processes 1
  3. 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.

  1. Go to Cron. Under Add Task leave Task type: Shell Script.
  2. Fill in the form:
    Field Value
    Task name Threadline scheduler
    Execute cycle N Minutes, then change the number from 30 to 1. The page then says executed every 1 minutes.
    Execute user www (the list starts at root; www is near the bottom, just above lsadm, mysql and redis)
    Script content cd /www/wwwroot/desk.example.com && /www/server/php/84/bin/php artisan schedule:run >> /dev/null 2>&1
  3. 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).

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.

  1. Install Node.js 18 or newer. Over SSH:
    apt-get install -y nodejs
    which node
    The second command prints its full path, usually /usr/bin/node.
  2. In Supervisor, click Add Daemon again, as in step 12, with Name threadline-ssr, Run User www, Process directory /www/wwwroot/desk.example.com, Processes 1 and this Start Command:
    /usr/bin/node bootstrap/ssr/app.js
    Its Status must show RUNNING.
  3. 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.