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

Steps

  1. 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):
    curl -fsSL https://get.docker.com | sh
    docker compose version
    The first command takes about half a minute. The second should print a version number. Docker's own documentation covers other systems.
  2. Open the firewall. Allow SSH and web traffic only:
    ufw allow 22/tcp
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw enable
    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. 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.
  3. Upload the files. Copy the ZIP to the server, unpack it, for example into /opt/threadline, and go into it:
    scp 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/threadline
    Make sure docker-compose.yml is directly inside the folder you are in. If the zip created an extra folder level, cd into it.
  4. Fill in your settings file. The package comes with a .env file. Open it:
    nano .env
    Find a line with Ctrl+W. Save with Ctrl+O, Enter, and leave with Ctrl+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:
    echo "base64:$(openssl rand -base64 32)"
    openssl rand -hex 16
    openssl rand -hex 16
    Set these lines in .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=8080

    Use your real https:// address for APP_URL. It becomes the base of your webhook URLs. Keep APP_ENV=production and APP_DEBUG=false: with debug on, errors show private settings to visitors. Never use root as DB_USERNAME, and keep the two passwords different.

    You 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 as CACHE_STORE=database and QUEUE_CONNECTION=database as 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
  5. Start Threadline.
    docker compose -f docker-compose.yml -f docker-compose.ports.yml up -d --build
    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 ps
    docker compose logs -f app
    You should see six services running: app, queue, scheduler, mysql, redis and qdrant. For the first half minute some say health: starting; run docker compose ps again until they say healthy (the scheduler has no health check and just says Up). In the app log, the database migrations run first and then nginx starts. Press Ctrl+C to leave the log; the containers keep running.
  6. 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):
    apt-get install -y caddy
    Open its settings file with nano /etc/caddy/Caddyfile, delete everything in it (it holds a sample :80 site), and put in only this:
    your-domain.com {
        reverse_proxy 127.0.0.1:8080
    }
    then reload it:
    sudo systemctl reload caddy
    Within a few seconds Caddy gets the certificate, and plain http:// visits are sent to https://. The domain's A record must already point at the server. nginx with Certbot, or Cloudflare in front, work the same way: forward to 127.0.0.1:8080 and keep the original host and protocol headers. Threadline trusts the proxy automatically in this mode.
  7. 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

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.

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.