TofuPilotTofuPilot

Self-hosting

Run TofuPilot on your own servers with Docker for full control over your data, network, and security.

Screenshot of TofuPilot application with custom url

When you create an account on tofupilot.com, your organization runs on our managed cloud. It is secure, performant, and always up to date.

For teams that need to keep data on-premises or comply with specific IT policies, you can self-host TofuPilot on your own server using Docker.

Prerequisites

Register your instance

You can run a self-hosted instance for free, for as long as you need. See Pricing for upgrade options.

  1. Sign in to TofuPilot Orbit
  2. Under Self-Hosting, click + to create a new instance
  3. Copy the deploy command, it contains your license key

The license key authenticates your instance and unlocks access to the Docker image.

System requirements

TofuPilot runs on any standard Linux server:

  • OS: Ubuntu 20.04+ or Debian (64-bit, x86_64)
  • CPU: 2+ cores
  • RAM: 4 GB minimum
  • Storage: 20 GB+ (scale with your runs and unit attachments)
  • Privileges: Sudo access
  • Network: Ports 80 and 443 open (all other services run inside Docker and are not exposed)

Configure

The deploy script uses a .env file for all your instance settings. On first run, it generates a template and exits so you can fill it in before deploying.

SSH into your server and run the deploy command you copied from Orbit:

curl -fsSL https://tofupilot.sh/deploy | bash -s -- --license YOUR_LICENSE_KEY

The previous command creates a .env file in your current directory. Open it with:

nano .env

The .env file also contains auto-generated secrets (AUTH_SECRET, DB_PASSWORD, S3_SECRET_ACCESS_KEY). Do not delete or modify these values — they are created on first deploy and reused on subsequent runs.

Domains

TofuPilot needs two subdomains. Create a DNS A record for each, pointing to your server, then set them in your .env:

  • DOMAIN_NAME for the application (e.g. tofupilot.yourcompany.com)
  • STORAGE_DOMAIN_NAME for file storage (e.g. storage.tofupilot.yourcompany.com)

SSL certificates

HTTPS is handled by Let's Encrypt by default. Just set your email:

SSL_MODE=letsencrypt
SSL_LETSENCRYPT_EMAIL=admin@yourcompany.com

If you use your own certificate authority, switch to custom mode instead. Use absolute paths on the host:

SSL_MODE=custom
SSL_CERT_PATH=/etc/ssl/tofupilot/fullchain.pem
SSL_KEY_PATH=/etc/ssl/tofupilot/privkey.pem

Authentication

You need at least one sign-in method. TofuPilot supports email magic links and OAuth providers. You can enable as many as you want.

If you skip email, users will only be able to sign in through OAuth and will not receive invitations or notifications from TofuPilot.

Users receive a one-time code by email to sign in. This also enables invitations and notifications.

EMAIL_SMTP_HOST=smtp.yourcompany.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=noreply@yourcompany.com
EMAIL_SMTP_PASSWORD=your-smtp-password
EMAIL_FROM_AUTH=noreply@yourcompany.com

Google

Create an OAuth app in the Google Cloud Console and set the redirect URI to:

https://tofupilot.yourcompany.com/api/auth/callback/google

Then copy the Client ID and Client Secret into your .env:

AUTH_GOOGLE_ID=your-client-id
AUTH_GOOGLE_SECRET=your-client-secret

Microsoft Entra ID

Register an app in the Azure Portal under App registrations and set the redirect URI to:

https://tofupilot.yourcompany.com/api/auth/callback/microsoft

Then copy the Application (client) ID and Secret into your .env:

AUTH_MICROSOFT_ENTRA_ID_ID=your-client-id
AUTH_MICROSOFT_ENTRA_ID_SECRET=your-client-secret
AUTH_MICROSOFT_ENTRA_ID_ISSUER=https://login.microsoftonline.com/your-tenant-id/v2.0

Replace your-tenant-id with your Azure AD tenant ID. For Azure Government, use login.microsoftonline.us.

GitHub

Create an OAuth app in GitHub Developer Settings and set the callback URL to:

https://tofupilot.yourcompany.com/api/auth/callback/github

Then copy the Client ID and Client Secret into your .env:

AUTH_GITHUB_ID=your-client-id
AUTH_GITHUB_SECRET=your-client-secret

GitLab

Create an application in GitLab (or your self-hosted GitLab instance) with the read_user and openid scopes. Set the redirect URI to:

