Add Clay agent interface, fix registry UI routing, and misc improvements

- Deploy Clay (claude Code web UI) at agent.ladkau.de behind basic auth;
  Traefik proxies to clay's HTTPS port 2633 with insecureSkipVerify
- Document MCP server persistence: binaries need to be baked into the
  Dockerfile, config in /opt/clay/data survives rebuilds
- Document scheduled agent workflows via Gitea Actions on.schedule with
  email reporting via Gmail SMTP
- Fix registry UI: split /v2/ (registry) and / (UI) into separate Traefik
  routers; add registry_internal network
- Add weekly registry GC cron job (/usr/local/bin/registry-gc)
- Remove rate-limit middleware from Gitea router (act_runner polling
  exceeded 60 req/min limit)
- Set Traefik websecure readTimeout: 0 to fix large layer upload 499s
This commit is contained in:
ml
2026-08-01 07:55:20 +02:00
parent 735563fc31
commit 8adbd3fac4
13 changed files with 321 additions and 1 deletions
+162
View File
@@ -25,6 +25,7 @@ The script checks these endpoints and verifies the expected HTTP status code:
| `https://k8s.ladkau.de` | 200 | Placeholder page |
| `https://vault.ladkau.de` | 200 | Vaultwarden web vault |
| `https://dl.ladkau.de` | 200 | Public download server |
| `https://agent.ladkau.de` | 401 | Clay — basic-auth prompt, correct without credentials |
A `000` result means the connection was refused or timed out — a container that
did not start. Keycloak and Nextcloud may return `502` for up to 90 seconds on
@@ -468,3 +469,164 @@ and tags, inspecting image manifests and digests, and deleting images.
Traefik routes `/v2/` (Docker API) to the registry container and everything else
to the UI container — both on the same domain, so no CORS configuration is
needed. `docker push` and `docker pull` work exactly as before.
## 9. Clay — Claude Code web interface
Clay is a browser-based front end for Claude Code, served at
`https://agent.ladkau.de`. Access is protected by HTTP basic auth using the
`clay_users` credentials from the vault.
### 9.1 Add projects
The container mounts `/opt/clay/workspace` at `/workspace`. Clone Gitea
repositories there to make them available to Claude Code:
```bash
ssh -i keys/notroot_cloud_ladkau_de deploy@217.154.207.148
cd /opt/clay/workspace
git clone https://gitea.ladkau.de/<username>/<repo>.git
```
Then in the clay web UI, use **Add project** and point it at `/workspace/<repo>`.
### 9.2 MCP servers and skills
Clay configuration (MCP server entries, session history, project registrations)
is stored in `~/.clay`, which is mounted from `/opt/clay/data` on the host and
survives container rebuilds.
MCP server **binaries** installed interactively inside a running container (e.g.
via `npm install -g` in a clay terminal) are part of the container filesystem
and are lost when the image is rebuilt.
To make a package survive rebuilds, add it to
`ansible/roles/clay/files/Dockerfile` and redeploy:
```dockerfile
# MCP servers and skills — add packages here to survive container rebuilds
RUN npm install -g @clay-ai/clay-ralph
RUN npm install -g @modelcontextprotocol/server-filesystem
```
```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags clay --ask-vault-pass
```
Ansible detects the Dockerfile change and triggers a rebuild automatically.
### 9.3 Rebuild after clay updates
The Docker image is built from `ansible/roles/clay/files/Dockerfile` at deploy
time. To pull upstream changes to `clay-server` or `@anthropic-ai/claude-code`,
rebuild the image and restart the container:
```bash
ansible-playbook -i ansible/inventory.ini ansible/site.yml --tags clay --ask-vault-pass
```
The Ansible role forces a rebuild whenever the Dockerfile changes; to force a
rebuild without a Dockerfile change, delete the cached image first:
```bash
ssh -i keys/notroot_cloud_ladkau_de deploy@217.154.207.148 \
"sudo docker image rm clay:local && sudo docker compose -f /opt/clay/docker-compose.yml up -d"
```
### 9.4 Interactive sessions vs. scheduled runs
Clay and Gitea Actions serve different purposes and are independent of each
other:
| | Clay | Gitea Actions |
|---|---|---|
| **Trigger** | You, in the browser | Schedule or manual button click |
| **Use case** | Exploratory, iterative work | Recurring automated tasks |
| **Project** | Repos cloned into `/opt/clay/workspace/` | Checked out fresh per run |
| **Output** | Interactive UI | Logs + optional email |
Clay has no scheduling API — there is no way to trigger a Clay session
programmatically. For recurring agent tasks, Gitea Actions runs Claude Code
directly on the act_runners, independent of Clay. If both operate on the same
git repository the context is identical, but the sessions are separate.
### 9.5 Scheduled Claude Code runs via Gitea Actions
The act_runner containers can run Claude Code on a recurring schedule using
Gitea's `on.schedule` trigger. Adding `workflow_dispatch` alongside it allows
the same workflow to be triggered manually from the Gitea UI without waiting
for the next scheduled run.
**Set up secrets**
In the repository go to **Settings → Secrets → Actions** (or
**Site Administration → Actions → Secrets** for org-wide secrets) and add:
| Secret | Value |
|--------|-------|
| `ANTHROPIC_API_KEY` | API key from the vault |
| `SMTP_USERNAME` | Gmail address used as the SMTP sender |
| `SMTP_PASSWORD` | Gmail App Password — myaccount.google.com → Security → App passwords |
**Workflow file**
Add `.gitea/workflows/weekly-agent.yml` to the repository:
```yaml
name: Weekly agent run
on:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
workflow_dispatch: # also runnable manually from the Gitea Actions UI
jobs:
agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --print "Your prompt here" \
--output-format text \
| tee agent-output.txt
- name: Send report email
env:
SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
run: |
python3 - <<'EOF'
import smtplib, os
from email.mime.text import MIMEText
with open("agent-output.txt") as f:
body = f.read()
msg = MIMEText(body)
msg["Subject"] = "Weekly agent report"
msg["From"] = os.environ["SMTP_USERNAME"]
msg["To"] = os.environ["SMTP_USERNAME"]
with smtplib.SMTP("smtp.gmail.com", 587) as s:
s.starttls()
s.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
s.send_message(msg)
EOF
```
Adjust the `cron` expression, prompt, and `msg["To"]` address as needed.
**Run manually**
Go to **Repository → Actions**, select **Weekly agent run** from the workflow
list, and click **Run workflow**. The run appears immediately in the Actions log.
**Verify the schedule is active**
After pushing the workflow file, scheduled runs appear automatically in
**Repository → Actions** at the configured time.
+10 -1
View File
@@ -81,6 +81,7 @@ Let's Encrypt certificates on first start and DNS must resolve at that point.
| k8s.ladkau.de | A → server IP |
| vault.ladkau.de | A → server IP |
| dl.ladkau.de | A → server IP |
| agent.ladkau.de | A → server IP |
### 4. Create the vault and populate secrets
@@ -121,6 +122,13 @@ registry_users:
- username: alice
password: "your-password"
# Clay — Claude Code web interface at agent.ladkau.de
# Generate API key at console.anthropic.com → API keys
anthropic_api_key: ""
clay_users:
- username: admin
password: "your-password"
# Gitea Actions runners — token obtained after Gitea is running
# See step 3.4 of runbook-configuration.md; replace after Gitea admin account is created
gitea_runner_registration_token: "placeholder"
@@ -221,7 +229,8 @@ This runs all roles in order:
| 10 | `k8s` | Placeholder page at k8s.ladkau.de |
| 11 | `vaultwarden` | Vaultwarden password vault at vault.ladkau.de |
| 12 | `dl` | Public download server at dl.ladkau.de — nginx HTTPS + SFTP upload |
| 13 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats |
| 13 | `clay` | Claude Code web interface at agent.ladkau.de — basic auth, Anthropic API key required |
| 14 | `dashboard` | Public status dashboard at cloud.ladkau.de — service health and server stats |
> **Note:** The `act_runner` role requires `gitea_runner_registration_token` in the
> vault, which can only be obtained after Gitea is running and an admin account has