ripen

module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: MIT

README

Ripen

CI Latest release License OpenSSF Scorecard

Fail-closed image updates for Portainer and Compose. A digest ripens. You apply.

Ripen watches the registries behind the images you already run. When a new digest appears it waits, watches it again, and tells you. If you have said so explicitly, it will update one service — pinned to an exact digest, verified afterwards, and rolled back the moment health fails. Then it stops and waits for you.

It never mounts the Docker socket.

$ ripen status
{"schema_version":1,"command":"status","occurred_at":"2026-08-19T09:14:22Z","ok":true,"data":{
  "breaker":{"open":false,"reason":null},
  "services":[{"backend":"docker-compose","stack":"media","service":null,
    "baseline":"sha256:6f8c…","candidate":{"digest":"sha256:19ab…","observations":2,"mature":true}}]}}

[!WARNING] Ripen recreates containers. Start in monitor mode, read what it records, and only then decide whether any stack should carry auto_apply: true.

Quick start

Ripen needs a policy file and somewhere to keep its state. Nothing else. Every field of the policy is documented in docs/configuration.md.

# 1. Get the binary
go install github.com/frankieramirez/ripen/cmd/ripen@latest
# or: docker pull ghcr.io/frankieramirez/ripen
# or: nix run github:frankieramirez/ripen -- version
# or: grab a signed archive from the Releases page

# 2. Describe exactly what Ripen may look at
cp config.example.yaml policy.yaml
$EDITOR policy.yaml

# 3. Watch, and only watch
ripen run --mode monitor --config policy.yaml

A minimal policy for one Compose stack:

mode: monitor
state_file: /var/lib/ripen/ripen.db

stacks:
  media:
    enabled: true
    backend: docker-compose
    file: /srv/media/compose.yaml
    expected_services: [jellyfin]
    health:
      target: http://127.0.0.1:8096/health

Or as a container, which is how most people run it:

services:
  ripen:
    image: ghcr.io/frankieramirez/ripen:latest
    command: ["daemon", "--config", "/config/policy.yaml"]
    read_only: true
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    volumes:
      - ./policy.yaml:/config/policy.yaml:ro
      - ./data:/data
      - /srv/media/compose.yaml:/srv/media/compose.yaml   # only for compose backends

No socket mount. Ever.

The first run records what is running now as the Baseline — nothing else. Later runs report a Candidate when the registry moves. After candidate_min_age_seconds (a day, by default) and a second sighting, that Candidate is mature and apply mode may act on it.

ripen status --pretty       # every configured service and where it stands
ripen candidates --pretty   # what is waiting, and whether it has matured
ripen explain media --pretty  # why the next run would, or would not, act
ripen audit --pretty        # what Ripen has actually done

Without --pretty, each of those prints the JSON Response envelope. The flag is never inferred from a TTY.

Run it on a schedule with ripen daemon, which does the same thing every check_interval_seconds and writes its Event stream to stderr. When Apply reports an open Circuit breaker, the daemon runs Monitor in the same cycle so Candidate observations stay current. Blocked Apply runs emit run.finished with breaker_open: true and the recorded reason. A person must still clear the breaker before updates or Proposals can resume.

status reads stored state; a successful response does not prove that the daemon is making progress. Check the run.finished Events in the container log. The Notifier is off unless configured; use ripen notify test to verify delivery.

How a Transaction works

  1. Observe. Read what is deployed and what is running, and ask the registry what the tag points at now.
  2. Baseline. The first time, record the running digest — and only if it can be proven. If an update is already pending, Ripen refuses to guess.
  3. Ripen. A new digest becomes a Candidate. It must be seen twice and be older than the maturity window before it is eligible for anything.
  4. Apply, in apply mode, on a stack that opted in: check every configured service's health first, pin exactly one image to tag@sha256:…, deploy, and verify every service again.
  5. Roll back if verification fails: restore the Baseline digest and open the Circuit breaker. Ripen takes no further outbound action until a person clears it with a reason.

Git-backed stacks replace step 4 with a Proposal: one deterministic pull request pinning the digest, which Ripen opens and never merges.

What it will not do

  • No privileged Docker socket. Permanently out of scope, not a roadmap item.
  • No unattended sprees. One service per run, and only where you opted in.
  • No self-merging. A Proposal is a pull request for a human to review.
  • No insecure TLS. A CA file or an exact fingerprint. There is no bypass.
  • No agent path to apply. The MCP surface cannot apply an update or clear the breaker; those tools do not exist.

ROADMAP.md has the full list of non-goals and what may come later.

Documentation