https://tofupilot.yourcompany.com/api/auth/callback/gitlab

Then copy the Application ID and Secret into your .env:

AUTH_GITLAB_ID=your-application-id
AUTH_GITLAB_SECRET=your-application-secret
AUTH_GITLAB_ISSUER=https://gitlab.com

For self-hosted GitLab, replace https://gitlab.com with your instance URL.

Git provider

TofuPilot can connect to GitHub or GitLab to enable automatic deployments from your CI/CD pipelines.

GitHub

Self-hosted instances need their own GitHub App.

Step 1. Go to your GitHub organization Settings > Developer Settings > GitHub Apps > New GitHub App and fill in:

FieldValue
App nametofupilot-yourcompany (must be globally unique on GitHub)
Homepage URLhttps://DOMAIN_NAME

Step 2. In Identifying and authorizing users, leave the Callback URL empty. Uncheck Request user authorization (OAuth) during installation and Expire user authorization tokens.

Step 3. Fill in the remaining sections:

  • Post installation
    • Setup URL: https://DOMAIN_NAME/api/github/setup
    • Redirect on update: unchecked
  • Webhook
    • Active: checked
    • Webhook URL: https://DOMAIN_NAME/api/webhooks/github
    • Secret: a random string (e.g. run openssl rand -hex 32 in your terminal)
  • Repository permissions
    • Contents: Read-only
    • Metadata: Read-only (automatically selected)
    • Pull requests: Read-only
  • Organization permissions: none
  • Account permissions: none
  • Subscribe to events
    • Delete, Pull request, Push, Repository
  • Install target
    • Only on this account

Step 4. Click Create GitHub App. On the App's General page, note the App ID.

Step 5. Scroll to Private keys, click Generate a private key, and base64-encode the downloaded file:

base64 -w 0 < downloaded_file.pem
base64 -i downloaded_file.pem
[Convert]::ToBase64String([IO.File]::ReadAllBytes("downloaded_file.pem"))

Step 6. Add these variables to your .env:

