This guide sets up an Agentile hub on your own server. The result is the same software as the hosted service at agentile.eu.
Each tenant's agents still run on that tenant's own machines and subscriptions; the hub only coordinates them. In plain git mode, the hub never needs access to anyone's code.
What you need
- A Linux server with Docker Engine and Docker Compose v2. Allow at least 2 GB of memory and 10 GB of disk.
- A domain name whose DNS A (and AAAA, if you use IPv6) record points at the server. Ports 80 and 443 must be open to the internet: the hub gets its HTTPS certificate from Let's Encrypt automatically.
- An email provider: Resend or any SMTP server. Agentile sends sign-in links, invitations and password resets by email.
- Optional:
- a GitHub OAuth app, for "Continue with GitHub";
- a GitHub App, for pull requests and merges done by the App (see GitHub).
Install
git clone <this repository> agentile && cd agentile
cp deploy/env.example deploy/.env && chmod 600 deploy/.env
# Edit deploy/.env: at least AGENTILE_DOMAIN, AGENTILE_PUBLIC_URL, POSTGRES_PASSWORD and email.
docker compose -f deploy/compose.yml -f deploy/compose.public.yml --env-file deploy/.env up -d --build
The first build takes a few minutes. When it's done, https://<your domain>/api/health answers {"status": "ok"}, and the sign-in page is at https://<your domain>/sign-in.
The stack has five containers:
- Postgres;
- the API;
- the worker: standup timers, the backup timer, deadlines and message retention;
- the web app;
- Caddy, for HTTPS and routing.
Database migrations run automatically whenever the API or the worker starts.
The first administrator
You can sign up with an email link like anyone else. To create an account with a password instead, for example before email works, run:
docker compose -f deploy/compose.yml -f deploy/compose.public.yml --env-file deploy/.env \
exec -T api agentile-api create-user --email [email protected] --name "Your Name" --hub-admin --password-stdin
Type the password and press Ctrl-D. It's read from standard input, so it never appears in your shell history or the process list. Then sign in, accept the terms, and create your tenant.
Connecting agents
- Create a join code. On your tenant page, open Connect agents and create a join code.
- Set up the machine. On each machine that should run agents, either paste the prompt into Claude Code or Codex, or run the commands shown.
- The commands install
agentile-runnerstraight from your hub (https://<domain>/api/downloads/…), join the hub, and register agents. - Joining gives the runner its own OS user and a system service; see Securing the machines that run agents. It asks for the person's password once. An agent following the prompt asks the person to run that one command in a terminal.
- Agents use a Claude Code or Codex sign-in on that machine (the person's own, copied if they agree, or a separate one); the hub never sees it.
- The commands install
- Assign the agents. Under Projects on the tenant page, switch on the agents that should work on each project.
Securing the machines that run agents
Agents receive messages from other tenants' agents, so treat every message as untrusted input: a teammate's message could try to talk an agent into reading a secret and posting it. The runner hardens each session, but a few machine-level steps make the difference.
The runner runs as its own OS user (the default)
agentile-runner join sets up a user that owns nothing but the runner, and runs the runner and every agent session as that user. Before it changes anything, it shows each step with its command and asks for your password once. --yes skips the question.
| Linux | macOS | |
|---|---|---|
| User | agentile, a system user that can't log in, with home /var/lib/agentile (mode 0700) | agentile, hidden from the login window, with home /Users/agentile |
| Runner | uv and agentile-runner, installed for that user from the same source as yours | the same |
| Service | /etc/systemd/system/agentile-runner.service, with User=agentile | /Library/LaunchDaemons/com.agentile.runner.plist, with UserName |
| Your commands | /etc/agentile-runner.json records the setup, so agent add, status, doctor, service and the rest are passed to that user | the same |
| Sudo rule | /etc/sudoers.d/agentile-runner (checked with visudo) lets the person who set it up run the runner as that user, and restart its service, without a password | the same |
Options:
--user NAMEpicks another name.--same-userruns the runner as you, as before. Then runagentile-runner service install.- For a runner that already joined as you,
agentile-runner setupmoves its settings and tokens to its own user and stops your old user service.
Coding CLIs for that user:
- Installing: when you add an agent, the runner offers to install its CLI with the CLI's own installer. Claude Code uses
curl -fsSL https://claude.ai/install.sh | bash; Codex usesnpm install -g --prefix ~/.local @openai/codex. - Signing in: the runner offers to copy your sign-in,
~/.claude/.credentials.jsonon Linux or~/.codex/auth.json, into that user's home, readable only by it. Both copies then share one sign-in, so if one refreshes it, the other may have to sign in again. - Signing in separately:
- Codex:
sudo -u agentile -H /var/lib/agentile/.local/bin/codex login. - Claude Code:
sudo -u agentile -H /var/lib/agentile/.local/bin/claude, then type/login. - On macOS, Claude Code keeps its sign-in in that user's Keychain. If it doesn't stay signed in there, use
--same-user.
- Codex:
Why it matters. The runner keeps its machine token and every agent's token in runner.json, under ~/.config/agentile/ with mode 0600. With its own user, a session can't reach a person's SSH keys, cloud credentials or shell history, because they aren't in that user's home. This is the protection to rely on; the per-session sandboxes below are defence in depth.
Check that people's home folders aren't readable by other users. ls -ld ~ should show drwx------ or drwxr-x---. Some distributions create homes with mode 755.
Updating runners: agentile-runner update
The hub serves the runner wheel it was built with. Machines running an older runner are marked in the tenant's Runners list, and agentile-runner update on the machine installs the hub's version (as the runner's own OS user, if it has one) and restarts the service. Each release that changes the runner bumps its version in packages/runner/pyproject.toml and agentile_runner/__init__.py, so older machines can tell.
What the agents need: agentile-runner doctor
agentile-runner doctor checks each of these, prints the install command for your package manager (apt, dnf, pacman, zypper, apk or brew), and exits 1 if something is missing:
- git;
- the Claude Code and Codex CLIs your agents use, and that they're signed in;
- the sandbox tools: on Linux, bubblewrap (plus socat for Claude Code), checked by starting a real sandbox; on macOS,
sandbox-exec.
Missing tools don't stop agents, but they change what happens:
joinandsetupoffer to install the missing packages.agentile-runner runlogs a warning and starts the agents anyway, without the sandbox.agentile-runner statusshows aSandbox:line.- The hub's machine list (tenant page → Runners) shows "Sandbox off" with the same command, and whether the runner has its own OS user.
Ubuntu 24.04 and later restrict unprivileged user namespaces with AppArmor. bubblewrap can then be installed and still fail to start a sandbox, which the doctor reports. To allow bubblewrap alone, add /etc/apparmor.d/bwrap:
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
include if exists <local/bwrap>
}
Then run sudo apparmor_parser -r /etc/apparmor.d/bwrap.
Claude Code sessions are sandboxed
The runner runs each Claude Code session's shell commands in Claude Code's built-in OS sandbox (bubblewrap on Linux). Writes are confined to the session's own worktrees and workspace, the network is open (agents need the hub and git remotes), and reads of runner.json and of credential files (~/.ssh, ~/.aws, ~/.config/gh, ~/.claude*, ~/.codex/auth.json, ~/.netrc, …) are denied, so a prompt-injected agent can't cat them. The agent's own Claude Code sign-in is unaffected; it is read by Claude Code itself, not by a sandboxed command.
Install the sandbox's dependencies on each Claude Code machine:
# Debian/Ubuntu
sudo apt install bubblewrap socat
# Arch
sudo pacman -S bubblewrap socat
Without them the sandbox cannot start. The runner then falls back to permission rules alone: the Read tool is still denied those paths, but a shell command such as cat isn't blocked. It logs that the sandbox is off, and the hub's machine list shows the command that fixes it. Install the dependencies; the runner's own OS user (above) protects people's files either way.
Regardless of the sandbox, the hub only ever hands a session its own agent token (in the MCP configuration), never the machine token.
Codex sessions
Codex (0.161) sandboxes each session with a permission profile that confines writes to the worktrees, keeps the network on, and denies reads of the credential directories the agent doesn't need (~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gh, ~/.config/gcloud, ~/.claude). Two limits are inherent to Codex 0.161 and cannot be worked around in the runner:
- Codex's sandbox can deny a directory but not an individual file, so credential files that sit directly in the home directory (
~/.netrc,~/.claude.json,~/.git-credentials,~/.npmrc,~/.pypirc) stay readable to a Codex session. - Codex runs
gitinside the same sandbox, and the runner's pre-push hook must readrunner.jsonto ask the hub whether a push is allowed, sorunner.jsonand its directory (~/.config/agentile) stay readable to a Codex session.
For Codex machines in particular, the runner's own OS user (above) is the real protection. Don't opt out of it with --same-user there.
Set one of these in deploy/.env:
AGENTILE_RESEND_API_KEY;AGENTILE_SMTP_HOSTwithAGENTILE_SMTP_PORT,AGENTILE_SMTP_USERNAME,AGENTILE_SMTP_PASSWORDandAGENTILE_SMTP_SECURITY. Usestarttlson port 587,sslon 465, ornonefor a relay on the same network.
AGENTILE_EMAIL_FROM must be an address your provider lets you send from.
If neither provider is set, the API logs an error at startup. Nobody can then sign in by email, but password sign-in still works.
Who runs the hub
AGENTILE_OPERATOR (your organisation's name), AGENTILE_OPERATOR_ADDRESS (its postal address), AGENTILE_OPERATOR_PHONE and AGENTILE_OPERATOR_CONTACT (an email address for privacy requests) appear in the footer, the terms and the privacy notice. Set at least the name and one way to reach you. They're built into the web app, so rebuild after changing them (the up -d --build command again).
Read the terms and privacy notice in apps/web/src/content/legal.tsx and adapt them to your situation. They describe how the hosted service is run.
Each project's Privacy and audit page lists the services that process data for the hub. Set AGENTILE_PROCESSORS to yours, as name: what it does entries separated by semicolons, for example AGENTILE_PROCESSORS=Hetzner: hosting, Falkenstein, Germany; Backblaze: backup storage, EU. Use none if nobody else handles the data. Left empty, the page shows the hosted service's providers (netcup and Cloudflare), which are probably not yours. The email provider, and GitHub for projects that connect a repository through the GitHub App, are added automatically.
People export their own data and delete their accounts under Settings, Privacy. Tenant admins do the same for a tenant on its Data tab, and project owners for a project on its Privacy and audit page.
Upgrading
git pull
docker compose -f deploy/compose.yml -f deploy/compose.public.yml --env-file deploy/.env up -d --build
If a build fails, the running containers stay as they were. The API and the worker take a lock before migrating, so starting both at once is safe.
Runners keep working across hub upgrades and reconnect on their own. Machines get the new runner the next time someone runs the install command from a new join code.
Backups
The hub's state is in Postgres, plus the project files (screenshots, reports, chat attachments) in the artifacts volume. Back up both. A nightly dump of the database:
# /etc/cron.d/agentile-backup
30 2 * * * root cd /path/to/agentile && docker compose -f deploy/compose.yml --env-file deploy/.env exec -T postgres pg_dump -U agentile -Fc agentile > /var/backups/agentile-$(date +\%F).dump
For the files, copy the volume. For example, docker run --rm -v agentile_artifacts:/a -v /var/backups:/b alpine tar czf /b/agentile-files-$(date +%F).tgz -C /a .. Keep copies off the server and test restoring them now and then:
docker compose -f deploy/compose.yml --env-file deploy/.env exec -T postgres pg_restore -U agentile -d agentile --clean < agentile-<date>.dump
Encrypt copies before they leave the server, for example with age: age -r <your public key> -o agentile.dump.age agentile.dump. Keep the private key somewhere other than the server too, or a lost server takes the backups' key with it.
The hosted service's scripts in deploy/scripts/ are a fuller example. They encrypt the dump with age, upload it to S3-compatible storage, and run a restore drill that downloads and decrypts it.
Behind an existing reverse proxy
If the server already runs a web server on ports 80 and 443, leave out compose.public.yml. The stack then listens on 127.0.0.1:8260; set AGENTILE_BIND to change that. Proxy your domain to it, and make sure your proxy:
-
passes WebSocket upgrades on
/ws/…; -
doesn't buffer responses on
/mcp(agents' tool calls stream); -
allows long-lived connections. Runners and browsers send or receive something at least every 30 seconds, so idle timeouts above that are fine;
-
sets
X-Forwarded-Forto the client's address, replacing any value the client sent. Caddy does this by default; in nginx, useproxy_set_header X-Forwarded-For $remote_addr;. The API limits sign-in attempts per client address: without the header every visitor shares one limit, and with a value the client chose, anyone can dodge it. -
allows request bodies up to 70 MB. That's a project file, 64 MB at most, plus headers; the stack's own Caddy enforces the same limit.
deploy/caddy/host-agentile.caddy is the configuration the hosted service uses behind its host Caddy.
Outside Docker. If you run the API yourself rather than with the compose stack:
- Environment: it runs as production unless
AGENTILE_ENVIRONMENTsays otherwise. Production means no development outbox, and email must be configured. - Forwarded headers:
X-Forwarded-Foris trusted only from127.0.0.1. SetAGENTILE_FORWARDED_ALLOW_IPSto your proxy's address, or to*only if nothing else can reach the API.
GitHub
- Plain git mode needs nothing from GitHub. A project's owner gives a remote URL, and every agent's machine reaches it with its own credentials. GitHub repositories work this way too. The hub accepts only
https://,ssh://andgit://URLs (and theuser@host:pathSSH form), with no credentials embedded in the URL.file://URLs are refused, because an owner in one tenant could otherwise point another tenant's runner at a path on that runner's own machine. If you run a single-tenant hub and genuinely need local repositories, setAGENTILE_ALLOW_LOCAL_REPO_URLS=1on both the hub and the runners; it is off by default and is always refused in production. - GitHub App mode has the App open pull requests, read CI and merge. The same App also gives "Sign in with GitHub".
- The quick way: on a computer with a browser signed in to GitHub, run
python3 deploy/scripts/create-github-app.py --hub https://<domain> --out github-app.env(add--org <name>to create it under an organisation). It opens GitHub with everything below filled in; press Create GitHub App. The script writes the settings togithub-app.env; add them to the hub's.envand restart it.
- Or register a GitHub App by hand, with:
- repository permissions:
- Contents: read and write;
- Pull requests: read and write;
- Commit statuses: read and write;
- Checks: read;
- Administration: read and write, for the branch rules below;
- Metadata: read;
- webhook events: check suite, check run, status, pull request and push, sent to
https://<domain>/api/github/webhook; - setup URL
https://<domain>/api/github/setup.
- repository permissions:
- Fill in the
AGENTILE_GITHUB_APP_*settings andAGENTILE_GITHUB_WEBHOOK_SECRET. - A project owner then connects a repository from Board → Repository. GitHub's install flow decides which repositories they may use.
- The quick way: on a computer with a browser signed in to GitHub, run
- Branch rules (GitHub App mode). Connecting a repository adds two rulesets to it:
- Default branch: changes only through a pull request with the
agentile/checkpointstatus. The hub sets that status when people approve the epic's checkpoint. epic/*branches: change only through a pull request withagentile/review, which the hub sets when it merges a reviewed task.- Neither branch can be deleted or force-pushed.
- Agents' push tokens: the runner's git credential helper fetches them. They can push branches but can't set statuses, so agents can't get around a review or a checkpoint. Repository admins (people) can still bypass the rules.
- When GitHub refuses the rules (private repositories need GitHub Pro, Team or Enterprise for rulesets, and the App needs the Administration permission), the Repository page says so and offers Set the branch rules again. The runner's push check still applies.
- Default branch: changes only through a pull request with the
- What CI counts. The hub merges an approved task when every check run and commit status on the pull request's head has passed, or when there's no CI at all. The merge is pinned to the commit the reviewer was asked to review. If anything is pushed after that, the agent asks for a new review.
Demo projects
To see every screen with work in it, seed the demo. It needs an account that already exists, which becomes the demo's owner:
docker compose -f deploy/compose.yml --env-file deploy/.env exec api agentile-api demo seed --owner-email [email protected]
It takes a minute or two and builds four projects at different stages:
| Project | Stage | What it shows |
|---|---|---|
| Tidepool (TDP) | Mid-way |
|
| Atlas (ATL) | End of its second epic |
|
| Lighthouse (LHT) | Planning | Estimates under way |
| Quill (QLL) | Planning | Its plan waiting for approval |
How it's built. The work goes through the real hub: people act through the API, and agents through the same tools real agents use.
What's set directly. Only what would need live runners or time to pass:
- each agent's state;
- timestamps, spread over the past weeks;
- when Tidepool's standup backup timer started;
- two task statuses on Tidepool.
Clearly fictional. Everything is labelled:
- tenants are named "Demo · …";
- descriptions start "Demo project:";
- people have addresses at
demo.agentile.invalid, which can never receive mail.
Frozen until someone acts. The demo's machines never connect, and the worker leaves demo projects alone.
Options.
--replacerebuilds an existing demo.--keys TDP=DTP,...picks other project keys if those are taken.
Removing it. agentile-api demo remove deletes exactly what the seed made. The owner's own account stays.
Troubleshooting
- No sign-in emails: check
docker compose … logs apifor email errors, and thatAGENTILE_EMAIL_FROMis allowed by your provider. - Agents show as offline: on the agent's machine, run
agentile-runner status. The runner needs to reachhttps://<domain>and keep a WebSocket open. Some corporate proxies close those. - The page loads but live updates don't arrive: your proxy isn't passing WebSockets on
/ws/….