Skip to content

Deploy to a server

deploy/ runs the portal on one Linux server with K3s, a small Kubernetes distribution. The Helm chart in deploy/chart defines the stack:

Part Kind What it does
matefactory-db StatefulSet, postgres:17 The database, on a 10 GiB volume (K3s local-path storage)
matefactory-backend Deployment, deploy/backend.Dockerfile Runs alembic upgrade head, then the API as one uvicorn worker. One replica, replaced with Recreate, because the agent gateway keeps its connections in that process.
matefactory-web Deployment, deploy/web.Dockerfile Caddy with the built web app on port 8080 and the docs on port 8081
matefactory Ingress Sends DOMAIN/api to the backend, the rest of DOMAIN to the web app, and DOCS_DOMAIN to the docs. It redirects http:// to https://.
matefactory-letsencrypt cert-manager Issuer Gets one Let’s Encrypt certificate for both host names over HTTP-01

The production deployment is https://factory.tarx.app, on the Hetzner VM 46.224.173.231.

  1. The server needs Ubuntu (or another Linux that K3s supports), Docker to build the images, and SSH access as a user who can use sudo.

  2. Point DNS for DOMAIN and DOCS_DOMAIN at the server. Without a DNS record for the docs, use an sslip.io name such as docs.203.0.113.7.sslip.io.

  3. Open ports 80 and 443 in the server’s firewall. Let’s Encrypt checks the domains on port 80. Keep the K3s ports, such as 6443, closed to the internet.

  4. Register the GitHub apps with the URLs of the deployment, as in GitHub apps:

    • Callback URL (sign-in): https://DOMAIN/api/auth/callback
    • Setup URL (GitHub App install): https://DOMAIN/api/github/setup
  5. Create /etc/matefactory/matefactory.env on the server from deploy/env.example, owned by root with mode 600. It holds the settings from Configuration and these values:

    Variable Value
    DOMAIN The portal’s host name. The chart sets APP_BASE_URL to https://DOMAIN.
    DOCS_DOMAIN The docs site’s host name
    PUBLIC_IP The server’s public address. The ingress takes ports 80 and 443 on this address only, so services that listen on other addresses, such as Tailscale, keep theirs.
    POSTGRES_PASSWORD A random password. The chart builds DATABASE_URL from it.

    Write GITHUB_APP_PRIVATE_KEY on one line, with \n for each line break.

The Daytona API key is not a server setting. Each workspace connects Daytona in Settings → Integrations, and the portal stores the key encrypted with ENCRYPTION_KEY.

From a clean checkout, run:

Terminal window
deploy/deploy.sh user@host

The script uploads the committed HEAD to /opt/matefactory/src and runs deploy/server.sh there as root. server.sh:

  1. Installs K3s (without its ServiceLB), Helm and cert-manager on the first run, at the versions pinned in the script. It configures K3s’s Traefik to listen on PUBLIC_IP, and turns off Traefik’s 60-second read timeout so agent event streams, terminals and desktops stay open.
  2. Builds the backend and web images with Docker, tagged with the commit, and imports them into K3s. Nothing is pushed to a registry.
  3. Copies the env file into the Kubernetes Secret matefactory-env. The Secret is never in Git.
  4. Runs helm upgrade --install matefactory deploy/chart. A changed env file restarts the backend.

The script then waits for https://DOMAIN/api/health. The env file and the database volume stay as they are. Pass SSH options in DEPLOY_SSH_OPTS, for example DEPLOY_SSH_OPTS="-i ~/.ssh/deploy_key -o IdentitiesOnly=yes".

Run these on the server:

Terminal window
alias k='sudo k3s kubectl -n matefactory'
k get pods,ingress,certificate
k logs -f deploy/matefactory-backend
k rollout restart deploy/matefactory-backend
k exec matefactory-db-0 -- pg_dump -U factory -Fc factory > factory.dump
k exec -i matefactory-db-0 -- pg_restore -U factory -d factory --clean --if-exists --no-owner < factory.dump

After you edit the env file, run deploy/deploy.sh again: it updates the Secret and restarts the backend.