GITHUB_APP_ID=your-app-id
GITHUB_APP_PRIVATE_KEY=your-base64-encoded-private-key
GITHUB_APP_WEBHOOK_SECRET=your-webhook-secret
GITHUB_APP_SLUG=tofupilot-yourcompany
  • GITHUB_APP_ID — from Step 4 (App's General page)
  • GITHUB_APP_PRIVATE_KEY — from Step 5 (base64-encoded output)
  • GITHUB_APP_WEBHOOK_SECRET — from Step 3 (webhook secret)
  • GITHUB_APP_SLUG — the last segment of your App URL: https://github.com/apps/{slug}

Step 7. After deploying, go to Settings > GitHub in your dashboard to install the App and select repositories.

GitLab

No extra environment variables are needed. Go to Settings > GitLab in your dashboard and click Connect to add your GitLab access token, instance URL, and select a group.

Deploy

Once your .env is ready, re-run the deploy script. The license key is already saved in your .env, so you don't need to pass it again:

curl -fsSL https://tofupilot.sh/deploy | bash

The script validates your configuration, pulls the Docker image, and starts four containers: the TofuPilot application (tofupilot-dashboard), PostgreSQL (tofupilot-pg), SeaweedFS for file storage (tofupilot-seaweed), and Traefik as a reverse proxy with automatic HTTPS (tofupilot-proxy).

After each deploy or update, the script reports the running version and domain to your Orbit account so you can monitor your instances. No other data leaves your server.

First login

Open your domain (e.g. https://tofupilot.yourcompany.com) and create your first account. This account becomes the workspace admin. Each self-hosted instance supports one organization.

Upload runs

To upload test runs, point your scripts to your instance URL. The SDK reads your API key from the TOFUPILOT_API_KEY environment variable, or you can pass it directly:

import openhtf as htf
from tofupilot.openhtf import TofuPilot

def main():
  test = htf.Test(
      procedure_id="FVT1",
      part_number="PCB1",
  )
  with TofuPilot(test, url="https://tofupilot.yourcompany.com"):
      test.execute(lambda: "PCB1A001")

if __name__ == '__main__':
  main()
from tofupilot import TofuPilotClient
from datetime import timedelta

def main():
  client = TofuPilotClient(url="https://tofupilot.yourcompany.com")

  client.create_run(
      procedure_id="FVT1",
      run_passed=True,
      unit_under_test={
          "serial_number": "PCB1A001",
          "part_number": "PCB1"
      },
      duration=timedelta(minutes=1, seconds=45),
  )

if __name__ == '__main__':
  main()
from tofupilot.v2 import TofuPilot

def main():
  client = TofuPilot(
      api_key="your_api_key",
      server_url="https://tofupilot.yourcompany.com/api"
  )

  result = client.runs.create(
      procedure_id="FVT1",
      run_passed=True,
      unit_under_test={
          "serial_number": "PCB1A001",
          "part_number": "PCB1"
      }
  )

  print(f"Run created: {result.url}")

if __name__ == '__main__':
  main()

Custom CA certificates

If your instance uses a self-signed or internal CA certificate, you need to tell the Python client to trust it.

Export the certificate from your server:

echo | openssl s_client -connect tofupilot.yourcompany.com:443 -showcerts 2>/dev/null | openssl x509 -outform PEM > ca-certificate.pem

Then pass it to the client:

from tofupilot import TofuPilotClient

client = TofuPilotClient(
  url="https://tofupilot.yourcompany.com",
  verify="/path/to/ca-certificate.pem"
)

# For OpenHTF
from tofupilot.openhtf import upload
test.add_output_callbacks(
  upload(
      url="https://tofupilot.yourcompany.com",
      verify="/path/to/ca-certificate.pem"
  )
)
from tofupilot.v2 import TofuPilot
import httpx

http_client = httpx.Client(
  verify="/path/to/ca-certificate.pem"
)

client = TofuPilot(
  api_key="your_api_key",
  server_url="https://tofupilot.yourcompany.com/api",
  client=http_client
)

License

Your instance pulls its license from Orbit on first boot. It controls which plan you are on, your usage limits, and which features are available.

View your license

Open Settings > Subscription in your dashboard to see your plan, license status, and current usage.

Refresh your license

If you upgrade your plan or renew, click the refresh button on the license card to sync the latest license from Orbit.

License expiry

Licenses are valid for a set period. As a license approaches or passes its expiry date, your instance gradually restricts write operations:

StateWhat changes
ActiveNormal operation
Expiring SoonA warning banner appears, nothing is restricted yet
Write RestrictedCreating new users and stations is blocked, existing data and runs are unaffected
ExpiredA renewal prompt appears on every page, contact sales to renew

Air-gapped activation

No internet on your server? You can paste the license manually:

  1. In TofuPilot Orbit, open the instance menu and select Copy license token
  2. In your dashboard, go to Settings > Subscription
  3. Click the upload button on the license card
  4. Paste the token and click Activate

Manage from Orbit

TofuPilot Orbit gives you an overview of all your self-hosted instances. You can see the running version, the domain, and when each instance last checked in.

From the instance menu you can open the dashboard, re-deploy, trigger an update, copy the license token for air-gapped activation, or delete the instance.

Operations

Update

To update your instance to the latest version, re-run the deploy script:

curl -sSL https://tofupilot.sh/deploy | bash

To update to a specific version:

curl -sSL https://tofupilot.sh/deploy | bash -s -- --version 2.8.0

Downgrades are not supported to protect your data. Orbit shows the new version once the update finishes.

Reconfigure

To change your configuration after deploying (e.g. add an OAuth provider or update SMTP settings), edit your .env file and re-run the deploy command:

curl -fsSL https://tofupilot.sh/deploy | bash

Your data and auto-generated secrets are preserved.

Manage your instance

Once deployed, you can manage your instance from the server with Docker Compose:

docker compose ps          # Check service status
docker compose logs -f     # Follow live logs
docker compose restart     # Restart all services
docker compose down        # Stop all services
docker compose up -d       # Start all services

If something is not working as expected, check the application logs first:

docker compose logs tofupilot-dashboard --tail 100

Uninstall

To completely remove TofuPilot from your server, run:

curl -sSL https://tofupilot.sh/deploy | bash -s -- --uninstall

This deletes all containers, volumes, configuration, and data. To also revoke the license, delete the instance from TofuPilot Orbit.

Backups

Your data lives in two Docker volumes. Back them up on a regular schedule to avoid data loss:

  • tofupilot-pg-data for the PostgreSQL database
  • tofupilot-seaweed-data for uploaded files and attachments

How is this guide?