Page What it covers
Configuration Every policy field, and what refusing to start protects
Portainer The API backend, its least-privilege user, and TLS trust
Compose Docker and Podman Compose, drift, and rootless sockets
Agents The CLI and MCP surface, envelopes, exit codes
Proposals Git-backed stacks and the pull-request transaction
Notifications The Event stream, the webhook Notifier, suppression
Architecture How the pieces fit and why they are shaped this way
Troubleshooting What each result code means and what to do about it

The vocabulary in all of them is defined once in CONTEXT.md.

Security

Ripen holds credentials for the systems that run your services. Read SECURITY.md before deploying it, and report anything you find through GitHub's private vulnerability reporting rather than an issue.

Every release archive carries GitHub build provenance. Check the one you downloaded before you extract it:

gh attestation verify ripen_<version>_linux_amd64.tar.gz --repo frankieramirez/ripen

The checksums file is attested the same way.

Contributing

Issue first, then a pull request — see CONTRIBUTING.md. This is a project maintained for its author's own use; contributions are welcome and reviewed on a best-effort basis.

License

MIT. See LICENSE.

Directories

Path Synopsis
cmd
ripen command
Command ripen is the entry point for the Ripen binary.
Command ripen is the entry point for the Ripen binary.
internal
app
Package app assembles Ripen from its parts and answers the reads.
Package app assembles Ripen from its parts and answers the reads.
backend
Package backend is the orchestrator seam of the Transaction: the port every backend (Portainer API, compose runtimes) implements, reshaped around what one Transaction needs — observe a stack, deploy a compose document, and prove the stack's services are running.
Package backend is the orchestrator seam of the Transaction: the port every backend (Portainer API, compose runtimes) implements, reshaped around what one Transaction needs — observe a stack, deploy a compose document, and prove the stack's services are running.
cli
Package cli is Ripen's command surface.
Package cli is Ripen's command surface.
compose
Package compose is the compose-runtime backend: one adapter driving a compose CLI, with docker-compose and podman-compose as thin constructors over the same code.
Package compose is the compose-runtime backend: one adapter driving a compose CLI, with docker-compose and podman-compose as thin constructors over the same code.
composefile
Package composefile reads and edits Compose documents as text.
Package composefile reads and edits Compose documents as text.
config
Package config loads and validates the Ripen policy file.
Package config loads and validates the Ripen policy file.
daemon
Package daemon runs Ripen on a schedule.
Package daemon runs Ripen on a schedule.
domain
Package domain holds the core vocabulary of Ripen: modes, backends, actors, result codes, and image references.
Package domain holds the core vocabulary of Ripen: modes, backends, actors, result codes, and image references.
event
Package event is Ripen's Event stream: one stream, many sinks.
Package event is Ripen's Event stream: one stream, many sinks.
github
Package github opens digest-pin Proposals as pull requests.
Package github opens digest-pin Proposals as pull requests.
health
Package health runs the functional health checks a Transaction verifies against.
Package health runs the functional health checks a Transaction verifies against.
mcpserver
Package mcpserver is Ripen's MCP surface: stdio, tools only, and a strict subset of the CLI.
Package mcpserver is Ripen's MCP surface: stdio, tools only, and a strict subset of the CLI.
notifier
Package notifier is the webhook sink of the Event stream: the one outbound Notifier Ripen ships with.
Package notifier is the webhook sink of the Event stream: the one outbound Notifier Ripen ships with.
portainer
Package portainer is the Portainer API backend.
Package portainer is the Portainer API backend.
proposal
Package proposal is the vocabulary of Git-native updates: the change Ripen asks a forge to open, and the Proposal that now exists.
Package proposal is the vocabulary of Git-native updates: the change Ripen asks a forge to open, and the Proposal that now exists.
registry
Package registry is a deliberately minimal OCI registry client for digest observation: resolve what digest a tag points at, per platform.
Package registry is a deliberately minimal OCI registry client for digest observation: resolve what digest a tag points at, per platform.
response
Package response is Ripen's wire surface: the Response envelope every verb answers in, and the typed payloads inside it.
Package response is Ripen's wire surface: the Response envelope every verb answers in, and the typed payloads inside it.
state
Package state is the SQLite state store — schema v1.
Package state is the SQLite state store — schema v1.
updater
Package updater is the Transaction engine: the deep module that owns one complete Monitor or Apply run.
Package updater is the Transaction engine: the deep module that owns one complete Monitor or Apply run.
version
Package version holds the build metadata stamped into release binaries.
Package version holds the build metadata stamped into release binaries.
webui
Package webui is the optional read-only Web UI, embedded in the binary and served inside `ripen daemon`.
Package webui is the optional read-only Web UI, embedded in the binary and served inside `ripen daemon`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL