Setup Guide
One command provisions a fresh server. Everything runs through Ansible — the manual steps in this guide document what Ansible automates, not an alternative path.
What You End Up With
Section titled “What You End Up With”- A dedicated server running the Yak Docker container (Laravel app, queue workers, scheduler, nginx)
- A MariaDB container with persistent storage for the application database
- Incus + ZFS for sandboxed task execution — each task runs in its own isolated system container with its own Docker daemon, network, and filesystem
- Webhook endpoints for whichever channels you have enabled
- A dashboard at
https://{your-domain}behind Google OAuth - Claude Code CLI configured with MCP servers matching your enabled channels
Prerequisites
Section titled “Prerequisites”| Requirement | Notes |
|---|---|
| Server | Dedicated box with 32GB+ RAM, 500GB+ disk. Hetzner AX-series, bare metal, or VM. Ubuntu 24.04 or Debian 12+. Public IP for inbound webhooks. |
| Domain | DNS A record pointing to the server. Used for dashboard and webhook endpoints. |
| Claude | Max subscription (for Claude Code CLI) plus an Anthropic API key (for the routing layer). |
| GitHub | Organization account. The Ansible provisioner creates a GitHub App automatically — repos are cloned via HTTPS using the App’s installation token (no SSH keys needed). |
| Google OAuth | Google Cloud project with OAuth credentials. Used for dashboard authentication. |
| Ansible | 2.15+ on your local machine (pip install ansible). |
Optional Per Channel
Section titled “Optional Per Channel”Only configure the channels you use. Everything except GitHub (for pushing branches and opening PRs) is optional.
| Channel | What you need |
|---|---|
| Slack | Slack app with bot token plus signing secret. |
| Linear | OAuth application (client id + secret + webhook signing secret). Authorize at /settings/linear. Requires workspace admin approval. |
| Sentry | Auth token plus webhook secret. Alert rules tagged yak-eligible. |
| Drone CI | API token. Yak polls the Drone API — no webhook needed. |
| GitHub Actions | Included with the GitHub App — no additional setup. |
See the Channels page for the full configuration of each channel.
Quick Start
Section titled “Quick Start”You run steps 1–5 on your own machine (laptop or workstation). Ansible reads the inventory file, connects to your target server over SSH, and provisions everything remotely. You only SSH into the server itself for step 6 (the one-time Claude Code login). Step 7 happens in your browser.
1. Install Ansible (on your local machine)
Section titled “1. Install Ansible (on your local machine)”Ansible 2.15 or newer is required. If you don’t already have it:
pip install ansible # or: brew install ansibleansible --version # confirm 2.15+You also need SSH access to the target server as root (or another user with passwordless sudo) before continuing.
2. Clone Yak (on your local machine)
Section titled “2. Clone Yak (on your local machine)”git clone https://github.com/geocodio/yak.gitcd yak3. Configure Secrets (on your local machine)
Section titled “3. Configure Secrets (on your local machine)”cp ansible/vault/secrets.example.yml ansible/vault/secrets.ymlansible-vault encrypt ansible/vault/secrets.ymlansible-vault edit ansible/vault/secrets.ymlOptionally, save your vault password to a file so you don’t have to type --ask-vault-pass on every run:
echo 'your-vault-password' > ansible/vault/.vault_passThis file is gitignored and referenced automatically by ansible.cfg.
Channels you are not using can be left blank — Ansible skips disabled channels automatically.
# === Required ===yak_domain: yak.yourcompany.comanthropic_api_key: sk-ant-...github_org: your-org
# Dashboard authgoogle_oauth_client_id: "..."google_oauth_client_secret: "..."google_oauth_allowed_domains: "yourcompany.com" # required, comma-separated
# === Auto-generated (leave blank) ===yak_app_key: ""
# Database (auto-provisioned MariaDB container)mariadb_root_password: ""mariadb_password: ""
# GitHub App (filled after guided setup on first run, then re-run)github_app_id: ""github_app_private_key: ""github_installation_id: ""github_webhook_secret: ""
# === Channels (leave blank to disable) ===
slack_bot_token: ""slack_signing_secret: ""slack_workspace_url: "" # e.g. https://acme.slack.com — for thread deep links
linear_oauth_client_id: ""linear_oauth_client_secret: ""linear_oauth_redirect_uri: "" # defaults to https://{yak_domain}/auth/linear/callbacklinear_webhook_secret: ""
sentry_auth_token: ""sentry_webhook_secret: ""sentry_org_slug: ""
drone_url: ""drone_token: ""
# === Extra Agent Environment Variables ===# agent_extra_env:# NODE_AUTH_TOKEN: "ghp_..."# NPM_TOKEN: "..."Agent Environment Variables
Section titled “Agent Environment Variables”Repos that need tokens at build time (e.g. private npm registries) can have those tokens forwarded to the agent process. Add them to agent_extra_env in your vault:
agent_extra_env: NODE_AUTH_TOKEN: "ghp_..."This does two things automatically:
- Sets
NODE_AUTH_TOKEN=ghp_...as a container env var (available tonpm install) - Sets
YAK_AGENT_PASSTHROUGH_ENV=NODE_AUTH_TOKENso the sandboxed agent process receives it
Only vars listed here are forwarded — app secrets like DB_PASSWORD and APP_KEY are never exposed to the agent.
Private Docker Registries
Section titled “Private Docker Registries”Repos that pull private Docker images (e.g. bases shared across services, internal tooling) can authenticate from inside every sandbox without rebuilding locally. Add credentials to docker_registries in your vault:
docker_registries: ghcr.io: username: "your-github-username" password: "ghp_..." # PAT with `read:packages` scope registry.example.com: username: "deploy" password: "..."Ansible renders these into ~/.docker/config.json on the host, bind-mounts them into the Yak container, and the sandbox manager pushes the file to /home/yak/.docker/config.json in each new sandbox. docker pull and docker-compose up pick it up automatically.
The google_oauth_allowed_domains field is required. Login is rejected for any email whose domain is not in the list.
Where to get credentials
Section titled “Where to get credentials”Anthropic API key
Section titled “Anthropic API key”- Go to console.anthropic.com/settings/keys
- Click Create Key
- Copy the key (
sk-ant-...) intoanthropic_api_key
This key is for the routing layer (Haiku/Sonnet API calls), not the CLI. The CLI authenticates separately via a Max subscription — see step 5 below.
Google OAuth (required — dashboard authentication)
Section titled “Google OAuth (required — dashboard authentication)”- Go to console.cloud.google.com and create a new project (or select an existing one)
- Go to APIs & Services → OAuth consent screen
- Set user type to Internal (restricts login to your Google Workspace org — no app review needed)
- Fill in the app name (e.g. “Yak”) and your support email, then save
- Go to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Application type: Web application
- Add an authorized redirect URI:
https://{your-domain}/auth/google/callback - Copy the Client ID into
google_oauth_client_id - Copy the Client Secret into
google_oauth_client_secret - Set
google_oauth_allowed_domainsto your domain (e.g.yourcompany.com)
GitHub
Section titled “GitHub”No manual setup needed before provisioning. Leave the github_app_id fields blank and set github_org to your GitHub organization name. On first run, the playbook prints step-by-step instructions to create the GitHub App via the manifest flow — you fill in the resulting credentials and re-run.
Slack (optional)
Section titled “Slack (optional)”- Go to api.slack.com/apps and click Create New App → From scratch
- Name it (e.g. “Yak”) and select your workspace
- Go to OAuth & Permissions and add these bot token scopes:
chat:writeapp_mentions:readchannels:historyreactions:write— lets Yak react 👀 / 🚧 / ✅ / ❌ on your mention for glanceable status
- Click Install to Workspace and authorize
- Under Basic Information → Display Information, upload
public/slack-icon.pngas the app icon, set the short description to “AI coding agent — mention me with a task, get a pull request”, and the background color to#3d4f5f(Yak slate — dark enough for Slack’s white wordmark) - Copy the Bot User OAuth Token (
xoxb-...) intoslack_bot_token - Go to Basic Information and copy the Signing Secret into
slack_signing_secret - Go to App Home, enable the Home Tab — this powers the welcome DM Yak sends the first time a user opens Yak in the sidebar
- Go to Interactivity & Shortcuts, enable interactivity, and set the request URL to
https://{your-domain}/webhooks/slack/interactive— this powers the click-to-answer buttons on clarification messages - Go to Event Subscriptions, enable events, and set the request URL to
https://{your-domain}/webhooks/slack - Subscribe to bot events:
app_mention,message.channels, andapp_home_opened
Add YAK_SLACK_WORKSPACE_URL=https://{your-workspace}.slack.com to your vault so the dashboard can deep-link tasks back to their originating Slack thread.
See Channels → Slack for usage and gotchas.
Linear (optional)
Section titled “Linear (optional)”Yak installs as a Linear Agent — a first-class workspace participant that appears in the assignee picker without consuming a seat.
- Go to linear.app/settings/api/applications
→ New application.
- Name:
Yak - Description:
AI coding agent — assign me an issue and I'll open a pull request(this appears in the assignee picker and the install consent screen, so keep it plain) - Icon: use
docs/mascot.pngor any small square yak image - Callback URL:
https://{your-domain}/auth/linear/callback - Enable Webhooks, set the URL to
https://{your-domain}/webhooks/linear, and under App events tick Agent session events. - Copy the app’s webhook signing secret into
linear_webhook_secret.
- Name:
- Copy
Client IDandClient secretintolinear_oauth_client_id/linear_oauth_client_secret. - Re-run Ansible so the env vars land in the container.
- Sign in to the Yak dashboard → Settings → Linear → Connect Linear
and approve the consent screen. A workspace admin must approve — the install requests
app:assignableandapp:mentionablescopes.
See Channels → Linear for usage and gotchas.
Sentry (optional)
Section titled “Sentry (optional)”- In your Sentry org, go to Settings → Developer Settings → Custom Integrations
- Click Create New Integration → Internal Integration
- Set permissions: Organization: Read, Project: Read, Issue & Event: Read (the first two are required for the Add Repository form to populate the Sentry project dropdown)
- Set the webhook URL to
https://{your-domain}/webhooks/sentry - Copy the Token into
sentry_auth_token - Copy the Webhook Signing Secret (under “Webhook Secret” in the integration’s Client Secret section) into
sentry_webhook_secret - Set
sentry_org_slugto your Sentry organization slug - Create an alert rule tagged
yak-eligiblefor the issues you want Yak to pick up - Map Sentry projects to repos via the
sentry_projectfield on each repo in the dashboard
See Channels → Sentry for filtering rules and gotchas.
Drone CI (optional)
Section titled “Drone CI (optional)”- Go to your Drone instance at
https://{drone-url}/account - Copy the Personal Token into
drone_token - Set
drone_urlto your Drone instance URL (e.g.https://drone.yourcompany.com)
Drone has no outbound webhooks — Yak polls the Drone API every minute for CI results, so no webhook config is required on the Drone side.
See Channels → Drone CI for usage and gotchas.
4. Configure Inventory (on your local machine)
Section titled “4. Configure Inventory (on your local machine)”Tell Ansible which server to provision:
cp ansible/inventory/hosts.example.yml ansible/inventory/hosts.ymlall: hosts: yak: ansible_host: 203.0.113.10 ansible_user: root ansible_python_interpreter: /usr/bin/python35. Provision (run from your local machine)
Section titled “5. Provision (run from your local machine)”This connects to the server over SSH and provisions everything:
ansible-playbook ansible/playbook.ymlThis single command runs the following roles in order:
- base — creates the
yakuser, configures UFW, fail2ban, swap, and automatic security updates - docker — installs Docker Engine and Compose
- ssl — provisions a Let’s Encrypt certificate via Caddy, configures log rotation
- github-app — creates and installs the GitHub App on your org (skipped if already provisioned)
- mcp-config — generates
mcp-config.jsonwith only the enabled channels’ MCP servers - mariadb — runs a MariaDB 11 container with persistent storage on a Docker network
- channel-* — conditionally runs each enabled channel role (Slack, Linear, Sentry, Drone)
- yak-container — pulls the pre-built Docker image from ghcr.io, starts the container with env vars
- claude-code-config — installs the Claude CLI, configures slash commands, prints the interactive login prompt
Total time: about 10 minutes.
6. Log In To Claude Code (on the server)
Section titled “6. Log In To Claude Code (on the server)”This is the first step that runs on the Yak server itself, not your local machine. Claude Code CLI authenticates against a Max subscription, not an API key. After provisioning completes, the playbook prints instructions — SSH into the server and run:
docker exec -it yak claude loginFollow the browser-based OAuth flow. The session token persists in the mounted /home/yak/.claude volume and survives container restarts.
The routing layer (Laravel AI) uses the ANTHROPIC_API_KEY from vault for Haiku/Sonnet API calls — separate from the CLI subscription auth.
7. Add Your Repositories (in your browser)
Section titled “7. Add Your Repositories (in your browser)”Repositories are managed through the dashboard — not Ansible. Log in to https://{your-domain}, go to Repositories > Add, and fill in each repo’s HTTPS clone URL. Yak clones the repo using the GitHub App and dispatches a setup task automatically.
See the Repositories page for the full field reference and how setup tasks work.
Verifying the Installation
Section titled “Verifying the Installation”Health Check
Section titled “Health Check”Visit https://{your-domain}/health or run:
docker exec yak php artisan yak:healthcheckThe check covers queue workers, repo fetchability, Claude CLI responsiveness, enabled channel MCP servers, and setup status for each repo.
Smoke Test
Section titled “Smoke Test”Run a manual task against your default repo:
docker exec yak php artisan yak:run TEST-001 "Add a comment to the README explaining what this repo does" --syncThe --sync flag runs the task in the foreground so you can watch the output. If it creates a branch, pushes, and CI runs, Yak is working.
Webhook Verification
Section titled “Webhook Verification”For each enabled channel, trigger a test event:
- Slack — mention
@yakin a channel - Linear — assign a test issue to Yak (the OAuth app appears in the assignee picker)
- Sentry — trigger a test alert rule
- GitHub Actions — push a commit to a
yak/test-*branch
Check https://{your-domain}/tasks — each event should create a task row.
Updating Yak
Section titled “Updating Yak”Application Updates
Section titled “Application Updates”Push to main triggers a GitHub Actions build that pushes a new image to ghcr.io/geocodio/yak. Then pull and deploy:
ansible-playbook ansible/playbook.yml --tags yak-containerTo deploy a specific version:
ansible-playbook ansible/playbook.yml --tags yak-container -e yak_image_tag=abc1234Adding a New Channel
Section titled “Adding a New Channel”- Add the channel’s credentials to
ansible/vault/secrets.yml - Re-run Ansible:
ansible-playbook ansible/playbook.yml - Ansible regenerates the MCP config, updates env vars, restarts the container
- Configure the external service’s webhook URL — see the Channels page
Removing a Channel
Section titled “Removing a Channel”Clear the channel’s credentials in vault (set them to empty strings) and re-run Ansible. Webhook routes for disabled channels return 404. Historical tasks from that channel remain in the database.
Rotating Secrets
Section titled “Rotating Secrets”ansible-vault edit ansible/vault/secrets.ymlansible-playbook ansible/playbook.yml --tags secretsUpdating Repos
Section titled “Updating Repos”Routine Maintenance
Section titled “Routine Maintenance”Yak runs git fetch origin {default_branch} every 30 minutes via the scheduled yak:refresh-repos command. No manual repo updates are needed during normal operation.
Infrastructure Changes
Section titled “Infrastructure Changes”If a repo’s dev environment changes (new Docker services, different database, etc.), re-run the setup task:
docker exec yak php artisan yak:setup-repo my-appOr click Re-run Setup on the repo’s edit page in the dashboard.
Branch deployments
Section titled “Branch deployments”Branch preview deployments use wildcard subdomains of yak_domain (e.g. my-repo-feat-x.yak.example.com). Two pieces of infrastructure beyond the base Yak install are required:
Wildcard DNS
Section titled “Wildcard DNS”Add a wildcard CNAME for *.yak.example.com pointing at the Yak host, same target as the main yak_domain A record. Verify with dig +short anything.yak.example.com.
DNS-01 TLS + provider API token
Section titled “DNS-01 TLS + provider API token”Wildcard certificates require DNS-01 (HTTP-01 does not issue wildcards). Caddy needs a provider plugin baked into its binary.
- Set
caddy_dns_providerinansible/group_vars/yak.ymlto one of Caddy’s supported providers (e.g.cloudflare,route53,digitalocean). - Add the provider’s API token to
ansible/vault/secrets.yml:caddy_dns_provider_api_token: "<token with zone:edit permission for your yak_domain zone>" - Re-run the provisioning playbook (
./deploy.shoransible-playbook ansible/playbook.yml). Thesslrole will download a Caddy binary bundled with the chosen plugin and enable the wildcard Caddyfile block.
If either value is unset, the Caddyfile falls back to dashboard-only routing. Preview deployments will not work until both are configured.
Where To Go Next
Section titled “Where To Go Next”- Channels — per-channel configuration and usage
- Repositories — adding and managing repos, CLAUDE.md guidance
- Architecture — how Yak works under the hood
- Troubleshooting — common issues and solutions