ripen

module
v1.3.0 Latest Latest
Warning

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

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

README

CI Latest release License OpenSSF Scorecard

Fail-closed container image updates for Portainer, Docker Compose, and Podman Compose.

Ripen watches image registries and waits for a new digest to mature before it can update a service. You choose which stacks may update automatically. Ripen verifies service health after each update and rolls back if verification fails. Git-backed stacks receive a pull request for human review.

Quick start · Run continuously · How it works · Safety limits · Documentation

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

Quick start

1. Install Ripen

Download a binary from Releases (see release verification), or install with Go:

go install github.com/frankieramirez/ripen/cmd/ripen@latest

With Nix, run nix run github:frankieramirez/ripen -- version. For the container image, see Run in a container.

2. Create a policy

For a local Compose stack, run Ripen where the Docker or Podman Compose CLI can reach your engine. The Compose file and its directory must be writable. Read the Compose setup guide for engine requirements and rootless connections. Ripen refuses the privileged Docker socket.

Save this as policy.yaml, adapting the stack path, service name, and health URL to your deployment:

mode: monitor
state_file: ./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

For Portainer, follow the Portainer setup guide to configure its API credentials and TLS trust. The example policy shows both backends; Configuration documents every field.

3. Run once in monitor mode

ripen run --mode monitor --config policy.yaml

The first run records the running digest as the Baseline. Later runs report a Candidate when the registry moves. A Candidate matures after candidate_min_age_seconds (one day by default) and a second observation. Monitor mode leaves your services unchanged.

4. Inspect the results

Command What it shows
ripen status --config policy.yaml --pretty Configured services and their current state
ripen candidates --config policy.yaml --pretty Candidates and whether they have matured
ripen explain media --config policy.yaml --pretty Why Ripen would or would not act on the stack
ripen audit --config policy.yaml --pretty Recorded actions

Omit --pretty for the JSON Response envelope used by scripts and agents. Ripen never infers this flag from a TTY. See Agents for the response format and exit codes.

Run continuously

ripen daemon --config policy.yaml

The daemon observes at startup and on fixed check_interval_seconds ticks, checking up to two stacks concurrently. Set observation_concurrency from 1 through 8 to change that limit. While one stack deploys, verifies, or rolls back, unrelated stacks continue receiving checks. The active stack waits until a later scheduled check. Missed checks coalesce instead of accumulating a backlog.

Deployments remain sequential, with at most one update per Apply cycle and a full interval after Apply completes before another cycle may update. An open Circuit breaker permits observation but blocks updates and Proposals until a person clears it. The daemon writes its Event stream to stderr.

status reads stored state; a successful response does not prove that the daemon is making progress. Its scheduler, stack checks, and service evaluations show stored progress separately from Candidate age; an unfinished Transaction shows its phase and whether its ownership has expired. Check the run.finished Events in the container log. The Notifier is off unless configured; use ripen notify test to verify delivery.

If status shows an interrupted Transaction, ordinary breaker clearing refuses to resume deployments. Once you have confirmed the earlier backend request finished and restored a healthy Baseline, use clear-breaker --reconcile --reason "...". See recovery requirements, including the restriction on interrupted Proposal requests.

Run in a container

The published image includes Ripen and CA certificates. Use it with a Portainer policy; local Compose backends also need an engine CLI, which this image does not include.

Set state_file: /data/ripen.db in your policy. Create a writable ./data directory for the container's UID/GID 65532:65532, and mount the credential file at the path configured by portainer.api_key_file:

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
      - ./portainer-api-key:/run/secrets/portainer-api-key:ro

Ensure the container user can read the policy and credential file. If you use a custom CA, mount that file read-only at portainer.tls_ca_file too. Health check URLs must be reachable from inside the container.

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 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. Further updates and Proposals stay blocked until a person clears it with a reason. Monitor and reads continue.

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

Safety limits

Limit Behavior
Privileged Docker socket Ripen refuses it at configuration load.
Update scope One service per run, only where you opted in.
Proposals Ripen opens a pull request and leaves merging to a person.
Portainer TLS An explicit CA file or exact certificate fingerprint is required.
Agent permissions MCP has no tools to apply updates or clear the Circuit breaker.

Roadmap covers non-goals and possible future work.

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

Open an issue before 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 persists Baselines, Candidates, and coordination state in SQLite.
Package state persists Baselines, Candidates, and coordination state in SQLite.
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