Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
50e0bb4
refactor(stacks): reorganize into per-stack directories with parametr…
felipefontoura Jun 4, 2026
69227aa
feat(installer): add bento boot.sh + install.sh + lib/ modules
felipefontoura Jun 4, 2026
7cc0343
docs: add CLAUDE.md maintainer guide and add-app-stack skill
felipefontoura Jun 4, 2026
00aa18a
docs(readme): rebrand to bento with logo, Hetzner section, and Best-R…
felipefontoura Jun 4, 2026
1cb7ea5
feat(cloudflare): add optional DNS sync via API token with template U…
felipefontoura Jun 4, 2026
9fb4cad
feat(hardening): rate-limit SSH and permit ICMP in UFW
felipefontoura Jun 4, 2026
c06dbeb
docs: document Cloudflare DNS automation and Hetzner Cloud Firewall
felipefontoura Jun 4, 2026
a16e516
revert(cloudflare): drop API token integration; keep manual DNS only
felipefontoura Jun 4, 2026
8ce28a5
feat(dns): surface Cloudflare DNS-records deep link in install + README
felipefontoura Jun 4, 2026
3a40897
feat(report): generate handoff HTML at end of Step 3 and from menu
felipefontoura Jun 4, 2026
1240f5c
docs(report): document the handoff HTML in README and CLAUDE.md
felipefontoura Jun 4, 2026
adf5822
docs(readme): add Hostinger as secondary BR-focused VPS partner
felipefontoura Jun 4, 2026
62393fe
docs(readme): tighten to first-citizen dev XP + drop tagline from banner
felipefontoura Jun 4, 2026
ff7320d
docs(readme): lead with TL;DR command, collapse ToC
felipefontoura Jun 4, 2026
3492a27
docs: add MIT LICENSE and CONTRIBUTING.md
felipefontoura Jun 4, 2026
8497cfa
chore(github): add issue + PR templates, reference governance from CL…
felipefontoura Jun 4, 2026
c826ced
fix(stacks): default to single-level hostnames so wildcards cover eve…
felipefontoura Jun 4, 2026
c711b4a
docs(dns): drop root A record from required list
felipefontoura Jun 4, 2026
9c2c15d
fix(boot): allow root execution; running as root on a fresh VPS is fine
felipefontoura Jun 4, 2026
19bc902
fix(boot): show progressive ⏵→✓ steps from first byte; silence needre…
felipefontoura Jun 4, 2026
7d62936
chore: English-only policy for all repo artifacts
felipefontoura Jun 4, 2026
468a16c
fix(lib): make every library module idempotent on re-source
felipefontoura Jun 4, 2026
db3f65b
fix(lib): stop re-declaring readonly BENTO_REPO_ROOT in two modules
felipefontoura Jun 4, 2026
6b0a952
fix(boot): default TERM so apt postinst hooks survive without a PTY
felipefontoura Jun 4, 2026
bd4919e
feat(unattended): one-shot install via BENTO_UNATTENDED=1
felipefontoura Jun 4, 2026
95a87cd
fix(install-helpers): correct postgres container pattern + wait for r…
felipefontoura Jun 4, 2026
b2a6391
fix(infra): drop Portainer agent + broken healthcheck, expose 9000 on…
felipefontoura Jun 4, 2026
0fc4260
fix(stacks,portainer): branch-derived REPO_REF + invalidatable JWT cache
felipefontoura Jun 4, 2026
98b2282
fix(evolution-api): move Redis DB to /4 — collision with plunk
felipefontoura Jun 4, 2026
12c6910
feat(unattended): skip already-deployed stacks on re-run
felipefontoura Jun 4, 2026
4cb1cc9
fix(n8n): bump editor/webhook memory so first-start migrations don't OOM
felipefontoura Jun 4, 2026
e7e3c47
fix(manifests): generate-with-tr commands produced variable-length se…
felipefontoura Jun 4, 2026
12449fb
fix(manifests): finish secret-generate cleanup (postgres + rabbitmq)
felipefontoura Jun 4, 2026
9296eb7
fix(cli-proxy-api): emit config.yaml via printf and drop port publish
felipefontoura Jun 4, 2026
9d8c27d
fix(stacks): stop publishing internal db ports to the host
felipefontoura Jun 4, 2026
5657264
fix(typebot): bump builder + viewer memory to 512M to clear Next.js OOM
felipefontoura Jun 4, 2026
6fc258f
fix(cli-proxy-api): root-level `port:` is what the binary actually reads
felipefontoura Jun 4, 2026
ea64bfc
fix(stacks,typebot): local image build for paperclip + V8 heap cap
felipefontoura Jun 5, 2026
2620d5d
fix(stacks): remove typebot healthcheck + run chatwoot db:prepare sta…
felipefontoura Jun 5, 2026
e9771dd
fix(postgres): switch image to pgvector/pgvector:pg15 for chatwoot AI…
felipefontoura Jun 5, 2026
220a063
fix(chatwoot): drop Swarm healthcheck — Rails boot exceeds probe window
felipefontoura Jun 5, 2026
f931868
fix(typebot): probe builder readiness with node, image has no wget
felipefontoura Jun 5, 2026
6d4bfd9
fix(postgres): unquote POSTGRES_INITDB_ARGS so scram-sha-256 actually…
felipefontoura Jun 5, 2026
7e57d85
chore(stacks): add json-file logging rotation to every service
felipefontoura Jun 5, 2026
5e50e88
fix(stacks): drop fragile Swarm healthchecks across app services
felipefontoura Jun 5, 2026
369961c
fix(infra): generate portainer admin password with openssl rand -hex 16
felipefontoura Jun 5, 2026
c4b7ac2
fix(infra): fail Step 2 instead of warning when DNS does not resolve
felipefontoura Jun 5, 2026
7a3774d
chore(stacks): standardize update_config + restart_policy across stacks
felipefontoura Jun 5, 2026
98c6dd5
feat(stacks): warn when selected stacks request more RAM than VPS has
felipefontoura Jun 5, 2026
cbe8e70
feat(stacks,report): track deploy failures and surface them in handoff
felipefontoura Jun 5, 2026
17ef1a9
refactor(paperclip): use upstream image, drop paperclip-custom build
felipefontoura Jun 5, 2026
c761ffb
feat(traefik): support Cloudflare DNS-01 via CF_DNS_API_TOKEN
felipefontoura Jun 5, 2026
fa99fab
docs(readme): update paperclip description after dropping custom image
felipefontoura Jun 5, 2026
2261697
fix(state): always quote values in state_set, drop regex type coercion
felipefontoura Jun 5, 2026
248fc1e
feat(install-helpers): add require_container abort-or-echo helper
felipefontoura Jun 5, 2026
dafffac
feat(menu): wire update menu's stack redeploy to portainer_redeploy_s…
felipefontoura Jun 5, 2026
9e2c5d7
fix(portainer): auto-retry create + redeploy when JWT goes stale
felipefontoura Jun 5, 2026
023caf0
refactor(stacks): assemble env payload with jq -s instead of printf
felipefontoura Jun 5, 2026
57511fa
refactor(ui,install): collapse three prompt-and-validate loops into o…
felipefontoura Jun 5, 2026
935c370
perf(portainer): memoise endpoint_id + swarm_id across calls
felipefontoura Jun 5, 2026
0a53a1b
refactor(install): lift _deploy_with_deps out of unattended_step3
felipefontoura Jun 5, 2026
d5fe810
refactor(palette): single source of truth for pre-gum ANSI colours
felipefontoura Jun 5, 2026
0aeb2ac
refactor(infra): call portainer_wait_ready directly through ui_spin
felipefontoura Jun 5, 2026
c5976f6
chore: small cleanups (state_path, regex scoping, repo-ref, hardening…
felipefontoura Jun 5, 2026
2709e50
fix(deps): verify SHA-256 of gum tarball before installing
felipefontoura Jun 5, 2026
182417b
refactor(stacks): split stacks_resolve_env into single-purpose helpers
felipefontoura Jun 5, 2026
2eac64d
docs: refresh README + CLAUDE.md + SKILL.md against current code
felipefontoura Jun 5, 2026
e4f0da3
feat(hermes,openclaw): add two OpenAI-compatible LLM gateway stacks
felipefontoura Jun 5, 2026
fd22f6d
fix(hermes): explicit gateway-run command so /v1/* actually serves
felipefontoura Jun 5, 2026
143d39a
fix(openclaw): seed gateway.mode + fix volume perms in install hook
felipefontoura Jun 5, 2026
425aebf
fix(openclaw): disable Control UI in the seeded config
felipefontoura Jun 5, 2026
29bc94a
feat(paperclip-runs): add bento stack pointing at ghcr image
felipefontoura Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .assets/bento-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 changes: 274 additions & 0 deletions .claude/skills/add-app-stack/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
---
name: add-app-stack
description: Scaffold a new application stack in the bento repo following the established conventions (per-stack directory with compose.yml + manifest.json + optional install.sh)
---

# add-app-stack

Use this skill when the user asks to add a new application stack to the
bento repo. Always start by reading `CLAUDE.md` for current conventions —
prefer its rules over anything stated here if they ever drift.

**Every comment, message, and doc you produce must be in English.** Even
when the user prompts you in Portuguese, the artifacts going into the
repo stay English-only.

## When to invoke

Trigger phrases include:
- "add a new stack for <App>"
- "I want to bundle <App> into bento"
- "create a stack: <App>"
- "scaffold <App>"

If the user is just asking to **deploy** an existing stack via the menu,
that is not this skill — point them at `Step 3` in the install menu.

## Required inputs

Ask for these up front if missing:

1. **Stack key** — short, kebab-case, lowercase (e.g. `n8n`, `cli-proxy-api`).
Will be the directory name and the user-visible label in the menu.
2. **Upstream GitHub repo** — `<owner>/<repo>` (e.g. `n8n-io/n8n`). This is
non-negotiable: the skill fetches the project's own reference
`docker-compose.yml` and `.env.example` before writing anything.
3. **Public-facing host** — does this app expose an HTTP UI/API behind
Traefik? If yes, the default host pattern is `<key>.${BASE_DOMAIN}`.
If no, no Traefik labels are needed.

Everything else (image tag, env vars, secrets, dependencies, ports,
healthcheck endpoint) is **derived from the upstream repo** in step 1
below — do not invent any of it from training data.

## Execution

Always run these steps **in order** with `TaskCreate` so progress is visible:

### 1. Fetch the upstream reference (mandatory)

Use `gh` to pull the project's own docker artifacts. **Do not guess env
vars or image tags from training data** — open-source projects keep these
in the repo, and that is the source of truth.

```bash
OWNER_REPO="<owner>/<repo>"

# Latest stable release tag — pin this in compose.yml, not :latest.
gh api "repos/${OWNER_REPO}/releases/latest" --jq '.tag_name'

# Reference docker-compose. Try the common paths.
for path in docker-compose.yml docker-compose.yaml compose.yml \
compose.yaml docker/docker-compose.yml; do
gh api "repos/${OWNER_REPO}/contents/${path}" --jq '.content // empty' \
2>/dev/null | base64 -d && echo "--- from ${path}" && break
done

# Env example.
gh api "repos/${OWNER_REPO}/contents/.env.example" --jq '.content' \
2>/dev/null | base64 -d

# README, especially the "Docker" / "Self-hosting" section.
gh api "repos/${OWNER_REPO}/readme" --jq '.content' | base64 -d
```

If `gh api contents/<path>` returns nothing, search:

```bash
gh api search/code -X GET \
-f q="repo:${OWNER_REPO} filename:docker-compose"
```

For projects whose docs live outside the GitHub repo (their own website,
Notion, etc.), use `WebFetch` against the documented deployment page.

From the upstream artifacts, extract:

- **Image and tag** — pin the latest stable release.
- **All env vars** with purpose + default + whether secret.
- **Required ports** the app listens on.
- **Dependencies** — Postgres? Redis? S3-compatible? Mail relay?
- **Volumes** the app expects.
- **Multi-service shape** — does it deploy a separate worker/webhook/UI?

### 2. Read the closest existing bento stack

Pick the analogue and read its three files. **n8n is the gold standard for
parametrization quality** — copy its env-block layout (commented
categories, one-line WHY per var) unless the app is genuinely simpler.

- **Gold standard / categorized env block / multi-service**: `stacks/app/n8n/`.
- **Custom-built image**: `stacks/app/paperclip/`.
- **Rails-style with DB migrations**: `stacks/app/chatwoot/`.
- **Genuinely tiny (no meaningful knobs)**: `stacks/app/cli-proxy-api/`.

Mirror the patterns exactly. Do not invent new structure or naming.

### 3. Create the directory

`stacks/app/<your-key>/`

### 4. Write `compose.yml`

Use the upstream reference as the base, then translate to bento
conventions. Aim for n8n's quality bar (see CLAUDE.md → Quality bar).

Parametrize everything that varies per deployment:

| Was hardcoded | Replace with |
|---|---|
| `Host(\`xxx.website.com\`)` | `` Host(`${KEY_HOST}`) `` |
| `APP_URI=https://xxx.website.com` | `APP_URI=https://${KEY_HOST}` |
| `JWT_SECRET=secret` | `JWT_SECRET=${KEY_JWT_SECRET}` |
| `postgresql://app:secret@postgres:5432/db` | `postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/db` |
| `external: true` (volumes other than `network_public`) | `driver: local` |

Do NOT add a Swarm healthcheck to an app service. Bento removed them
on purpose: `wget`-based probes on small VPS were SIGKILLing healthy
containers (missing binary, slow boot, SSR compile inside the
`start_period`). Traefik does the user-facing health check externally.
The only healthchecks bento keeps are the cheap DB ones
(`pg_isready`, `redis-cli ping`, `rabbitmq-diagnostics ping`).

Always include a `logging:` block with `json-file` + `max-size: 10m`
+ `max-file: 3` — keeps a chatty debug app from filling the disk.

Use `network_public` external and as the only network attached.

**Env block layout** — group variables into commented sections with the
same pattern n8n uses:

```yaml
environment:
# =============================================================================
# Core Application
# =============================================================================

# Brief WHY this matters or what it controls.
- SOME_VAR=${SOME_VAR}

# =============================================================================
# Database
# =============================================================================

# …
```

Categories that recur across stacks: `Core Application`,
`Observability / Logging`, `Public URLs / Reverse Proxy`,
`Database (PostgreSQL)`, `Cache / Queue (Redis)`, `Security`, `Email (SMTP)`,
`OAuth / Authentication`, `Storage (S3)`, `Workers / Queue Mode`, `Timezone`.

### 5. Write `manifest.json`

Required keys: `name`, `category` (`"app"`), `description`, `env[]`.

For each env in the compose, add a corresponding entry. Pick the right
resolution mode:

- `default: "<key>.${BASE_DOMAIN}"` + `prompt` — for hostnames the user may
want to override.
- `generate: "openssl rand -hex 32"` + `hide: true` — for secrets.
- `from_state: "POSTGRES_PASSWORD"` — to reuse the postgres password
without re-prompting.
- `prompt: "…"` + `hide: true` — for sensitive user-supplied values (API keys).
- `prompt: "…"` only — for non-sensitive optional values.

Set `depends_on: ["postgres"]` (or `["postgres", "redis"]`) if applicable.
Add `post_deploy_url: "https://${KEY_HOST}"` so the menu prints the right
URL at the end.

Validate with `jq -e .` before moving on.

### 6. Write `install.sh` ONLY if needed

Decide:
- **No install.sh** if the app self-bootstraps on first browser visit
(n8n, plunk, paperclip, typebot pattern).
- **Yes install.sh** if you need to: create a Postgres DB, run migrations,
seed initial data, bootstrap an admin user via internal CLI/exec.

Template:

```bash
#!/bin/bash
set -euo pipefail
source "${BENTO_REPO_ROOT}/lib/install-helpers.sh"

ensure_database <dbname>
```

For Rails-style migrations, model on `stacks/app/chatwoot/install.sh`:
wait for the web service to be healthy, then `docker exec` the migration
command.

Make it executable: `chmod +x stacks/app/<your-key>/install.sh`.

### 7. Update README

Insert an alphabetical entry under `### Applications`:

```markdown
- **[<App Name>](stacks/app/<your-key>):** <one-line description>.
```

### 8. Smoke verify

Run all of these from the repo root and ensure each passes:

```bash
bash -n stacks/app/<your-key>/install.sh # if it exists
jq -e . stacks/app/<your-key>/manifest.json
docker compose -f stacks/app/<your-key>/compose.yml config >/dev/null
grep -rn 'website\.com\|=secret$' stacks/app/<your-key>/ || echo "clean"
```

If any check fails, fix before reporting back to the user.

### 9. Commit

Single atomic commit:

```
feat(<your-key>): add <App Name> stack
```

If the install script is non-trivial, consider a follow-up commit:

```
feat(<your-key>): bootstrap database on first deploy
```

## Failure modes to watch for

- **Skipping the upstream fetch and inventing env vars** — the most common
cause of a broken stack. Always fetch first.
- **Pinning `:latest` when the upstream has tagged releases** — defeats
reproducibility. Use the tag from `releases/latest`.
- **Stack key mismatches directory name** — `manifest.json` `name` field
must equal the directory name.
- **Forgetting `chmod +x` on install.sh** — `lib/stacks.sh` checks `-x`
before running it, so a non-executable script is silently skipped.
- **Using `${VAR:?…}` for a default-able value** — that aborts with no
default; prefer `${VAR:-default}` or proper manifest entries.
- **Flat env block without categories** — fails the quality bar. Group
with comment headers like n8n does.
- **Reaching for a custom Dockerfile too quickly** — `lib/stacks.sh`
auto-detects `build:` and runs `docker compose build` before the
Portainer stack create, so a custom image will work, but the
operational cost (big images, slow first deploy, disk pressure) is
steep. Prefer extending the upstream image at runtime via
`docker exec` whenever possible. If you must use a Dockerfile,
context = `.` (the stack directory), dockerfile = `Dockerfile`.

## Reporting back

After finishing, tell the user:
- Upstream sources consulted (which `docker-compose.yml`, `.env.example`,
README sections you read).
- Stack key chosen and directory created.
- Image tag pinned and what release it came from.
- Which env vars will be auto-generated vs prompted vs reused from state.
- Whether an install script was needed and what it does.
- The smoke checks that passed.
- Where the post-deploy URL will land.
67 changes: 67 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Bug report
description: Something is broken in bento — hardening crashed, Step 2 fails, a stack won't deploy, etc.
title: "Bug: "
labels: ["bug"]
body:
- type: textarea
id: what
attributes:
label: What happened?
description: What were you trying to do and what broke? Be specific.
validations:
required: true

- type: textarea
id: expected
attributes:
label: What did you expect?
validations:
required: true

- type: dropdown
id: step
attributes:
label: Which step broke?
options:
- Bootstrap (domain / email / IP prompts)
- Step 1 — Harden
- Step 2 — Infra
- Step 3 — Apps
- Update
- Other / unsure
validations:
required: true

- type: input
id: distro
attributes:
label: Distro and version
placeholder: "e.g. Ubuntu 24.04 LTS, Debian 12"
validations:
required: true

- type: input
id: commit
attributes:
label: bento commit
description: "Run: cd ~/.local/share/bento && git rev-parse --short HEAD"
placeholder: "e.g. ff7320d"
validations:
required: true

- type: textarea
id: logs
attributes:
label: Relevant logs
description: |
Hardening: ~/.local/state/bento/logs/hardening-*.log
Stack logs: `docker service logs --tail 200 <stack-name>`
render: shell

- type: checkboxes
id: terms
attributes:
label: Before submitting
options:
- label: I read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md).
required: true
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question or discussion
url: https://github.com/felipefontoura/bento/discussions
about: For general questions, ideas, or "is this the right approach" — please use Discussions instead of opening an Issue.
34 changes: 34 additions & 0 deletions .github/ISSUE_TEMPLATE/feature.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Feature request
description: Propose a change to bento's behavior. For new application stacks, use the "Stack request" template instead.
title: "Feature: "
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What's currently painful or missing? Be concrete — describe a real situation, not a hypothetical.
validations:
required: true

- type: textarea
id: proposal
attributes:
label: Proposed approach
description: How would you solve it? Describe the outcome, not implementation details (unless that's the point).
validations:
required: true

- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What else did you think about? Why did the proposal win?

- type: checkboxes
id: terms
attributes:
label: Before submitting
options:
- label: I read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md).
required: true
Loading