Self-hosting

Deploy Gitterm on your own infrastructure with Docker Compose or Railway, choose a routing mode, enable a provider, and check that each step worked.

View as markdown

This page is for whoever deploys and runs a Gitterm instance, person or agent. It lists what you run and what you must decide first, then gives numbered deploy steps with a check after each one. Every environment variable, with whether it is required, is on Configuration. How credentials are stored and how to rotate secrets is on Security.

A self-hosted deployment has no plans, quotas or billing. Sign-in is always email and password. It serves only the app: your URL opens the dashboard (or sign-in), there is no landing page, pricing or legal pages, and docs links go to gitterm.dev.

What you run

ServiceRequiredWhat it doesHealth check
PostgreSQLYesDatabasepg_isready
RedisYesCache and pub/subredis-cli ping returns PONG
serverYesMain API. Applies migrations, seeds the catalogs and creates the admin account on every startGET /api/health returns {"status":"healthy"}
webYesDashboard and sign-inGET /login shows the sign-in form
proxyYesCaddy. Serves everything from one origin: /api to server, /listener to listener, workspaces, the rest to webGET /health returns {"status":"UP","mode":"railway"}
listenerYesProvider and GitHub webhooks, and live workspace status for the dashboardGET /listener/health (through the proxy) returns OK
workerYesPauses idle workspacesLogs [idle-reaper] Pass completed

The proxy serves plain HTTP. Put TLS in front of it.

Before you start

You needRequiredWhy
A public HTTPS URL for GittermYes, to run workspacesWorkspaces run on the provider's machines and call back to <PUBLIC_URL>/api/trpc to report setup and to refresh GitHub and model credentials. Provider webhooks reach the listener the same way. The CLI, SDK and OpenCode plugins only accept https:// (or http:// on loopback)
An account with at least one providerYes, to run workspacesGitterm doesn't run workspaces itself. See Enable a provider
A wildcard DNS record and certificateOnly if you use subdomain routingSee Routing
An SMTP server or a Resend keyNoWithout one, emails are written to the server log instead of sent
A GitHub App or a fine-grained GitHub tokenNoOnly for private repositories. Configure it after deploy under Admin → Integrations

On localhost you can sign in and configure everything, but a cloud provider can't reach your machine, so workspaces won't finish setup.

Routing

Choose the routing mode before anyone creates a workspace. Each workspace's address is saved when it is created.

ModeWorkspace and port URLsYou needWhat doesn't work
path (default)<PUBLIC_URL>/ws/<workspace>/, <PUBLIC_URL>/ws/<port>-<workspace>/One DNS record and one certificateT3 Code workspaces (creating one is rejected). OpenCode desktop pairing links and the signed-in browser link (clients use the server password instead). Some preview apps' asset paths
subdomain<port>-<workspace>.<PUBLIC_HOST>A wildcard DNS record *.<PUBLIC_HOST> and a wildcard certificate on whatever terminates TLSAll of these work

Use subdomain if you control DNS for the domain. For a wildcard certificate, use a reverse proxy that supports the ACME DNS-01 challenge, or Cloudflare. With subdomain, PUBLIC_HOST is a bare host name with no port.

ROUTING_MODE has to be the same on server, proxy and web (NEXT_PUBLIC_ROUTING_MODE). Docker Compose sets all three from one value.

Deploy with Docker Compose

docker-compose.selfhost.yml runs all seven services from the published images. You need Docker with Compose v2, git and openssl on the host.

  1. Get the Compose file and the example settings:

    Shell
    git clone https://github.com/OpeOginni/gitterm.git
    cd gitterm
    cp .env.selfhost.example .env.selfhost
    chmod 600 .env.selfhost

    You should see .env.selfhost in the directory, readable only by you.

  2. Generate the secrets. Run this and copy each line into .env.selfhost, replacing the empty value with the same name:

    Shell
    for name in POSTGRES_PASSWORD BETTER_AUTH_SECRET WORKSPACE_JWT_SECRET INTERNAL_API_KEY ENCRYPTION_MASTER_KEY; do
      echo "$name=$(openssl rand -hex 32)"
    done

    Each value is 64 hexadecimal characters. Use hex for POSTGRES_PASSWORD too: Compose puts it into the database URL without escaping. Back up ENCRYPTION_MASTER_KEY somewhere separate from the database backups. If you lose it, the saved credentials can't be decrypted.

  3. Set the address, routing mode and admin account in .env.selfhost:

    INI
    PUBLIC_URL=https://gitterm.example.com
    PUBLIC_HOST=gitterm.example.com
    ROUTING_MODE=subdomain
    ADMIN_EMAIL[email protected]
    ADMIN_PASSWORD=<at least 8 characters>
    # Fresh install only: store new secrets in the hardened format from the start
    ENCRYPTION_WRITE_VERSION=v3
    AUTH_ENCRYPT_OAUTH_TOKENS=true

    <angle brackets> mark values you replace. PUBLIC_URL has no trailing slash. To try Gitterm on one machine, keep the defaults PUBLIC_URL=http://localhost:8888, PUBLIC_HOST=localhost:8888 and ROUTING_MODE=path. Leave the GitHub, email and startup settings empty unless you need them; see Configuration.

  4. Check that every required value is set:

    Shell
    docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml config --quiet

    You should see no output. An error such as required variable POSTGRES_PASSWORD is missing a value names the setting to fill in.

  5. Start the stack:

    Shell
    docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml up -d

    The first start can take a few minutes. If an image can't be pulled, Compose builds it from the checkout. Then check the services:

    Shell
    docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml ps

    You should see all seven services running, and postgres, redis and server marked healthy.

  6. Check that the server prepared the database and created the admin:

    Shell
    docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml logs server | grep -E 'entrypoint|seed-admin'

    You should see [entrypoint] Running migrations..., Running seed..., Running admin seed..., [seed-admin] Created admin user: <your email> (or Admin user already exists on later starts) and [entrypoint] Starting server....

  7. Check the routes through the proxy, from the host:

    Shell
    curl -s http://localhost:8888/health
    curl -s http://localhost:8888/api/health
    curl -s http://localhost:8888/listener/health

    You should see {"status":"UP","mode":"railway"}, {"status":"healthy"} and OK. Replace 8888 if you changed GITTERM_PORT.

  8. Put TLS in front of the proxy for any address other than localhost. Forward PUBLIC_HOST (and *.PUBLIC_HOST for subdomain routing) to port GITTERM_PORT on the host. You should see the same three responses when you run the curl commands against PUBLIC_URL.

