RepoPilotOpen in app →

SUSE/Portus

Authorization service and frontend for Docker registry (v2)

Healthy

Healthy across all four use cases

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • 6 active contributors
  • Apache-2.0 licensed
  • CI configured
Show 3 more →
  • Tests present
  • Stale — last commit 3y ago
  • Concentrated ownership — top contributor handles 71% of recent commits

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/suse/portus)](https://repopilot.app/r/suse/portus)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/suse/portus on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: SUSE/Portus

Generated by RepoPilot · 2026-05-10 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/SUSE/Portus shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across all four use cases

  • 6 active contributors
  • Apache-2.0 licensed
  • CI configured
  • Tests present
  • ⚠ Stale — last commit 3y ago
  • ⚠ Concentrated ownership — top contributor handles 71% of recent commits

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live SUSE/Portus repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/SUSE/Portus.

What it runs against: a local clone of SUSE/Portus — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in SUSE/Portus | Confirms the artifact applies here, not a fork | | 2 | License is still Apache-2.0 | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 1237 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>SUSE/Portus</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of SUSE/Portus. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/SUSE/Portus.git
#   cd Portus
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of SUSE/Portus and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "SUSE/Portus(\\.git)?\\b" \\
  && ok "origin remote is SUSE/Portus" \\
  || miss "origin remote is not SUSE/Portus (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(Apache-2\\.0)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"Apache-2\\.0\"" package.json 2>/dev/null) \\
  && ok "license is Apache-2.0" \\
  || miss "license drift — was Apache-2.0 at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "app/assets/javascripts/main.js" \\
  && ok "app/assets/javascripts/main.js" \\
  || miss "missing critical file: app/assets/javascripts/main.js"
test -f "app/assets/javascripts/config.js" \\
  && ok "app/assets/javascripts/config.js" \\
  || miss "missing critical file: app/assets/javascripts/config.js"
test -f "Gemfile" \\
  && ok "Gemfile" \\
  || miss "missing critical file: Gemfile"
test -f ".rubocop.yml" \\
  && ok ".rubocop.yml" \\
  || miss "missing critical file: .rubocop.yml"
test -f "app/assets/javascripts/modules/namespaces/store.js" \\
  && ok "app/assets/javascripts/modules/namespaces/store.js" \\
  || miss "missing critical file: app/assets/javascripts/modules/namespaces/store.js"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 1237 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~1207d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/SUSE/Portus"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Portus is an authorization server and web UI for Docker Registry v2 that implements token-based authentication and fine-grained permission control. It sits between Docker clients and a private registry, issuing access tokens and providing a dashboard for managing users, teams, and namespaces with granular read/write permissions on Docker images. Monolithic Rails app (Gemfile present) with Vue.js frontend modules: app/assets/javascripts/modules/ contains Vue components (e.g., admin/registries/, dashboard/) alongside Rails controllers. Assets organized by domain: app/assets/javascripts/base for shared utilities, modules for feature domains with nested pages/, components/, and service.js for API calls.

👥Who it's for

DevOps engineers and container platform administrators running self-hosted Docker registries who need centralized access control, LDAP integration, and a web interface to manage private image repositories without relying on Docker Hub or third-party SaaS.

🌱Maturity & risk

Production-ready and actively maintained: last stable release is 2.4.3, dual CI pipelines on master and v2.4 branches via Travis CI, Code Climate integration with test coverage tracking, and Dockerfile + Vagrantfile provided for deployment. Codebase shows 1M+ Ruby LOC indicating substantial maturity, though activity appears to have stabilized post-2.4.

Moderate risk: depends on external Docker Registry v2.1+ API contract (no vendoring visible), requires PostgreSQL/MySQL backend not shown in file list, and LDAP integration adds authentication complexity. Single GitHub organization (SUSE) owns it; community contribution appears limited based on minimal visible CI/CD workflow files.

Active areas of work

Cannot determine from file list alone—no recent commit dates or open PR data visible. Last documented stable version is 2.4.3; README references both master and v2.4 branches under active CI, suggesting maintenance mode rather than active feature development.

🚀Get running

git clone https://github.com/SUSE/Portus.git
cd Portus
bundle install
rake db:create db:migrate
rails server

Then visit http://localhost:3000. Alternatively use Vagrant: vagrant up per Vagrantfile.

Daily commands: Development: rails server (starts on port 3000 by default). Docker: docker build -t portus . && docker run -p 3000:3000 portus. Guard available (Guardfile) for file-watch reload: bundle exec guard.

🗺️Map of the codebase

  • app/assets/javascripts/main.js — Primary JavaScript entry point that bootstraps the frontend application and loads all modules.
  • app/assets/javascripts/config.js — Central configuration file that sets up global application settings and environment variables.
  • Gemfile — Ruby dependency manifest; defines all backend services, frameworks, and authentication mechanisms.
  • .rubocop.yml — Code style enforcement configuration that every backend contributor must follow for CI/CD compliance.
  • app/assets/javascripts/modules/namespaces/store.js — Vuex store for namespace state management; central hub for authorization and access control state.
  • .eslintrc — JavaScript linting configuration that enforces frontend code quality standards across all modules.
  • VERSION — Single source of truth for application versioning, used in builds and deployment manifests.

🛠️How to make changes

Add a new API service endpoint

  1. Create a new service file at app/assets/javascripts/modules/{module}/services/{resource}.js exporting REST methods (app/assets/javascripts/modules/repositories/services/repositories.js)
  2. Define HTTP methods (GET, POST, PUT, DELETE) that call your backend routes with proper error handling (app/assets/javascripts/modules/namespaces/services/namespaces.js)
  3. Import and use the service in your Vue component or Vuex action to fetch/update data (app/assets/javascripts/modules/namespaces/pages/show.vue)

Add a new Vue form component

  1. Create .vue file at app/assets/javascripts/modules/{module}/components/{name}-form.vue with template, script, and style (app/assets/javascripts/modules/namespaces/components/edit-form.vue)
  2. Use form mixin for validation and submission logic at app/assets/javascripts/modules/{module}/mixins/form.js (app/assets/javascripts/modules/namespaces/mixins/form.js)
  3. Call service methods from your form to POST/PUT data and emit success/error events to parent (app/assets/javascripts/modules/repositories/components/comments/form.vue)

Add a new data table with actions

  1. Create table component at app/assets/javascripts/modules/{module}/components/{resource}-table.vue iterating over data array (app/assets/javascripts/modules/repositories/components/table.vue)
  2. Create corresponding table-row.vue component for individual row rendering with action buttons (app/assets/javascripts/modules/repositories/components/table-row.vue)
  3. Bind action handlers to service methods and dispatch state mutations via Vuex store (app/assets/javascripts/modules/namespaces/store.js)

Add a new visibility/access control feature

  1. Create visibility component at app/assets/javascripts/modules/{module}/components/visibility.vue (app/assets/javascripts/modules/namespaces/components/visibility.vue)
  2. Use visibility mixin at app/assets/javascripts/modules/{module}/mixins/visibility.js for access logic (app/assets/javascripts/modules/namespaces/mixins/visibility.js)
  3. Update Vuex store mutations to handle visibility state changes (app/assets/javascripts/modules/namespaces/store.js)

🔧Why these technologies

  • Vue.js + Vuex — Provides reactive, component-based UI with centralized state management for complex namespace/repository authorization workflows.
  • Rails Backend — Rapid development of REST APIs, built-in ORM for PostgreSQL, and strong authentication/authorization middleware ecosystem.
  • Docker Registry v2 API — Standard protocol for image distribution; Portus acts as authorization proxy and UI layer for registry operations.
  • PostgreSQL — Relational data model for users, namespaces, repositories, and access control rules with ACID guarantees.
  • ESLint + Rubocop — Enforce consistent code quality and style across JavaScript and Ruby to reduce onboarding friction for contributors.

⚖️Trade-offs already made

  • Vue SPA with separate REST API backend rather than server-side rendering

    • Why: Enables rich interactive UI, decouples frontend from backend concerns, and allows independent scaling.
    • Consequence: Requires API versioning strategy and CORS handling; slightly higher latency for initial data loads without caching.
  • Centralized Vuex store for namespace/repository state rather than component-local state

    • Why: Multiple pages and components need to share and synchronize authorization state; single source of truth reduces bugs.
    • Consequence: Adds boilerplate for actions and mutations; steeper learning curve for new contributors unfamiliar with Vuex patterns.
  • Authorization checks in both Rails API and frontend Vue components

    • Why: Backend enforces security; frontend hides unauthorized actions for UX and reduces unnecessary API calls.
    • Consequence: Duplicated authorization logic risks drift; requires careful testing to ensure consistency.
  • Soft delete support (Registry v2.1+) rather than hard delete

    • Why: Allows recovery and audit trails while minimizing blob storage impact.
    • Consequence: Must manage soft-deleted data in queries and UI; adds complexity to cleanup jobs.

