Docs ยท Deployment

Deploy Trestle on a company subdomain

This guide deploys one Trestle instance for a team behind a company hostname such as trestle.company.com. It is self-contained: you do not need to install Cortex, Warden or Watchpost to use it. Trestle, Cortex, Warden and Watchpost can be deployed together on sibling subdomains, and every project also works independently. Sharing a parent domain creates no implicit trust or shared authentication - credentials, cookies, databases, service accounts and authorization remain separate per application. Use one distinct hostname per application (for example trestle.company.com rather than company.com/trestle), because independent subdomains simplify proxying, cookie scope, origins, upgrades and operational isolation.

Ecosystem map.trestle.company.com is the backend platform, cortex.company.com the coding agent, warden.company.com the browser IDE and agent, and watchpost.company.com the monitoring control plane. Deploy only the projects you need.

Recommended topology

Internet or company network
        |
        v
DNS hostname (trestle.company.com)
        |
        v
Caddy or nginx with HTTPS (TLS termination, hostname routing)
        |
        v
127.0.0.1:7333
        |
        v
systemd user service (trestle.service)

Trestle listens on loopback by default. The reverse proxy owns the externally reachable hostname and TLS termination; Trestle stays bound to 127.0.0.1.

1. Choose the hostname

Pick a stable internal or public name. trestle.company.com is used throughout this guide.

2. Create the DNS record

With a fixed public IP, create an A record (and an AAAA record with the IPv6 address where applicable):

Type: A
Name: trestle
Value: 203.0.113.10

When the hostname should point at another hostname instead, use a CNAME:

Type: CNAME
Name: trestle
Value: apps.company.com

DNS only maps the hostname. It does not select the application port, provide HTTPS, or secure the service - Caddy or nginx performs hostname routing and TLS termination. Private deployments do not require public DNS: split-horizon DNS, internal DNS and VPN-only hostnames are valid and often preferable.

3. Install the Trestle binary

Place the binary at a stable absolute path:

mkdir -p /opt/trestle
install -m 0755 trestle /opt/trestle/trestle

The recorded unit contains an absolute executable path; moving or deleting it breaks the service until you reinstall.

4. Install the service unit

Keep the listener on loopback and point the service at a durable data directory and a protected TRESTLE_* environment file:

trestle service install \
  --host 127.0.0.1 --port 7333 \
  --data-dir /var/lib/trestle \
  --env-file /etc/trestle/trestle.env

The environment file is loaded as an owner-only EnvironmentFile; it must be owned by the service user with restrictive permissions. Configure the external origin via TRESTLE_PUBLIC_URL in that file where applicable, and trust only the loopback proxy peer with TRESTLE_TRUSTED_PROXIES=127.0.0.1/32.

5. Keep the listener on loopback

Installing with --host 127.0.0.1 --port 7333 keeps Trestle private; only the reverse proxy on the same host reaches it. The legacy --listen 127.0.0.1:7333 single-address form is also supported. Browser sessions and API authentication remain mandatory even behind the proxy.

6. Database and storage configuration

Choose SQLite for a single-node deployment (the default, with its data under the data directory) or PostgreSQL with TRESTLE_DATABASE_PROVIDER=postgres and TRESTLE_DATABASE_URL. Object storage is local by default; switch to S3-compatible storage with TRESTLE_STORAGE_BACKEND=s3 and the TRESTLE_S3_* (and where applicable TRESTLE_AWS_*) settings in the environment file. First-run picks the provider and creates the administrator before serving traffic.

7. Configure Caddy

trestle.company.com {
    reverse_proxy 127.0.0.1:7333 {
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
    request_body {
        max_size 500MB
    }
}

Trestle's realtime stream (/api/v1/realtime) uses Server-Sent Events; do not buffer it (Caddy proxies streaming responses natively). Set the body limit large enough for configured uploads while retaining Trestle's own per-object and per-file limits. Automatic HTTPS requires working public DNS and a reachable ACME challenge path; private networks should use an internal certificate or company PKI.

8. Configure nginx as an alternative

server {
    listen 80;
    server_name trestle.company.com;
    return 301 https://trestle.company.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name trestle.company.com;

    ssl_certificate     /etc/letsencrypt/live/trestle.company.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/trestle.company.com/privkey.pem;

    client_max_body_size 500m;

    location / {
        proxy_pass http://127.0.0.1:7333;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Obtain the certificate with Certbot or your company PKI before starting nginx. The proxy deliberately replaces X-Forwarded-* from $remote_addr rather than trusting a client-supplied header. proxy_buffering off keeps the realtime SSE stream flowing, and the long timeouts accommodate long-running batch and job operations.

9. Obtain and verify HTTPS

curl -I https://trestle.company.com

Confirm the certificate is issued for the hostname and the TLS handshake completes.

10. Verify the public health endpoint

curl -s https://trestle.company.com/system/health

Expect a JSON health response. /system/health stays healthy only while the database is reachable; /system/ready returns 503 until the system is ready to serve.

11. Open the application and complete first-run setup

Open https://trestle.company.com and complete first-run administration: select the database provider, run migrations, and create the first administrator. This happens once; subsequent starts serve traffic immediately.

12. Confirm boot enablement

The machine service starts at boot without any login (systemd system unit, WantedBy=multi-user.target); no systemd lingering is required. Confirm enablement structurally:

systemctl is-enabled trestle.service
 grep -q 'WantedBy=multi-user.target' /etc/systemd/system/trestle.service

Trestle never enables lingering because it does not use user units.

13. Verify service status and logs

trestle service status
trestle service logs

status reports enabled/running state, PID, version, listen address and a live health check of /system/health. Back up the data directory and database regularly, and expect a restart after an upgrade or configuration change to pick up the new build.

14. Troubleshoot

  • DNS - the hostname resolves but nothing loads: confirm the A/AAAA or CNAME points at the proxy host, and use internal DNS for VPN-only names.
  • TLS - certificate warnings: automatic HTTPS needs public DNS and reachable ACME paths; internal deployments need a company PKI/internal CA.
  • Proxy - requests loop or authentication fails: the proxy must replace forwarding headers and TRESTLE_TRUSTED_PROXIES must list only the actual loopback proxy peer.
  • Realtime - SSE events appear only at the end: disable proxy buffering so the realtime stream flows.
  • Database - /system/health degrades: confirm TRESTLE_DATABASE_URL connectivity and PostgreSQL initialization before the service starts.