Continue with First sign-in.

Upgrade

Back up the database first. Then pull the new images and recreate the services:

Shell
docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml pull
docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml up -d

The server applies new migrations as it starts. You should see the [entrypoint] lines from step 6 again. To run a specific release, set GITTERM_IMAGE_TAG=v<version>. To run migrations yourself instead, see Startup steps.

Deploy on Railway

The Railway template deploys the same services. Its configuration lives on Railway, not in this repository, so check the variables against Configuration after it deploys.

  1. Open the Gitterm Railway template and deploy it to your Railway account.
  2. Fill in the variables Railway asks for, including ADMIN_EMAIL and ADMIN_PASSWORD. Generate each secret with openssl rand -hex 32. You should see every service build and turn healthy in the Railway project.
  3. Check the server variables. WORKSPACE_API_URL must be <your URL>/api/trpc and DEVICE_CODE_VERIFICATION_URI must be <your URL>/device. When unset they point at gitterm.dev, which isn't your deployment.
  4. For subdomain routing, add a wildcard custom domain such as *.example.com to the proxy service, and set ROUTING_MODE=subdomain on server and proxy and NEXT_PUBLIC_ROUTING_MODE=subdomain on web. Redeploy web after changing any NEXT_PUBLIC_* value. You should see the proxy domain verified in Railway.
  5. Check that the worker runs as a Railway Cron job, for example on */10 * * * *, with REAP_INTERVAL_MINUTES unset or 0 so each run does one pass and exits. You should see [idle-reaper] Mode: run-once in its logs.
  6. Open https://<your domain>/health, /api/health and /listener/health. You should see the same responses as in step 7 of the Compose deploy.

First sign-in

  1. Open <PUBLIC_URL>/login and sign in with ADMIN_EMAIL and ADMIN_PASSWORD.

    You should see the dashboard with an Admin group in the sidebar. If sign-in fails, check the step 6 log lines. The admin seed only creates the account: if the email already exists, it makes it an admin and leaves the password unchanged.

  2. Enable a provider. Required before anyone can create a workspace.

  3. Set up integrations if you need them. Optional.

  4. Create a test workspace from the dashboard on the provider you enabled.

    You should see it reach running and open in the browser. If it stays in setup, or reports No setup status report was received, the provider's machine can't reach <PUBLIC_URL>/api/trpc.

Enable a provider

Every provider except AWS is listed under Admin → Cloud providers after the first start, disabled.

  1. Open Admin → Cloud providers and select the provider.
  2. Fill in Credentials and config using the provider's setup guide in the table below.
  3. Choose Save settings.
  4. Turn on the Enabled switch. It stays off until the settings are saved.
  5. If the table says the provider uses a webhook, add it in the provider's dashboard. The provider page shows the exact URL and events under Status webhook.

You should see the provider listed as Configured, without a Disabled label.

For AWS, use Add a region in the AWS section of Admin → Cloud providers. Each region is its own provider.

ProviderWebhookSetup guide
E2BRequiredE2B
RailwayRequiredRailway
DaytonaOptionalDaytona
BoatOptionalBoat
Vercel SandboxNoVercel
exe.devNoexe.dev
CloudflareNoCloudflare
AWSNoAWS

Webhooks go to the listener through the proxy: <PUBLIC_URL>/listener/trpc/<provider>.handleWebhook.

What each provider offers to workspaces is on Workspaces. Enabled providers also back managed sandboxes for local agents; see Sandbox providers.

Optional admin settings

None of these are needed for a working install.

PageWhat to set
Admin → IntegrationsGitHub: one mode for private repositories, a GitHub App or a shared PAT. The App form shows the Setup URL and Webhook URL to copy into your GitHub App. Google Cloud, MCP servers, Executor: turn on what your users need. See Connections
Admin → SystemIdle timeout before a workspace pauses, 5 to 120 minutes. Default 30
Admin → Agent typesWhich agents users can pick
Admin → ImagesWhich images users can pick
Admin → UsersManage accounts

GitLab and Bitbucket appear under Admin → Integrations but aren't implemented yet.

Point clients at your deployment

The CLI, SDK, OpenCode plugins and bots use the API URL, which is <PUBLIC_URL>/api behind the bundled proxy:

Shell
gitterm login --server https://gitterm.example.com/api
gitterm auth status

You should see your account and server in the auth status output. See the CLI reference.

Verify the install

Run through this list once after deploying. Each item names the step that covers it.

  1. <PUBLIC_URL>/health, /api/health and /listener/health respond (Compose step 7, Railway step 6).
  2. The server log shows migrations, seed and admin seed finishing (Compose step 6).
  3. You can sign in as the admin and see the Admin group (First sign-in).
  4. At least one provider is Configured and enabled, with its webhook added if required (Enable a provider).
  5. A test workspace reaches running and opens in the browser.
  6. The worker log shows [idle-reaper] Pass completed at least once.
  7. ENCRYPTION_MASTER_KEY is backed up separately from the database (Security).