🚫Non-goals (don't propose these)

  • Does not implement Docker image build or CI/CD orchestration
  • Does not handle direct image push/pull—relies on Docker Registry v2 for that
  • Not a real-time system; does not support live push notifications or streaming image uploads
  • Does not provide image vulnerability scanning or compliance enforcement (integrations only)

🪤Traps & gotchas

Registry credentials and token signing keys must be configured in .env (not in repo); Docker Registry v2 instance must be running and accessible at a configured endpoint before Portus can sync images; LDAP support is optional but requires additional config if enabled; database must exist before running migrations (no auto-creation visible); Gemfile.lock ties Ruby/gem versions—bundle install may fail on different Ruby versions without matching .ruby-version.

🏗️Architecture

💡Concepts to learn

  • docker/distribution — Official Docker Registry v2 implementation that Portus wraps authorization for; defines the token API spec Portus implements
  • projectatomic/atomic-registry — Alternative Kubernetes-native registry UI and auth solution for Docker images; direct competitor in self-hosted registry space
  • harbor/harbor — Modern alternative offering similar team/namespace permissions, LDAP, and scanning; more actively developed replacement for Portus use cases
  • SUSE/Portus-operator — Kubernetes operator for deploying Portus; ecosystem extension showing how Portus integrates with container orchestration
  • opencontainers/image-spec — OCI image format specification that Portus registry clients conform to; foundational standard Portus manages access to

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive Vue component unit tests for namespace management modules

The namespaces module has multiple Vue components (delete-btn.vue, details.vue, edit-form.vue, info.vue, new-form.vue, panel.vue, table-row.vue, table.vue, transfer-modal.vue) but no visible test files in the repo structure. These are critical UI components handling namespace CRUD operations and permissions. Adding Jest/Vue Test Utils tests would improve reliability and serve as examples for other Vue components in the dashboard and admin modules.

  • [ ] Create spec/javascripts/modules/namespaces/components/ directory structure
  • [ ] Add unit tests for app/assets/javascripts/modules/namespaces/components/transfer-modal.vue (complex component with modal logic)
  • [ ] Add unit tests for app/assets/javascripts/modules/namespaces/components/edit-form.vue and new-form.vue (form validation)
  • [ ] Add unit tests for app/assets/javascripts/modules/namespaces/components/delete-btn.vue (destructive action confirmation)
  • [ ] Update .rspec or add jest.config.js configuration if not present
  • [ ] Document testing approach in CONTRIBUTING.md

Add E2E tests for Docker registry authentication flow using Portus

Portus is an authorization service for Docker Registry v2, but there are no visible E2E test files that verify the critical authentication/token flow between a real Docker client, the registry, and Portus. This is the core functionality of the project. Adding Cypress or similar E2E tests would catch regressions in this critical path and provide documentation for how the system actually works.

  • [ ] Create spec/e2e/ or e2e/ directory for end-to-end tests
  • [ ] Add E2E test for Docker login flow: client authentication → Portus token generation → Registry access
  • [ ] Add E2E test for namespace access control: verify unauthorized users cannot pull/push to protected namespaces
  • [ ] Add E2E test for team-based repository permissions if applicable
  • [ ] Update .travis.yml or GitHub Actions workflow to run E2E tests against containerized registry + Portus
  • [ ] Document E2E test setup in CONTRIBUTING.md

Refactor and add tests for admin registries service layer with API integration tests

The app/assets/javascripts/modules/admin/registries/service.js file handles communication with the registries API, but there are no visible API integration tests. This service is responsible for managing multiple Docker registry connections, which is critical infrastructure code. Integration tests would ensure API contracts are maintained and provide examples for testing other services.

  • [ ] Create spec/javascripts/modules/admin/registries/ directory
  • [ ] Add integration tests for app/assets/javascripts/modules/admin/registries/service.js covering all CRUD operations
  • [ ] Mock API responses and verify correct HTTP methods, headers, and payload structures
  • [ ] Add tests for error handling and edge cases (registry connection failures, invalid credentials)
  • [ ] Verify tests for the corresponding Rails API endpoints in app/controllers/admin/registries_controller.rb (if present)
  • [ ] Document API contract expectations in CONTRIBUTING.md

🌿Good first issues

  1. Add missing unit tests for app/assets/javascripts/modules/dashboard/components/search.js and tabbed_widget.js (no .spec.js files visible for these components). 2. Document the token generation flow in CONTRIBUTING.md—README references 'token based authentication system' but implementation details are sparse. 3. Create example .env.sample from actual .env keys used by Registry sync and LDAP features for easier onboarding (currently .env listed but not shown).

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 2843437 — Added missing changelog entries (mssola)
  • b87d37e — Merge pull request #2228 from dleborgne/master (mssola)
  • 73cd3b7 — Fixed email field placeholder (dleborgne)
  • a1b9f2e — Merge pull request #2189 from mssola/update-packaging-templates (mssola)
  • b3bd7ea — packaging/suse: updated the templates for the release script (mssola)
  • ce3ce89 — Updated gems (mssola)
  • 165f6a9 — Merge pull request #2185 from mssola/fix-packaging (mssola)
  • c343aef — packaging: fixed spec file (mssola)
  • 3de71a6 — ci: use a more specific ruby version (mssola)
  • 39c4028 — Merge pull request #2183 from mssola/fixed-travis-ruby-and-obs-credentials (mssola)

🔒Security observations

  • High · Outdated Ruby Version in Docker Image — Dockerfile (FROM opensuse/ruby:2.6). The Dockerfile uses Ruby 2.6, which reached end-of-life on March 31, 2022. This version no longer receives security patches, making the application vulnerable to known Ruby vulnerabilities that have been disclosed and not patched. Fix: Upgrade to Ruby 3.0 or later. Update the base image to a more recent version that includes security patches: 'FROM opensuse/ruby:3.1' or newer.
  • High · Outdated OpenSUSE Base Image — Dockerfile (zypper addrepo commands). The Dockerfile references openSUSE_Leap_15.0, which is outdated (released 2019). Repository references point to outdated and potentially unmaintained package sources that may contain unpatched vulnerabilities. Fix: Update to openSUSE_Leap_15.5 or 15.6. Update all repository URLs to current supported versions and verify package availability before deployment.
  • High · Incomplete Dependency Installation in Docker Build — Dockerfile (RUN zypper -n in command). The RUN command in the Dockerfile appears to be truncated ('libmariadb-devel postg'). This incomplete command may cause build failures or missing critical dependencies for database connectivity, particularly for PostgreSQL. Fix: Complete the package installation list. Ensure 'postgresql-devel' is fully specified and all required build dependencies are properly listed.
  • High · Potential Environment Variable Exposure — .env file. The presence of a .env file in the repository root suggests environment variables may be tracked in version control. If this contains secrets, credentials could be exposed to anyone with repository access. Fix: Add .env to .gitignore immediately. Never commit .env files with secrets. Use secure secret management: environment variables injected at runtime, or secrets management tools like HashiCorp Vault.
  • Medium · Exposed Port 3000 Without Security Context — Dockerfile (EXPOSE 3000). Port 3000 is exposed in the Docker image without documented security measures, TLS configuration, or network policy definition. This could allow unauthorized direct access to the application. Fix: Document required network policies and TLS termination setup. Ensure a reverse proxy (nginx/haproxy) with TLS is deployed in front of this service. Consider documenting security requirements in CONTRIBUTING.md.
  • Medium · GPG Key Auto-import Without Verification — Dockerfile (zypper --gpg-auto-import-keys ref). The Dockerfile uses 'zypper --gpg-auto-import-keys ref' which automatically imports GPG keys without manual verification. This increases the risk of man-in-the-middle attacks or compromised packages. Fix: Remove the --gpg-auto-import-keys flag and explicitly import and verify required GPG keys. Use specific key fingerprints for verification.
  • Medium · Potential XSS Vulnerability Surface — app/assets/javascripts/modules/ (especially comments/form.vue, search.js, result.vue). The codebase contains Vue.js components (.vue files) rendering user-generated content. Vue components in modules like comments, search results, and repository details may be vulnerable to XSS if input sanitization is not properly implemented throughout. Fix: Audit all Vue component templates for proper use of Vue's safe interpolation ({{ }}) vs v-html. Implement Content Security Policy headers. Add input validation and sanitization libraries (e.g., DOMPurify).
  • Medium · Missing Security Headers Documentation — Application configuration (not visible in provided structure). No evidence of security headers configuration (HSTS, CSP, X-Frame-Options, etc.) in the visible configuration files. An authorization service handling sensitive Docker registry operations should enforce strict security headers. Fix: Implement security headers in the Rails application: X-Content-Type-Options, X-Frame-Options: DENY, Strict-Transport-Security, Content-Security-Policy. Add documentation to CONTRIBUTING.md.
  • Low · Potential Information Disclosure via Error Pages — undefined. The presence of error images (portus-error Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · SUSE/Portus — RepoPilot