Install on a VPS (Docker)
Use this guide for a server you manage yourself over SSH, with no control panel. One command starts Threadline together with MySQL, Redis, Qdrant (the vector database), a queue worker and the scheduler. You provide the domain and HTTPS. If you use Dokploy, follow Install: VPS (Dokploy) instead.
What you need
- A Linux VPS with root or sudo access. Ubuntu 22.04 or 24.04 is the easiest. We recommend at least 2 CPU cores, 4 GB of RAM and 20 GB of disk, because MySQL, Redis, Qdrant and three PHP containers all run on it.
- A domain name with an A record pointing at the server's IP address, and ports 80 and 443 open in the firewall.
-
The Threadline ZIP from CodeCanyon or ui-lib.com, for example
threadline-v1.2.0.zip. This guide calls itthreadline.zip.
Steps
-
Install Docker. On Ubuntu, Docker's own
install script is the quickest way. You need Docker Engine and
the Compose plugin (version 2 or newer):
The first command takes about half a minute. The second should print a version number. Docker's own documentation covers other systems.curl -fsSL https://get.docker.com | sh docker compose version -
Open the firewall. Allow SSH and web traffic
only:
ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enableufw enablewarns that it may disrupt SSH; typey, your connection stays open. If your hosting provider has its own firewall, open ports 22, 80 and 443 there too. The database, Redis and Qdrant are never published outside Docker, and the app is published only on the server itself (step 6), so nothing else needs opening. -
Upload the files. Copy the ZIP to the server,
unpack it, for example into
/opt/threadline, and go into it:
Make surescp threadline.zip root@YOUR_SERVER_IP:/opt/ ssh root@YOUR_SERVER_IP cd /opt && apt-get install -y unzip && unzip threadline.zip -d threadline cd /opt/threadlinedocker-compose.ymlis directly inside the folder you are in. If the zip created an extra folder level,cdinto it. -
Fill in your settings file. The package
comes with a
.envfile. Open it:
Find a line withnano .envCtrl+W. Save withCtrl+O,Enter, and leave withCtrl+X. Generate a key and two database passwords. These commands print values you can paste in; the passwords use only letters and digits on purpose, because symbols such as$and#can break the file:
Set these lines inecho "base64:$(openssl rand -base64 32)" openssl rand -hex 16 openssl rand -hex 16.env(change the ones that are already there, add the ones that are not):APP_NAME=Threadline APP_ENV=production APP_DEBUG=false APP_KEY=base64:PASTE_THE_KEY_HERE APP_URL=https://your-domain.com SERVER=vps DB_DATABASE=threadline DB_USERNAME=threadline DB_PASSWORD=PASTE_FIRST_PASSWORD DB_ROOT_PASSWORD=PASTE_SECOND_PASSWORD # Publish the app only on this machine, for the reverse proxy in step 6 APP_BIND=127.0.0.1 APP_PORT=8080You do not need to set the database host, Redis, Qdrant, queue, cache or session settings. The Compose file sets them for you, so leave lines such asUse your real
https://address forAPP_URL. It becomes the base of your webhook URLs. KeepAPP_ENV=productionandAPP_DEBUG=false: with debug on, errors show private settings to visitors. Never userootasDB_USERNAME, and keep the two passwords different.CACHE_STORE=databaseandQUEUE_CONNECTION=databaseas they are: inside Docker they are replaced. Email can be set up later in the Owner area (see Mail & Email Templates).When you have saved it, make the file readable by root only, because it holds the database passwords and the app key. Docker reads it as root, so nothing else needs it:
chmod 600 .env -
Start Threadline.
The first run builds the image, which takes about five minutes on a 2-core server; most of it is compiling PHP extensions, with a lot of scrolling text. It ends with each container Started. Then check that everything is up:docker compose -f docker-compose.yml -f docker-compose.ports.yml up -d --build
You should see six services running:docker compose ps docker compose logs -f appapp,queue,scheduler,mysql,redisandqdrant. For the first half minute some say health: starting; rundocker compose psagain until they say healthy (theschedulerhas no health check and just says Up). In theapplog, the database migrations run first and then nginx starts. PressCtrl+Cto leave the log; the containers keep running. -
Add HTTPS with a reverse proxy. Threadline
listens on
127.0.0.1:8080, so nothing outside the server can reach it yet. Put a proxy in front. The simplest is Caddy, which gets and renews your HTTPS certificate on its own. On Ubuntu 24.04 it is one command (on 22.04, follow Caddy's own install page):
Open its settings file withapt-get install -y caddynano /etc/caddy/Caddyfile, delete everything in it (it holds a sample:80site), and put in only this:
then reload it:your-domain.com { reverse_proxy 127.0.0.1:8080 }
Within a few seconds Caddy gets the certificate, and plainsudo systemctl reload caddyhttp://visits are sent tohttps://. The domain's A record must already point at the server. nginx with Certbot, or Cloudflare in front, work the same way: forward to127.0.0.1:8080and keep the original host and protocol headers. Threadline trusts the proxy automatically in this mode. -
Create the Owner account. Open
https://your-domain.com. Because the environment is already configured, Threadline skips the requirements and database steps and shows one Create owner account screen. Fill it in, then log in.
There is no cron job to add in this mode: the
scheduler container runs scheduled tasks and the
queue container sends messages.
Reboot the server once to be sure (reboot): Docker,
all six containers and Caddy start again by themselves, and the
site is back within about half a minute.
After you install
- Connect an AI provider. See AI Provider Setup.
- Set up email in Mail & Email Templates.
- Check Cron & Health to confirm the scheduler is running.
Updating
Copy the new version's ZIP to the server and unpack it over the
folder, leaving out its .env (the
-x .env below) so your own settings are kept. Then
run the update script. It saves a database dump, rebuilds,
restarts and checks the result:
scp threadline-v1.2.0.zip root@YOUR_SERVER_IP:/opt/
Then, on the server:
cd /opt && unzip -o threadline-v1.2.0.zip -x .env -d threadline
cd /opt/threadline
bash update.sh
It takes about two minutes and ends with The app is up.
Running version: 1.2.0. The dump is saved in the
backups folder next to .env; copy it
off the server. Within a minute the queue worker and the
scheduler show the new version under
System → Maintenance.
Prefer to do it by hand? The script only wraps this command:
docker compose -f docker-compose.yml -f docker-compose.ports.yml up -d --build
Database changes are applied automatically when the
app container starts. The uploader in
Maintenance is not used in this
mode: you update by rebuilding. Every option is in
Updating Threadline.
Never run docker compose down -v.
The -v deletes the database and your uploaded
files. docker compose down without it is safe.
Backups
Your data lives in Docker volumes. A fourth one,
redis_data, holds only waiting jobs and sign-in
sessions and does not need a backup.
mysql_data: the database. Back this up.-
storage_data: uploaded files, knowledge-base documents and in-app backups. Back this up. -
qdrant_data: the vector index. It can be rebuilt from your documents, but that costs AI time and tokens, so back it up too.
A database dump is safer than copying the raw volume:
docker compose exec mysql sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE"' > threadline-$(date +%F).sql
Copy the dump off the server. Run docker volume ls
to see the exact volume names, which start with your folder's
name.
If something goes wrong
| What you see | Likely cause and fix |
|---|---|
| The MySQL container keeps restarting |
DB_USERNAME is root, or
DB_ROOT_PASSWORD is missing. Fix
.env and start again. If it already created
the database with the wrong password, remove the volume
once (docker compose down -v) — only on
a brand-new install with no data.
|
| The site loads without styling, or shows blank |
APP_URL does not match the address you open,
or the proxy is not forwarding the host and protocol.
|
| The proxy shows 502 Bad Gateway |
The app is not up yet, or the port differs. Run
docker compose ps and check that
APP_PORT matches your proxy.
|
| "port is already allocated" |
Something else uses the port. Change
APP_PORT in .env.
|
| The build is killed or stalls | The server ran out of memory. Add swap space or use a server with more RAM. |
More in Troubleshooting & FAQ.