Skip to content

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.

  • 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
RequirementNotes
ServerDedicated box with 32GB+ RAM, 500GB+ disk. Hetzner AX-series, bare metal, or VM. Ubuntu 24.04 or Debian 12+. Public IP for inbound webhooks.
DomainDNS A record pointing to the server. Used for dashboard and webhook endpoints.
ClaudeMax subscription (for Claude Code CLI) plus an Anthropic API key (for the routing layer).
GitHubOrganization 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 OAuthGoogle Cloud project with OAuth credentials. Used for dashboard authentication.
Ansible2.15+ on your local machine (pip install ansible).

Only configure the channels you use. Everything except GitHub (for pushing branches and opening PRs) is optional.

ChannelWhat you need
SlackSlack app with bot token plus signing secret.
LinearOAuth application (client id + secret + webhook signing secret). Authorize at /settings/linear. Requires workspace admin approval.
SentryAuth token plus webhook secret. Alert rules tagged yak-eligible.
Drone CIAPI token. Yak polls the Drone API — no webhook needed.
GitHub ActionsIncluded with the GitHub App — no additional setup.

See the Channels page for the full configuration of each channel.

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:

Terminal window
pip install ansible # or: brew install ansible
ansible --version # confirm 2.15+

You also need SSH access to the target server as root (or another user with passwordless sudo) before continuing.

Terminal window
git clone https://github.com/geocodio/yak.git
cd yak

3. Configure Secrets (on your local machine)

Section titled “3. Configure Secrets (on your local machine)”
Terminal window
cp ansible/vault/secrets.example.yml ansible/vault/secrets.yml
ansible-vault encrypt ansible/vault/secrets.yml
ansible-vault edit ansible/vault/secrets.yml

Optionally, save your vault password to a file so you don’t have to type --ask-vault-pass on every run:

Terminal window
echo 'your-vault-password' > ansible/vault/.vault_pass

This 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.com
anthropic_api_key: sk-ant-...
github_org: your-org
# Dashboard auth
google_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/callback
linear_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: "..."

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:

  1. Sets NODE_AUTH_TOKEN=ghp_... as a container env var (available to npm install)
  2. Sets YAK_AGENT_PASSTHROUGH_ENV=NODE_AUTH_TOKEN so 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.

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.

  1. Go to console.anthropic.com/settings/keys
  2. Click Create Key
  3. Copy the key (sk-ant-...) into anthropic_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)”
  1. Go to console.cloud.google.com and create a new project (or select an existing one)
  2. Go to APIs & Services → OAuth consent screen
  3. Set user type to Internal (restricts login to your Google Workspace org — no app review needed)
  4. Fill in the app name (e.g. “Yak”) and your support email, then save
  5. Go to APIs & Services → Credentials
  6. Click Create Credentials → OAuth client ID
  7. Application type: Web application
  8. Add an authorized redirect URI: https://{your-domain}/auth/google/callback
  9. Copy the Client ID into google_oauth_client_id
  10. Copy the Client Secret into google_oauth_client_secret
  11. Set google_oauth_allowed_domains to your domain (e.g. yourcompany.com)

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.

  1. Go to api.slack.com/apps and click Create New App → From scratch
  2. Name it (e.g. “Yak”) and select your workspace
  3. Go to OAuth & Permissions and add these bot token scopes:
    • chat:write
    • app_mentions:read
    • channels:history
    • reactions:write — lets Yak react 👀 / 🚧 / ✅ / ❌ on your mention for glanceable status
  4. Click Install to Workspace and authorize
  5. Under Basic Information → Display Information, upload public/slack-icon.png as 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)
  6. Copy the Bot User OAuth Token (xoxb-...) into slack_bot_token
  7. Go to Basic Information and copy the Signing Secret into slack_signing_secret
  8. 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
  9. 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
  10. Go to Event Subscriptions, enable events, and set the request URL to https://{your-domain}/webhooks/slack
  11. Subscribe to bot events: app_mention, message.channels, and app_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.

Yak installs as a Linear Agent — a first-class workspace participant that appears in the assignee picker without consuming a seat.

  1. Go to linear.app/settings/api/applicationsNew 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.png or 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.
  2. Copy Client ID and Client secret into linear_oauth_client_id / linear_oauth_client_secret.
  3. Re-run Ansible so the env vars land in the container.
  4. Sign in to the Yak dashboard → Settings → Linear → Connect Linear and approve the consent screen. A workspace admin must approve — the install requests app:assignable and app:mentionable scopes.

