datahenge-cairn 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datahenge_cairn-0.1.0/.gitignore +34 -0
- datahenge_cairn-0.1.0/.ventwig.lock +4 -0
- datahenge_cairn-0.1.0/ABOUT_GHCR.md +383 -0
- datahenge_cairn-0.1.0/ABOUT_REGISTRIES.md +176 -0
- datahenge_cairn-0.1.0/CLAUDE.md +82 -0
- datahenge_cairn-0.1.0/High Level Motivations and Workflows.md +225 -0
- datahenge_cairn-0.1.0/LICENSE +21 -0
- datahenge_cairn-0.1.0/PKG-INFO +301 -0
- datahenge_cairn-0.1.0/README.md +266 -0
- datahenge_cairn-0.1.0/docs/00-project-scope.md +73 -0
- datahenge_cairn-0.1.0/docs/01-decisions-closed.md +1200 -0
- datahenge_cairn-0.1.0/docs/02-decisions-open.md +119 -0
- datahenge_cairn-0.1.0/docs/03-discussion-log.md +554 -0
- datahenge_cairn-0.1.0/docs/04-lessons-learned.md +394 -0
- datahenge_cairn-0.1.0/docs/CHANGELOG.md +838 -0
- datahenge_cairn-0.1.0/docs/plans/next-steps.md +229 -0
- datahenge_cairn-0.1.0/docs/plans/phase-1-build.md +219 -0
- datahenge_cairn-0.1.0/docs/requirements/00-overview.md +49 -0
- datahenge_cairn-0.1.0/docs/requirements/01-vendoring.md +58 -0
- datahenge_cairn-0.1.0/docs/requirements/02-build.md +166 -0
- datahenge_cairn-0.1.0/docs/requirements/03-deploy.md +198 -0
- datahenge_cairn-0.1.0/docs/requirements/04-data.md +54 -0
- datahenge_cairn-0.1.0/docs/requirements/05-config.md +128 -0
- datahenge_cairn-0.1.0/docs/requirements/06-cli.md +215 -0
- datahenge_cairn-0.1.0/pyproject.toml +92 -0
- datahenge_cairn-0.1.0/src/cairn/__init__.py +18 -0
- datahenge_cairn-0.1.0/src/cairn/__main__.py +8 -0
- datahenge_cairn-0.1.0/src/cairn/adopt.py +459 -0
- datahenge_cairn-0.1.0/src/cairn/appsjson.py +73 -0
- datahenge_cairn-0.1.0/src/cairn/build.py +407 -0
- datahenge_cairn-0.1.0/src/cairn/cli.py +938 -0
- datahenge_cairn-0.1.0/src/cairn/config.py +486 -0
- datahenge_cairn-0.1.0/src/cairn/descriptor.py +219 -0
- datahenge_cairn-0.1.0/src/cairn/doctor.py +340 -0
- datahenge_cairn-0.1.0/src/cairn/engine.py +169 -0
- datahenge_cairn-0.1.0/src/cairn/environments.py +263 -0
- datahenge_cairn-0.1.0/src/cairn/errors.py +96 -0
- datahenge_cairn-0.1.0/src/cairn/images.py +533 -0
- datahenge_cairn-0.1.0/src/cairn/project.py +63 -0
- datahenge_cairn-0.1.0/src/cairn/provision.py +849 -0
- datahenge_cairn-0.1.0/src/cairn/prune.py +149 -0
- datahenge_cairn-0.1.0/src/cairn/push.py +73 -0
- datahenge_cairn-0.1.0/src/cairn/py.typed +0 -0
- datahenge_cairn-0.1.0/src/cairn/reconcile.py +458 -0
- datahenge_cairn-0.1.0/src/cairn/registry.py +515 -0
- datahenge_cairn-0.1.0/src/cairn/resolve.py +229 -0
- datahenge_cairn-0.1.0/src/cairn/systemd.py +151 -0
- datahenge_cairn-0.1.0/src/cairn/tagging.py +135 -0
- datahenge_cairn-0.1.0/src/cairn/timing.py +93 -0
- datahenge_cairn-0.1.0/src/cairn/transcript.py +230 -0
- datahenge_cairn-0.1.0/src/cairn/vendor.py +297 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.dockerignore +4 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.editorconfig +19 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/ISSUE_TEMPLATE/question-about-using-frappe_docker.md +12 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/dependabot.yml +26 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/scripts/get_latest_tags.py +88 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/scripts/update_example_env.py +28 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/scripts/update_pwd.py +30 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/app-build-image.yml +189 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/build_develop.yml +12 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/build_stable.yml +12 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/core-build-bench.yml +59 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/core-build-develop.yml +51 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/core-build-stable.yml +148 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/core-build-test-images.yml +108 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/core-publish-images.yml +92 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/docs-publish-site.yml +66 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/lint.yml +35 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/pre-commit-autoupdate.yml +26 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.github/workflows/stale.yml +19 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.gitignore +34 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.pre-commit-config.yaml +57 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.shellcheckrc +1 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/.vscode/extensions.json +9 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/CODE_OF_CONDUCT.md +76 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/CONTRIBUTING.md +146 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/LICENSE +21 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/MAINTAINERS.md +25 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/README.md +117 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/compose.yaml +100 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/devcontainer-example/devcontainer.json +53 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/devcontainer-example/docker-compose.yml +88 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/development/apps-example.json +6 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/development/installer.py +245 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/development/vscode-example/launch.json +77 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/development/vscode-example/settings.json +20 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/development/vscode-example/tasks.json +22 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docker-bake.hcl +117 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/.vitepress/config.mts +29 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/00-introduction.md +92 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/01-choosing-a-deployment-method.md +126 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/02-docker-immutability.md +52 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/03-arm64.md +235 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/04-single-compose-setup.md +42 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/01-getting-started/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/01-overview.md +53 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/02-build-setup.md +153 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/03-start-setup.md +66 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/04-env-variables.md +160 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/05-overrides.md +35 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/06-setup-examples.md +142 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/07-single-server-example.md +298 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/08-single-server-nginxproxy-example.md +173 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/02-setup/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/01-tls-ssl-setup.md +57 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/02-backup-strategy.md +62 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/03-multi-tenancy.md +73 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/04-nginx-proxy-acme-companion.md +86 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/05-caddy-https.md +48 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/06-automated-builds-and-deployment.md +147 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/03-production/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/04-operations/01-site-operations.md +85 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/04-operations/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/05-development/01-development.md +446 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/05-development/02-debugging.md +20 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/05-development/03-local-services-connection.md +17 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/05-development/04-alternate-setup.md +254 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/05-development/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/06-migration/01-migrate-from-multi-image-setup.md +127 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/06-migration/02-traefik-v3-migration.md +83 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/06-migration/03-postgres-major-version-upgrade.md +49 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/06-migration/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/07-troubleshooting/01-troubleshoot.md +84 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/07-troubleshooting/02-windows-nginx-entrypoint-error.md +16 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/07-troubleshooting/03-arm64-apple-silicon.md +10 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/07-troubleshooting/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/01-build-version-10-images.md +20 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/02-configuring-vitepress.md +45 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/03-fork-management.md +161 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/04-framework-comparisons.md +163 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/05-external-links.md +18 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/06-github-actions-image-workflows.md +312 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/07-how-assets-are-handled.md +62 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/08-reference/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/09-concepts/01-custom-app.md +64 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/09-concepts/02-docker-bind-mounts.md +62 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/09-concepts/index.md +3 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/getting-started.md +978 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/images/Docker Desktop Screenshot - Resources section.png +0 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/images/Docker Manual Screenshot - Resources section.png +0 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/index.md +26 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/package.json +19 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/pnpm-lock.yaml +1759 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/public/favicon.png +0 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/docs/public/frappe-docker.png +0 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/example.env +76 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/images/bench/Dockerfile +170 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/images/custom/Containerfile +174 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/images/layered/Containerfile +58 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/images/production/Containerfile +165 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/install_x11_deps.sh +104 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.backup-cron.yaml +15 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.custom-domain-ssl.yaml +5 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.custom-domain.yaml +29 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.https.yaml +32 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.mariadb-secrets.yaml +11 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.mariadb-shared.yaml +30 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.mariadb.yaml +30 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.migrator.yaml +44 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.multi-bench-ssl.yaml +14 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.multi-bench.yaml +52 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.nginxproxy-ssl.yaml +28 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.nginxproxy.yaml +21 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.noproxy.yaml +4 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.postgres.yaml +23 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.proxy.yaml +20 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.redis.yaml +21 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.traefik-ssl.yaml +48 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/overrides/compose.traefik.yaml +45 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/pwd.yml +224 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/requirements-test.txt +1 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/resources/core/main-entrypoint.sh +12 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/resources/core/nginx/nginx-entrypoint.sh +52 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/resources/core/nginx/nginx-template.conf +113 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/resources/core/nginx/security_headers.conf +5 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/resources/core/start.sh +20 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/setup.cfg +12 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/__init__.py +0 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/_check_connections.py +52 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/_check_website_theme.py +17 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/_create_bucket.py +19 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/_ping_frappe_connections.py +26 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/compose.ci.yaml +21 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/conftest.py +168 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/test_frappe_docker.py +175 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker/tests/utils.py +99 -0
- datahenge_cairn-0.1.0/src/cairn/vendored/frappe_docker.pin.toml +5 -0
- datahenge_cairn-0.1.0/tests/test_adopt.py +398 -0
- datahenge_cairn-0.1.0/tests/test_appsjson.py +93 -0
- datahenge_cairn-0.1.0/tests/test_build.py +520 -0
- datahenge_cairn-0.1.0/tests/test_cli.py +1181 -0
- datahenge_cairn-0.1.0/tests/test_config.py +474 -0
- datahenge_cairn-0.1.0/tests/test_conventions.py +91 -0
- datahenge_cairn-0.1.0/tests/test_descriptor.py +164 -0
- datahenge_cairn-0.1.0/tests/test_doctor.py +512 -0
- datahenge_cairn-0.1.0/tests/test_engine.py +165 -0
- datahenge_cairn-0.1.0/tests/test_environments.py +304 -0
- datahenge_cairn-0.1.0/tests/test_images.py +394 -0
- datahenge_cairn-0.1.0/tests/test_project.py +45 -0
- datahenge_cairn-0.1.0/tests/test_provision.py +671 -0
- datahenge_cairn-0.1.0/tests/test_prune.py +210 -0
- datahenge_cairn-0.1.0/tests/test_push.py +95 -0
- datahenge_cairn-0.1.0/tests/test_reconcile.py +383 -0
- datahenge_cairn-0.1.0/tests/test_registry.py +479 -0
- datahenge_cairn-0.1.0/tests/test_resolve.py +226 -0
- datahenge_cairn-0.1.0/tests/test_systemd.py +120 -0
- datahenge_cairn-0.1.0/tests/test_tagging.py +212 -0
- datahenge_cairn-0.1.0/tests/test_timing.py +96 -0
- datahenge_cairn-0.1.0/tests/test_transcript.py +178 -0
- datahenge_cairn-0.1.0/tests/test_vendor.py +230 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# NOTE: src/cairn/vendored/frappe_docker/ is intentionally NOT ignored — it is the
|
|
2
|
+
# ventwig-managed, pinned copy of upstream, committed as plain files (D-007).
|
|
3
|
+
|
|
4
|
+
# Local scratch deployments used to exercise the CLI end-to-end (manifest discovery,
|
|
5
|
+
# build config layering). Real deployments live outside this repo (ADR-029).
|
|
6
|
+
/deployments/
|
|
7
|
+
|
|
8
|
+
# Build/deploy artifacts
|
|
9
|
+
apps.json
|
|
10
|
+
compose.custom.yaml
|
|
11
|
+
*.env
|
|
12
|
+
!example.env
|
|
13
|
+
|
|
14
|
+
# Python
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
dist/
|
|
21
|
+
build/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
.coverage.*
|
|
27
|
+
htmlcov/
|
|
28
|
+
|
|
29
|
+
# OS / editor
|
|
30
|
+
.DS_Store
|
|
31
|
+
*.swp
|
|
32
|
+
|
|
33
|
+
# Claude Code local session/plan state
|
|
34
|
+
.claude/
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# About GHCR — GitHub Container Registry
|
|
2
|
+
|
|
3
|
+
Written for someone who has used GitHub for years but has never pushed a container image to
|
|
4
|
+
it. This explains what you are actually signing into, who ends up owning what, and the handful
|
|
5
|
+
of sharp edges that are genuinely surprising.
|
|
6
|
+
|
|
7
|
+
> ### Read [ABOUT_REGISTRIES.md](ABOUT_REGISTRIES.md) first
|
|
8
|
+
>
|
|
9
|
+
> **GHCR is not cairn's recommended default.** It is one option, and it is the weakest of them
|
|
10
|
+
> on cost: GitHub Packages prices multi-gigabyte artifacts poorly, and an ERPNext image is
|
|
11
|
+
> roughly 2.75 GB with no cheap incremental layer (see §6).
|
|
12
|
+
>
|
|
13
|
+
> This document is most useful for **your own projects**, or for a client already committed to
|
|
14
|
+
> GitHub. For client work generally, the ownership and least-privilege rules in
|
|
15
|
+
> `ABOUT_REGISTRIES.md` come first, and a client-owned cloud registry usually wins.
|
|
16
|
+
|
|
17
|
+
cairn is **registry-agnostic** — nothing here is required. Set `[cairn.registry]` in the
|
|
18
|
+
deployment's `cairn.toml` and the rest of cairn behaves identically against any registry.
|
|
19
|
+
|
|
20
|
+
> **Verify the pricing and token details against GitHub's own documentation before
|
|
21
|
+
> committing money or credentials.** The mechanics below are stable; the numbers and the
|
|
22
|
+
> fine-grained-token story are the parts GitHub changes.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. What GHCR actually is
|
|
27
|
+
|
|
28
|
+
`ghcr.io` is a container registry — a web service that stores container images and hands them
|
|
29
|
+
out. It speaks the same standard protocol as Docker Hub, so `podman`, `docker`, and cairn all
|
|
30
|
+
talk to it the same way.
|
|
31
|
+
|
|
32
|
+
It is one component of a larger feature called **GitHub Packages**, which also stores npm,
|
|
33
|
+
NuGet, Maven, and RubyGems artifacts. Those share your account's storage quota with GHCR but
|
|
34
|
+
are otherwise unrelated. When you read GitHub's docs, "Packages" is the umbrella and
|
|
35
|
+
"Container registry" is the part you want.
|
|
36
|
+
|
|
37
|
+
### "Package" — GitHub's word for a Docker concept
|
|
38
|
+
|
|
39
|
+
GitHub's docs and UI say **package** where you would say *repository*, and **version** where you
|
|
40
|
+
would say *image*. The vocabulary is generic because the same permissions, UI, and API cover npm
|
|
41
|
+
tarballs and Maven jars too. Translated:
|
|
42
|
+
|
|
43
|
+
| Docker / OCI | GHCR calls it | Example |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| repository | a **package** (of type `container`) | `ghcr.io/acme-corp/erpnext-acme` |
|
|
46
|
+
| image / manifest — one digest | a **package version** | `sha256:1b019793…` |
|
|
47
|
+
| tag | a **tag** on a version | `:production`, `:v16-1b019793dc20` |
|
|
48
|
+
|
|
49
|
+
So a "package" is not vague: for containers it is **exactly one image repository** — the name,
|
|
50
|
+
plus every version and tag beneath it. Two things follow that matter later in this document:
|
|
51
|
+
|
|
52
|
+
- **Permissions are per package, meaning per image repository.** "Write access to the
|
|
53
|
+
`erpnext-acme` package" grants exactly that one repository and nothing else in the account
|
|
54
|
+
(§3).
|
|
55
|
+
- **Deletion operates on a version, not a tag.** That is *why* deleting takes every tag on that
|
|
56
|
+
image with it: `production`, `v16`, and `v16-1b019793dc20` are three tags on one version (§8).
|
|
57
|
+
|
|
58
|
+
One side effect of the shared umbrella: the storage quota covers **all** package types in the
|
|
59
|
+
account, so a client's npm packages and your container images draw from the same allowance.
|
|
60
|
+
|
|
61
|
+
An image lives at a path with three parts:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ghcr.io/datahenge/erpnext-btu-v16:production
|
|
65
|
+
└─┬───┘ └───┬────┘ └──────┬──────┘ └───┬────┘
|
|
66
|
+
registry owner image name tag
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **owner** — a GitHub user or organization. Yours.
|
|
70
|
+
- **image name** — whatever you choose. cairn takes it from `image_name` in `cairn.toml`.
|
|
71
|
+
- **tag** — a movable label. cairn writes several per image; see §7.
|
|
72
|
+
|
|
73
|
+
**Both the owner and the image name must be lowercase.** This is a registry rule, not a
|
|
74
|
+
GitHub one, and it is the first thing that bites people: the organization displayed as
|
|
75
|
+
`Datahenge` is addressed as `datahenge`.
|
|
76
|
+
|
|
77
|
+
## 2. What you are logging into
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
podman login ghcr.io
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It prompts for a username and a password. What it wants:
|
|
84
|
+
|
|
85
|
+
- **Username** — your GitHub username. Not the organization name, even when you are pushing
|
|
86
|
+
to the organization. You authenticate *as yourself*; authorization is separate.
|
|
87
|
+
- **Password** — **not your GitHub password.** GHCR will not accept it. It wants a
|
|
88
|
+
**Personal Access Token**, which is a long generated string you create in GitHub's
|
|
89
|
+
settings and treat as a password.
|
|
90
|
+
|
|
91
|
+
Use a **classic** personal access token. GitHub has two kinds — "classic" and "fine-grained"
|
|
92
|
+
— and the container registry's support for fine-grained tokens has historically lagged.
|
|
93
|
+
Classic is the documented, reliable path. If you prefer fine-grained, check GitHub's current
|
|
94
|
+
docs first rather than assuming.
|
|
95
|
+
|
|
96
|
+
Create one at **Settings → Developer settings → Personal access tokens → Tokens (classic)**.
|
|
97
|
+
The scopes (checkboxes) that matter:
|
|
98
|
+
|
|
99
|
+
| Scope | Grants | Who needs it |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `read:packages` | Pull images | **The VPS** |
|
|
102
|
+
| `write:packages` | Push images (includes read) | **Your build machine** |
|
|
103
|
+
| `delete:packages` | Delete image versions | Nobody, ideally — see §8 |
|
|
104
|
+
|
|
105
|
+
If a package is linked to a **private** repository, a classic token may also need the `repo`
|
|
106
|
+
scope to read it. If a pull fails with a permission error despite `read:packages`, that is the
|
|
107
|
+
first thing to try.
|
|
108
|
+
|
|
109
|
+
**Make two separate tokens.** The build machine gets `write:packages`; the VPS gets
|
|
110
|
+
`read:packages` and nothing else. This is not ceremony — it is the entire mechanism by which
|
|
111
|
+
a compromised VPS cannot overwrite your production image. cairn's design leans on it: the
|
|
112
|
+
roles are separated by *credentials*, not by shipping different code to each machine.
|
|
113
|
+
|
|
114
|
+
Give the tokens an expiry you will actually notice, and put them somewhere you can find them
|
|
115
|
+
again. GitHub shows a token exactly once.
|
|
116
|
+
|
|
117
|
+
## 3. How this relates to your GitHub account and repos
|
|
118
|
+
|
|
119
|
+
This is the part that surprises people, so plainly:
|
|
120
|
+
|
|
121
|
+
**A package is owned by an account, not by a repository.** When you push
|
|
122
|
+
`ghcr.io/datahenge/erpnext-btu-v16`, GHCR creates a *package* belonging to the `datahenge`
|
|
123
|
+
organization. It exists whether or not any repository is involved. It did not come from a
|
|
124
|
+
repo and it is not inside one.
|
|
125
|
+
|
|
126
|
+
You can optionally **link** a package to a repository afterwards. Linking:
|
|
127
|
+
|
|
128
|
+
- makes the package appear in that repo's sidebar, so people find it;
|
|
129
|
+
- lets GitHub Actions *in that repo* push to it using the automatic `GITHUB_TOKEN`, with no
|
|
130
|
+
personal token at all;
|
|
131
|
+
- can make the package inherit the repo's access permissions, so your collaborators get
|
|
132
|
+
access without being granted it package-by-package.
|
|
133
|
+
|
|
134
|
+
Two ways to link. Manually, in the package's settings page. Or automatically, by stamping the
|
|
135
|
+
image with a label naming the repository:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
org.opencontainers.image.source = https://github.com/Datahenge/cairn
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
GHCR reads that label on push and links the package for you. **cairn does not currently stamp
|
|
142
|
+
this label** — it stamps creation time, title, version, and revision, but not source. That is
|
|
143
|
+
a deliberate gap rather than an oversight: adding it means deciding *which* repository an
|
|
144
|
+
image points at (the deployment's, or the tool's), which is a design question and not a
|
|
145
|
+
detail. Until it is decided, link manually if you want the linkage.
|
|
146
|
+
|
|
147
|
+
**Repos you merely participate in are irrelevant here.** Being a contributor to someone
|
|
148
|
+
else's project grants you nothing on their packages, and your packages are invisible to them
|
|
149
|
+
unless you make them public or grant access. Package permissions are their own system.
|
|
150
|
+
|
|
151
|
+
### How narrow can access be? Narrower than it looks
|
|
152
|
+
|
|
153
|
+
The question that matters if you are pushing into a *client's* organization: does write access
|
|
154
|
+
mean you can overwrite all hundred of their packages? **No.**
|
|
155
|
+
|
|
156
|
+
- `write:packages` on a token is a **ceiling on what the token may attempt**, not a grant of
|
|
157
|
+
what you may touch. Authorization is resolved per package, every time.
|
|
158
|
+
- Each package has its own access list with **Read / Write / Admin** roles, granted to a user
|
|
159
|
+
or a team, **per package**.
|
|
160
|
+
- A package **linked to a repository inherits that repository's permissions**. So the per-repo
|
|
161
|
+
model you are used to applies to images: link the ERPNext image to the repo you already have
|
|
162
|
+
write on, and your image access is exactly your repo access — nothing more.
|
|
163
|
+
- **Plain org membership grants nothing** on existing private packages. They are invisible.
|
|
164
|
+
- **A typo cannot clobber anything.** Pushing to a misspelled name either creates a *new*
|
|
165
|
+
package, or is denied if that name exists and you lack Write on it. There is no path where a
|
|
166
|
+
mistyped push overwrites a package you were never granted.
|
|
167
|
+
|
|
168
|
+
The configuration that *would* be dangerous is being made an **organization owner**, or being
|
|
169
|
+
put in a team with admin over all packages. Do not ask for that, and decline it if offered —
|
|
170
|
+
see rule 2 in `ABOUT_REGISTRIES.md`. Ask for write on one package, or on the one repository it
|
|
171
|
+
is linked to.
|
|
172
|
+
|
|
173
|
+
## 4. Who owns the images after deployment
|
|
174
|
+
|
|
175
|
+
The GitHub account that pushed them — in your case the `datahenge` organization. Pulling an
|
|
176
|
+
image to a VPS does not transfer anything; the VPS holds a *copy*, and the registry keeps the
|
|
177
|
+
original.
|
|
178
|
+
|
|
179
|
+
The practical consequences are worth stating because they are the kind of thing discovered at
|
|
180
|
+
a bad moment:
|
|
181
|
+
|
|
182
|
+
- **If the package is deleted, targets can no longer pull it.** Already-running containers
|
|
183
|
+
keep running — they use the local copy — but a fresh host, or one whose local image has been
|
|
184
|
+
pruned, cannot deploy. cairn's rollback model depends on old images still being in the
|
|
185
|
+
registry.
|
|
186
|
+
- **If the organization is deleted or renamed, every image path changes.** The registry path
|
|
187
|
+
contains the owner name.
|
|
188
|
+
- **If you pushed to your own namespace, your client does not own their image.** An image
|
|
189
|
+
sitting in *your* organization is a dependency they have on you: end the relationship badly
|
|
190
|
+
and they cannot deploy or roll back software they own. This is a rule, not a caveat —
|
|
191
|
+
`ABOUT_REGISTRIES.md` rule 1, and a requirement in `docs/requirements/05-config.md`. Push to
|
|
192
|
+
an account **they** own: set `[cairn.registry]` in their deployment's `cairn.toml` and
|
|
193
|
+
nothing else about cairn changes.
|
|
194
|
+
- **Anyone with `read:packages` on a private package can pull the whole image**, which
|
|
195
|
+
contains your application code. Treat pull tokens as code access, because that is what they
|
|
196
|
+
are.
|
|
197
|
+
|
|
198
|
+
## 5. Visibility: private by default
|
|
199
|
+
|
|
200
|
+
A newly pushed package is **private**. Nobody but you (and accounts you grant) can pull it.
|
|
201
|
+
|
|
202
|
+
You can change a package to **public** in its settings, and then *anyone on the internet* can
|
|
203
|
+
pull it anonymously, with no token. For a proprietary ERPNext build that is almost certainly
|
|
204
|
+
wrong — the image contains your custom app source.
|
|
205
|
+
|
|
206
|
+
Two facts about public that are easy to miss:
|
|
207
|
+
|
|
208
|
+
- Public packages are **free**, in both storage and bandwidth. Private ones are not. See §6.
|
|
209
|
+
- A public image version that has been downloaded more than a few thousand times **cannot be
|
|
210
|
+
deleted at all.** See §8.
|
|
211
|
+
|
|
212
|
+
## 6. What it costs — read this before you push several images
|
|
213
|
+
|
|
214
|
+
GHCR is free for public packages. For **private** packages, storage and outbound data
|
|
215
|
+
transfer count against your plan's GitHub Packages allowance, and you are billed per gigabyte
|
|
216
|
+
beyond it.
|
|
217
|
+
|
|
218
|
+
**This matters more for ERPNext than for most projects, because the images are large.** A
|
|
219
|
+
custom Frappe + ERPNext image is roughly **2.75 GB**. The allowances included with the
|
|
220
|
+
personal and small-team plans are on the order of a couple of gigabytes of storage and ten
|
|
221
|
+
gigabytes of monthly transfer — meaning **one single image can exceed your entire included
|
|
222
|
+
storage**, and one deployment pull can consume a meaningful share of the monthly transfer.
|
|
223
|
+
|
|
224
|
+
Do the arithmetic for your own situation, with current prices from GitHub's billing docs, on
|
|
225
|
+
these three numbers:
|
|
226
|
+
|
|
227
|
+
1. **Image size** × **how many versions you keep** = storage. Rollback headroom is not free;
|
|
228
|
+
keeping five versions of a 2.75 GB image is about 14 GB.
|
|
229
|
+
2. **Image size** × **deploys per month** = outbound transfer. Every `cairn reconcile` that
|
|
230
|
+
actually converges pulls an image.
|
|
231
|
+
3. Transfer *into* GitHub Actions is free. Transfer to your VPS is not.
|
|
232
|
+
|
|
233
|
+
Two things that make this better or worse than it looks:
|
|
234
|
+
|
|
235
|
+
- **Better:** layers are shared. Two images differing only in an upper layer store the common
|
|
236
|
+
layers once, and a pull only fetches what the host lacks.
|
|
237
|
+
- **Worse, and specific to this stack:** the upstream build recipe installs Frappe and every
|
|
238
|
+
app in a *single* step, so changing one line in one custom app rebuilds that entire step.
|
|
239
|
+
The result is a new multi-gigabyte layer rather than a small one — so in practice a
|
|
240
|
+
custom-app change costs close to a full image in both storage and transfer. This is a known
|
|
241
|
+
characteristic of the upstream recipe, recorded in cairn's design notes as one of the
|
|
242
|
+
standing arguments for eventually maintaining our own build recipe. It now has a cost in
|
|
243
|
+
money as well as in build minutes.
|
|
244
|
+
|
|
245
|
+
If the bill turns out to be the deciding factor, the alternatives are a registry with cheaper
|
|
246
|
+
egress, or one you host yourself. cairn does not care which you choose.
|
|
247
|
+
|
|
248
|
+
## 7. What cairn puts in the registry
|
|
249
|
+
|
|
250
|
+
Per build, cairn writes **two** tags pointing at the same image:
|
|
251
|
+
|
|
252
|
+
- a **deterministic** tag like `v16-1b019793dc20`, where the trailing hash is derived from
|
|
253
|
+
every resolved input — the Frappe commit, each app's commit, and the build settings. The
|
|
254
|
+
same inputs always produce the same name.
|
|
255
|
+
- a **moving** tag like `v16`, which is repointed to the newest build.
|
|
256
|
+
|
|
257
|
+
Then, per environment, you point an **environment tag** at a chosen image:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
cairn new-tag production --latest
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`production` is now a third name for the same underlying image, and it is the name your VPS
|
|
264
|
+
watches. Moving that tag is what deploying, promoting, and rolling back all are — no rebuild,
|
|
265
|
+
no upload, just a new name written server-side.
|
|
266
|
+
|
|
267
|
+
So a single image commonly carries three or more tags at once, and `cairn images` folds them
|
|
268
|
+
together and reports them as one image, because that is what they are.
|
|
269
|
+
|
|
270
|
+
One thing to be clear about, because the word invites the wrong inference: cairn's
|
|
271
|
+
deterministic tag is **deterministic, not immutable.** Same inputs → same name. The name is
|
|
272
|
+
still a pointer, and a registry will happily move it. The thing that never changes is the
|
|
273
|
+
**digest** — the `sha256:…` value. That is the real identity of an image, and it is what
|
|
274
|
+
cairn's deploy machinery actually compares.
|
|
275
|
+
|
|
276
|
+
## 8. Deleting images — the genuinely sharp edge
|
|
277
|
+
|
|
278
|
+
GHCR's deletion model does not work the way you would guess, and cairn is shaped around it.
|
|
279
|
+
|
|
280
|
+
**There is no way to delete a single tag.** Deletion operates on a *version* — one underlying
|
|
281
|
+
image — and deleting a version removes **every tag pointing at it, and the image itself.**
|
|
282
|
+
|
|
283
|
+
Read that against §7. If `v16-1b019793dc20`, `v16`, and `production` all point at one image,
|
|
284
|
+
then "deleting the `v16` tag" is not an operation GHCR offers. Attempting to clean up a tag
|
|
285
|
+
name destroys the image your production environment is running.
|
|
286
|
+
|
|
287
|
+
Also: **a public version with more than a few thousand downloads cannot be deleted at all**,
|
|
288
|
+
by anyone, including you.
|
|
289
|
+
|
|
290
|
+
This is why `cairn retire <env>` deletes nothing. It tells you what to remove from your
|
|
291
|
+
manifest, and warns you that the registry tag will still exist and still resolve. And it is
|
|
292
|
+
why `cairn prune` operates only on your **build machine's** local images and never on the
|
|
293
|
+
registry. Registry-side cleanup is deliberately left as a manual, deliberate act.
|
|
294
|
+
|
|
295
|
+
If you do need to reclaim registry space, do it by hand in the package's **Manage versions**
|
|
296
|
+
page, deliberately, having first checked with `cairn images` that no environment tag points at
|
|
297
|
+
the version you are about to destroy.
|
|
298
|
+
|
|
299
|
+
## 9. First-time setup, start to finish
|
|
300
|
+
|
|
301
|
+
**On your build machine, once:**
|
|
302
|
+
|
|
303
|
+
1. Create a classic token with `write:packages`.
|
|
304
|
+
2. `podman login ghcr.io` — your GitHub username, then the token as the password.
|
|
305
|
+
3. Verify with a read: `cairn images`. It will report an empty repository rather than a
|
|
306
|
+
permission error.
|
|
307
|
+
|
|
308
|
+
The credential is stored by podman, not by cairn — normally in
|
|
309
|
+
`${XDG_RUNTIME_DIR}/containers/auth.json`. cairn only ever *reads* it. Note that
|
|
310
|
+
`XDG_RUNTIME_DIR` is on a tmpfs that is cleared at reboot, so a login there does not survive
|
|
311
|
+
one. If you want it to persist, `podman login` writes to `~/.config/containers/auth.json`
|
|
312
|
+
when the runtime directory is unavailable, or you can point it there explicitly.
|
|
313
|
+
|
|
314
|
+
**Then build and publish:**
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
cairn build --push
|
|
318
|
+
cairn images # confirm it arrived
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**On the VPS, once:**
|
|
322
|
+
|
|
323
|
+
1. Create a *second* classic token with only `read:packages`.
|
|
324
|
+
2. `docker login ghcr.io` with that one.
|
|
325
|
+
3. Nothing else. cairn on the target reads that credential through Docker and stores nothing
|
|
326
|
+
of its own.
|
|
327
|
+
|
|
328
|
+
**Then set visibility.** Go to the package's settings and confirm it is private. It will be,
|
|
329
|
+
but confirm it, because the consequence of being wrong is publishing your client's source
|
|
330
|
+
code.
|
|
331
|
+
|
|
332
|
+
## 10. Errors you will actually hit
|
|
333
|
+
|
|
334
|
+
**`the registry would not issue a read token (requested access to the resource is denied)`**
|
|
335
|
+
|
|
336
|
+
GHCR answers "you are not logged in", "this repository does not exist", and "this is private
|
|
337
|
+
and you cannot read it" with the *same* response, deliberately, so that guessing cannot reveal
|
|
338
|
+
which repositories exist. cairn's message lists all three causes because it genuinely cannot
|
|
339
|
+
tell them apart. Check them in that order — and check the spelling of the namespace, since a
|
|
340
|
+
typo is indistinguishable from a permissions problem.
|
|
341
|
+
|
|
342
|
+
**`unauthorized` / `denied` on push**
|
|
343
|
+
|
|
344
|
+
Your token has `read:packages` but not `write:packages`, or you are logged in as a different
|
|
345
|
+
account than you think. `podman login ghcr.io` again.
|
|
346
|
+
|
|
347
|
+
**`name unknown` or a 404 on a path you are sure exists**
|
|
348
|
+
|
|
349
|
+
Almost always capitalization. `ghcr.io/Datahenge/...` is not `ghcr.io/datahenge/...`.
|
|
350
|
+
|
|
351
|
+
**A pull fails on the VPS but works on your laptop**
|
|
352
|
+
|
|
353
|
+
The VPS's token is missing `read:packages`, or the package is linked to a private repository
|
|
354
|
+
and the token also needs `repo`, or the login was done as `root` and the pull is running as a
|
|
355
|
+
different user — Docker credentials are per-user.
|
|
356
|
+
|
|
357
|
+
## 11. What cairn does and does not do with your credentials
|
|
358
|
+
|
|
359
|
+
Stated plainly, because it is a reasonable thing to want to know about a tool you point at a
|
|
360
|
+
production registry:
|
|
361
|
+
|
|
362
|
+
- cairn **never** asks you for a password, stores one, writes one to disk, or puts one in a
|
|
363
|
+
log or a build transcript.
|
|
364
|
+
- cairn **reads** the credential file your `podman login` or `docker login` already created,
|
|
365
|
+
uses it for the duration of one command, and forgets it.
|
|
366
|
+
- cairn tries **unauthenticated** access first. For a public repository it never opens your
|
|
367
|
+
credential file at all.
|
|
368
|
+
- The build config that names your registry and namespace holds **no secrets** — just the
|
|
369
|
+
hostname and the owner — which is why it is safe to keep beside a deployment.
|
|
370
|
+
- On a target, the environment descriptor also holds no secrets. It names the *mechanism*
|
|
371
|
+
holding them and nothing more.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Further reading
|
|
376
|
+
|
|
377
|
+
- GitHub's own docs: *Working with the Container registry*, and *About billing for GitHub
|
|
378
|
+
Packages* — the authoritative source for tokens and pricing, both of which change.
|
|
379
|
+
- [`docs/requirements/03-deploy.md`](docs/requirements/03-deploy.md) — how cairn uses the
|
|
380
|
+
registry as the desired-state pointer, and the deletion constraints above stated as
|
|
381
|
+
requirements.
|
|
382
|
+
- [`docs/requirements/05-config.md`](docs/requirements/05-config.md) — where registry settings
|
|
383
|
+
live, and why credentials are the container engine's job and never cairn's.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Choosing a container registry
|
|
2
|
+
|
|
3
|
+
cairn builds an image and puts it somewhere; your servers pull it from there. That "somewhere"
|
|
4
|
+
is a **container registry**, and cairn works with any of them — it stores no credentials, hardwires
|
|
5
|
+
no hostname, and has no preference.
|
|
6
|
+
|
|
7
|
+
This document exists because *which* registry you pick is not a neutral technical choice when you
|
|
8
|
+
build software for clients. Two of the three rules below are about liability, not engineering.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The three rules
|
|
13
|
+
|
|
14
|
+
### 1. The image belongs in the account that owns the source
|
|
15
|
+
|
|
16
|
+
You already do this with code: the client's GitHub holds their app, and you have been granted
|
|
17
|
+
access to read it. **The image is a build artifact of that code and inherits the same ownership.**
|
|
18
|
+
|
|
19
|
+
The failure this prevents is specific. If you are the sole owner of a client's built image, then
|
|
20
|
+
the day the relationship ends badly, that client cannot deploy or roll back software **they own**.
|
|
21
|
+
Their operations depend on your goodwill. That is not a position to be in, in either direction —
|
|
22
|
+
it is bad for them, and it is an obligation you did not agree to carry.
|
|
23
|
+
|
|
24
|
+
So: the registry account is theirs. You are granted access to it. When the engagement ends, they
|
|
25
|
+
revoke your access and lose nothing.
|
|
26
|
+
|
|
27
|
+
Your own projects are the obvious exception. Your registry, your account, your bill.
|
|
28
|
+
|
|
29
|
+
### 2. Your credential should reach the engagement's images and nothing else
|
|
30
|
+
|
|
31
|
+
Ask for write access to **the ERPNext image, or the repository holding it** — not to the client's
|
|
32
|
+
registry as a whole.
|
|
33
|
+
|
|
34
|
+
The reason is not that you might act badly. It is that **you will eventually make a mistake**, and
|
|
35
|
+
the size of the mistake should be bounded by the credential. A token that can write exactly one
|
|
36
|
+
repository cannot destroy a client's other images no matter what you type. This protects you more
|
|
37
|
+
than it protects them: it means a bad afternoon cannot become a liability event.
|
|
38
|
+
|
|
39
|
+
A corollary worth stating: **do not accept broad access when it is offered.** A client who makes
|
|
40
|
+
you an administrator of their whole registry to "save time" has handed you a risk, not a
|
|
41
|
+
convenience. Ask to be narrowed.
|
|
42
|
+
|
|
43
|
+
### 3. Nobody's credentials live in cairn
|
|
44
|
+
|
|
45
|
+
cairn never asks for, stores, writes, or logs a password or token. You run `podman login` or
|
|
46
|
+
`docker login`; the container engine keeps the credential; cairn reads it when it needs to and
|
|
47
|
+
forgets it. What cairn stores is a hostname and an account name, neither of which is a secret.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What cairn needs from you
|
|
52
|
+
|
|
53
|
+
Two values, in the deployment's `cairn.toml`, committed:
|
|
54
|
+
|
|
55
|
+
```toml
|
|
56
|
+
[cairn.registry]
|
|
57
|
+
host = "ghcr.io"
|
|
58
|
+
namespace = "acme-corp" # the client's account — not yours
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
These are committed deliberately, so the client can take the deployment over and keep publishing
|
|
62
|
+
to their own registry without needing anything from your laptop. Machine-specific settings —
|
|
63
|
+
which container engine you build with, where transcripts go — stay in your own config and are
|
|
64
|
+
never committed.
|
|
65
|
+
|
|
66
|
+
Absent a `[cairn.registry]`, images stay local and cairn never guesses a registry.
|
|
67
|
+
|
|
68
|
+
To publish somewhere else temporarily without editing a client's file, a `cairn.local.toml` beside
|
|
69
|
+
the manifest overrides it.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## The options
|
|
74
|
+
|
|
75
|
+
Ordered by how well they satisfy rules 1 and 2. **Verify current pricing before committing** — it
|
|
76
|
+
changes, and the numbers below are indicative.
|
|
77
|
+
|
|
78
|
+
### Client-owned cloud registry — the default recommendation
|
|
79
|
+
|
|
80
|
+
AWS **ECR**, Google **Artifact Registry**, Azure **ACR**.
|
|
81
|
+
|
|
82
|
+
- **Ownership** — unambiguous. It is inside the client's cloud account, alongside everything else
|
|
83
|
+
of theirs.
|
|
84
|
+
- **Least privilege** — the strongest available. ECR scopes IAM policies to an individual
|
|
85
|
+
repository ARN; Artifact Registry grants a writer role on a single repository; ACR offers
|
|
86
|
+
repository-scoped tokens. You can hold a credential that can write `erpnext-acme` and literally
|
|
87
|
+
nothing else.
|
|
88
|
+
- **Cost** — the best fit by a wide margin at ERPNext image sizes. Storage is flat per GB with no
|
|
89
|
+
small included cap to blow through, and **egress is often free or trivial when the VPS is in the
|
|
90
|
+
same cloud and region** — which is the single biggest cost lever available, since every deploy
|
|
91
|
+
pulls a multi-gigabyte image.
|
|
92
|
+
- **Credential handling** — one login per registry hostname. Because the hostname *is* the
|
|
93
|
+
identity, there is no ambiguity about which client you are pushing to and no browser session
|
|
94
|
+
involved.
|
|
95
|
+
- **Cost to you** — the most setup. Each client needs a cloud account and someone to create the
|
|
96
|
+
repository and the scoped credential.
|
|
97
|
+
|
|
98
|
+
**Pick this when** the client already has a cloud account, or their VPS is already in one.
|
|
99
|
+
|
|
100
|
+
### Client-owned GitHub organization
|
|
101
|
+
|
|
102
|
+
- **Ownership** — good. The package belongs to their org.
|
|
103
|
+
- **Least privilege** — good, and better than it first appears: packages carry their own
|
|
104
|
+
Read/Write/Admin access list, and a package **linked to a repository inherits that repository's
|
|
105
|
+
permissions** — so the familiar per-repo model applies to images too. Link the image to the
|
|
106
|
+
ERPNext repo you already have write on, and your image access is exactly your repo access.
|
|
107
|
+
Plain org membership grants nothing on existing private packages, and a mistyped push either
|
|
108
|
+
creates a new package or is denied; it cannot overwrite one you were never granted. **Do not
|
|
109
|
+
accept org ownership** — that is the configuration that would make access effectively
|
|
110
|
+
boundless.
|
|
111
|
+
- **Credential handling** — the simplest of any option. **One** GitHub account and one token, for
|
|
112
|
+
every client. Authorization resolves server-side per organization. No second login, ever.
|
|
113
|
+
- **Cost** — **the weak point, and the reason this is not the default.** GitHub Packages prices
|
|
114
|
+
multi-gigabyte artifacts poorly: a small included allowance, then per-GB storage at roughly
|
|
115
|
+
2.5× a purpose-built registry, plus per-GB egress on every pull to a VPS. It is made worse by
|
|
116
|
+
the upstream build recipe having no per-app layer seam, so **every build is a fresh full-size
|
|
117
|
+
layer** rather than a cheap delta — layer sharing saves you almost nothing, and each retained
|
|
118
|
+
rollback version costs close to a whole image.
|
|
119
|
+
- See **[ABOUT_GHCR.md](ABOUT_GHCR.md)** for the mechanics: tokens, scopes, visibility, and the
|
|
120
|
+
deletion rule that is genuinely surprising.
|
|
121
|
+
|
|
122
|
+
**Pick this when** the client is already on GitHub, images are small or few, or the convenience of
|
|
123
|
+
one credential outweighs the bill.
|
|
124
|
+
|
|
125
|
+
### Registry on the client's own VPS
|
|
126
|
+
|
|
127
|
+
Run a registry container on the deploy target itself.
|
|
128
|
+
|
|
129
|
+
- **Ownership** — their hardware, their disk. Unambiguous.
|
|
130
|
+
- **Least privilege** — you have access to their server anyway, so this adds no new exposure.
|
|
131
|
+
- **Cost** — no registry bill at all, and pulls never leave the machine, so they are instant and
|
|
132
|
+
free. Attractive given cairn targets a single VPS.
|
|
133
|
+
- **The real cost** — your **rollback history shares a failure domain with the thing you would
|
|
134
|
+
roll back.** Lose the host and you lose every stored image with it. It also puts disk pressure
|
|
135
|
+
on the server that is running production.
|
|
136
|
+
|
|
137
|
+
**Pick this when** cost dominates and off-host rollback history is genuinely not needed — or
|
|
138
|
+
alongside another registry, not instead of one.
|
|
139
|
+
|
|
140
|
+
### Your own namespace
|
|
141
|
+
|
|
142
|
+
- Correct for **your own projects** and nothing else.
|
|
143
|
+
- For client work it fails rule 1 outright.
|
|
144
|
+
- It is defensible in exactly one business model, which is not cairn's assumption: if you were a
|
|
145
|
+
SaaS host and clients logged into *your* hosted ERPNext, the images would legitimately be yours
|
|
146
|
+
— while their source stayed in their own accounts with you granted read.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## What to ask a client for
|
|
151
|
+
|
|
152
|
+
Concretely, so the request is small and easy to grant:
|
|
153
|
+
|
|
154
|
+
> I need somewhere to publish the ERPNext container image I build for you. It should live in an
|
|
155
|
+
> account **you** own, so you are never dependent on me to deploy or roll back your own software.
|
|
156
|
+
>
|
|
157
|
+
> Please create one container repository named `erpnext-<yourname>` and grant me **write access to
|
|
158
|
+
> that repository only** — not to your registry as a whole. If anything goes wrong on my end, I
|
|
159
|
+
> want the damage to be limited to this one repository.
|
|
160
|
+
>
|
|
161
|
+
> Your servers will also need a **read-only** credential to pull it.
|
|
162
|
+
|
|
163
|
+
That last line matters: the deploy target gets a **pull-only** credential. It is what makes a
|
|
164
|
+
compromised server unable to overwrite the image it runs, and it is the same separation on every
|
|
165
|
+
registry.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Further reading
|
|
170
|
+
|
|
171
|
+
- **[ABOUT_GHCR.md](ABOUT_GHCR.md)** — GitHub's registry in detail, for your own projects or a
|
|
172
|
+
client already on GitHub.
|
|
173
|
+
- [`docs/requirements/05-config.md`](docs/requirements/05-config.md) — the rules above stated as
|
|
174
|
+
requirements, including why the registry coordinates are committed with the deployment.
|
|
175
|
+
- [`docs/requirements/03-deploy.md`](docs/requirements/03-deploy.md) — how cairn uses a registry
|
|
176
|
+
tag as the desired-state pointer your servers converge to.
|