See Channels → Linear for usage and gotchas.

  1. In your Sentry org, go to Settings → Developer Settings → Custom Integrations
  2. Click Create New IntegrationInternal Integration
  3. 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)
  4. Set the webhook URL to https://{your-domain}/webhooks/sentry
  5. Copy the Token into sentry_auth_token
  6. Copy the Webhook Signing Secret (under “Webhook Secret” in the integration’s Client Secret section) into sentry_webhook_secret
  7. Set sentry_org_slug to your Sentry organization slug
  8. Create an alert rule tagged yak-eligible for the issues you want Yak to pick up
  9. Map Sentry projects to repos via the sentry_project field on each repo in the dashboard

See Channels → Sentry for filtering rules and gotchas.

  1. Go to your Drone instance at https://{drone-url}/account
  2. Copy the Personal Token into drone_token
  3. Set drone_url to 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:

Terminal window
cp ansible/inventory/hosts.example.yml ansible/inventory/hosts.yml
all:
hosts:
yak:
ansible_host: 203.0.113.10
ansible_user: root
ansible_python_interpreter: /usr/bin/python3

5. 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:

Terminal window
ansible-playbook ansible/playbook.yml

This single command runs the following roles in order:

  1. base — creates the yak user, configures UFW, fail2ban, swap, and automatic security updates
  2. docker — installs Docker Engine and Compose
  3. ssl — provisions a Let’s Encrypt certificate via Caddy, configures log rotation
  4. github-app — creates and installs the GitHub App on your org (skipped if already provisioned)
  5. mcp-config — generates mcp-config.json with only the enabled channels’ MCP servers
  6. mariadb — runs a MariaDB 11 container with persistent storage on a Docker network
  7. channel-* — conditionally runs each enabled channel role (Slack, Linear, Sentry, Drone)
  8. yak-container — pulls the pre-built Docker image from ghcr.io, starts the container with env vars
  9. claude-code-config — installs the Claude CLI, configures slash commands, prints the interactive login prompt

Total time: about 10 minutes.

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:

Terminal window
docker exec -it yak claude login

Follow 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.

Visit https://{your-domain}/health or run:

Terminal window
docker exec yak php artisan yak:healthcheck

The check covers queue workers, repo fetchability, Claude CLI responsiveness, enabled channel MCP servers, and setup status for each repo.

Run a manual task against your default repo:

Terminal window
docker exec yak php artisan yak:run TEST-001 "Add a comment to the README explaining what this repo does" --sync

The --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.

For each enabled channel, trigger a test event:

  • Slack — mention @yak in 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.

Push to main triggers a GitHub Actions build that pushes a new image to ghcr.io/geocodio/yak. Then pull and deploy:

Terminal window
ansible-playbook ansible/playbook.yml --tags yak-container

To deploy a specific version:

Terminal window
ansible-playbook ansible/playbook.yml --tags yak-container -e yak_image_tag=abc1234
  1. Add the channel’s credentials to ansible/vault/secrets.yml
  2. Re-run Ansible: ansible-playbook ansible/playbook.yml
  3. Ansible regenerates the MCP config, updates env vars, restarts the container
  4. Configure the external service’s webhook URL — see the Channels page

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.

Terminal window
ansible-vault edit ansible/vault/secrets.yml
ansible-playbook ansible/playbook.yml --tags secrets

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.

If a repo’s dev environment changes (new Docker services, different database, etc.), re-run the setup task:

Terminal window
docker exec yak php artisan yak:setup-repo my-app

Or click Re-run Setup on the repo’s edit page in the dashboard.

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:

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.

Wildcard certificates require DNS-01 (HTTP-01 does not issue wildcards). Caddy needs a provider plugin baked into its binary.

  1. Set caddy_dns_provider in ansible/group_vars/yak.yml to one of Caddy’s supported providers (e.g. cloudflare, route53, digitalocean).
  2. 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>"
  3. Re-run the provisioning playbook (./deploy.sh or ansible-playbook ansible/playbook.yml). The ssl role 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.