Full Code of basecamp/kamal for AI

main 453d8d7dc2b4 cached
313 files
739.5 KB
218.9k tokens
1275 symbols
1 requests
Download .txt
Showing preview only (815K chars total). Download the full file or copy to clipboard to get everything.
Repository: basecamp/kamal
Branch: main
Commit: 453d8d7dc2b4
Files: 313
Total size: 739.5 KB

Directory structure:
gitextract_hqeups_8/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── docker-publish.yml
├── .gitignore
├── .rubocop.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── MIT-LICENSE
├── README.md
├── bin/
│   ├── docs
│   ├── kamal
│   ├── release
│   └── test
├── gemfiles/
│   └── rails_edge.gemfile
├── kamal.gemspec
├── lib/
│   ├── kamal/
│   │   ├── cli/
│   │   │   ├── accessory.rb
│   │   │   ├── alias/
│   │   │   │   └── command.rb
│   │   │   ├── app/
│   │   │   │   ├── assets.rb
│   │   │   │   ├── boot.rb
│   │   │   │   ├── error_pages.rb
│   │   │   │   └── ssl_certificates.rb
│   │   │   ├── app.rb
│   │   │   ├── base.rb
│   │   │   ├── build/
│   │   │   │   ├── clone.rb
│   │   │   │   └── port_forwarding.rb
│   │   │   ├── build.rb
│   │   │   ├── healthcheck/
│   │   │   │   ├── barrier.rb
│   │   │   │   ├── error.rb
│   │   │   │   └── poller.rb
│   │   │   ├── lock.rb
│   │   │   ├── main.rb
│   │   │   ├── proxy.rb
│   │   │   ├── prune.rb
│   │   │   ├── registry.rb
│   │   │   ├── secrets.rb
│   │   │   ├── server.rb
│   │   │   └── templates/
│   │   │       ├── deploy.yml
│   │   │       ├── sample_hooks/
│   │   │       │   ├── docker-setup.sample
│   │   │       │   ├── post-app-boot.sample
│   │   │       │   ├── post-deploy.sample
│   │   │       │   ├── post-proxy-reboot.sample
│   │   │       │   ├── pre-app-boot.sample
│   │   │       │   ├── pre-build.sample
│   │   │       │   ├── pre-connect.sample
│   │   │       │   ├── pre-deploy.sample
│   │   │       │   └── pre-proxy-reboot.sample
│   │   │       └── secrets
│   │   ├── cli.rb
│   │   ├── commander/
│   │   │   └── specifics.rb
│   │   ├── commander.rb
│   │   ├── commands/
│   │   │   ├── accessory/
│   │   │   │   └── proxy.rb
│   │   │   ├── accessory.rb
│   │   │   ├── app/
│   │   │   │   ├── assets.rb
│   │   │   │   ├── containers.rb
│   │   │   │   ├── error_pages.rb
│   │   │   │   ├── execution.rb
│   │   │   │   ├── images.rb
│   │   │   │   ├── logging.rb
│   │   │   │   └── proxy.rb
│   │   │   ├── app.rb
│   │   │   ├── auditor.rb
│   │   │   ├── base.rb
│   │   │   ├── builder/
│   │   │   │   ├── base.rb
│   │   │   │   ├── clone.rb
│   │   │   │   ├── cloud.rb
│   │   │   │   ├── hybrid.rb
│   │   │   │   ├── local.rb
│   │   │   │   ├── pack.rb
│   │   │   │   └── remote.rb
│   │   │   ├── builder.rb
│   │   │   ├── docker.rb
│   │   │   ├── hook.rb
│   │   │   ├── lock.rb
│   │   │   ├── proxy.rb
│   │   │   ├── prune.rb
│   │   │   ├── registry.rb
│   │   │   └── server.rb
│   │   ├── commands.rb
│   │   ├── configuration/
│   │   │   ├── accessory.rb
│   │   │   ├── alias.rb
│   │   │   ├── boot.rb
│   │   │   ├── builder.rb
│   │   │   ├── docs/
│   │   │   │   ├── accessory.yml
│   │   │   │   ├── alias.yml
│   │   │   │   ├── boot.yml
│   │   │   │   ├── builder.yml
│   │   │   │   ├── configuration.yml
│   │   │   │   ├── env.yml
│   │   │   │   ├── logging.yml
│   │   │   │   ├── proxy.yml
│   │   │   │   ├── registry.yml
│   │   │   │   ├── role.yml
│   │   │   │   ├── servers.yml
│   │   │   │   ├── ssh.yml
│   │   │   │   └── sshkit.yml
│   │   │   ├── env/
│   │   │   │   └── tag.rb
│   │   │   ├── env.rb
│   │   │   ├── logging.rb
│   │   │   ├── proxy/
│   │   │   │   ├── boot.rb
│   │   │   │   └── run.rb
│   │   │   ├── proxy.rb
│   │   │   ├── registry.rb
│   │   │   ├── role.rb
│   │   │   ├── servers.rb
│   │   │   ├── ssh.rb
│   │   │   ├── sshkit.rb
│   │   │   ├── validation.rb
│   │   │   ├── validator/
│   │   │   │   ├── accessory.rb
│   │   │   │   ├── alias.rb
│   │   │   │   ├── builder.rb
│   │   │   │   ├── configuration.rb
│   │   │   │   ├── env.rb
│   │   │   │   ├── proxy.rb
│   │   │   │   ├── registry.rb
│   │   │   │   ├── role.rb
│   │   │   │   └── servers.rb
│   │   │   ├── validator.rb
│   │   │   └── volume.rb
│   │   ├── configuration.rb
│   │   ├── docker.rb
│   │   ├── env_file.rb
│   │   ├── git.rb
│   │   ├── secrets/
│   │   │   ├── adapters/
│   │   │   │   ├── aws_secrets_manager.rb
│   │   │   │   ├── base.rb
│   │   │   │   ├── bitwarden.rb
│   │   │   │   ├── bitwarden_secrets_manager.rb
│   │   │   │   ├── doppler.rb
│   │   │   │   ├── enpass.rb
│   │   │   │   ├── gcp_secret_manager.rb
│   │   │   │   ├── last_pass.rb
│   │   │   │   ├── one_password.rb
│   │   │   │   ├── passbolt.rb
│   │   │   │   └── test.rb
│   │   │   ├── adapters.rb
│   │   │   └── dotenv/
│   │   │       └── inline_command_substitution.rb
│   │   ├── secrets.rb
│   │   ├── sshkit_with_ext.rb
│   │   ├── tags.rb
│   │   ├── utils/
│   │   │   └── sensitive.rb
│   │   ├── utils.rb
│   │   └── version.rb
│   └── kamal.rb
└── test/
    ├── cli/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── build_test.rb
    │   ├── cli_test_case.rb
    │   ├── lock_test.rb
    │   ├── main_test.rb
    │   ├── proxy_test.rb
    │   ├── prune_test.rb
    │   ├── registry_test.rb
    │   ├── secrets_test.rb
    │   └── server_test.rb
    ├── commander_test.rb
    ├── commands/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── auditor_test.rb
    │   ├── builder_test.rb
    │   ├── docker_test.rb
    │   ├── hook_test.rb
    │   ├── lock_test.rb
    │   ├── proxy_test.rb
    │   ├── prune_test.rb
    │   ├── registry_test.rb
    │   └── server_test.rb
    ├── configuration/
    │   ├── accessory_test.rb
    │   ├── boot_test.rb
    │   ├── builder_test.rb
    │   ├── env/
    │   │   └── tags_test.rb
    │   ├── env_test.rb
    │   ├── proxy/
    │   │   └── boot_test.rb
    │   ├── proxy_test.rb
    │   ├── role_test.rb
    │   ├── ssh_test.rb
    │   ├── sshkit_test.rb
    │   ├── validation_test.rb
    │   └── volume_test.rb
    ├── configuration_test.rb
    ├── env_file_test.rb
    ├── fixtures/
    │   ├── deploy.elsewhere.yml
    │   ├── deploy.erb.yml
    │   ├── deploy.yml
    │   ├── deploy2.yml
    │   ├── deploy_for_dest.mars.yml
    │   ├── deploy_for_dest.world.yml
    │   ├── deploy_for_dest.yml
    │   ├── deploy_for_required_dest.world.yml
    │   ├── deploy_for_required_dest.yml
    │   ├── deploy_primary_web_role_override.yml
    │   ├── deploy_simple.yml
    │   ├── deploy_with_accessories.yml
    │   ├── deploy_with_accessories_on_independent_server.yml
    │   ├── deploy_with_accessories_with_different_registries.yml
    │   ├── deploy_with_aliases.yml
    │   ├── deploy_with_assets.yml
    │   ├── deploy_with_boot_strategy.yml
    │   ├── deploy_with_cloud_builder.yml
    │   ├── deploy_with_env_tags.yml
    │   ├── deploy_with_error_pages.yml
    │   ├── deploy_with_extensions.yml
    │   ├── deploy_with_hybrid_builder.yml
    │   ├── deploy_with_local_registry.yml
    │   ├── deploy_with_local_registry_and_accessories.yml
    │   ├── deploy_with_local_registry_and_remote_builder.yml
    │   ├── deploy_with_local_registry_and_remote_builder_with_port.yml
    │   ├── deploy_with_multiple_proxy_roles.yml
    │   ├── deploy_with_only_workers.yml
    │   ├── deploy_with_parallel_roles.yml
    │   ├── deploy_with_proxy.yml
    │   ├── deploy_with_proxy_roles.yml
    │   ├── deploy_with_proxy_run_config.yml
    │   ├── deploy_with_proxy_run_config_conflicts.yml
    │   ├── deploy_with_remote_builder.yml
    │   ├── deploy_with_remote_builder_and_custom_ports.yml
    │   ├── deploy_with_roles.yml
    │   ├── deploy_with_roles_workers_primary.yml
    │   ├── deploy_with_secrets.yml
    │   ├── deploy_with_single_accessory.yml
    │   ├── deploy_with_two_roles_one_host.yml
    │   ├── deploy_with_uncommon_hostnames.yml
    │   ├── deploy_without_clone.yml
    │   ├── deploy_without_parallel_roles.yml
    │   └── files/
    │       ├── my.cnf
    │       └── structure.sql.erb
    ├── git_test.rb
    ├── integration/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── broken_deploy_test.rb
    │   ├── docker/
    │   │   ├── deployer/
    │   │   │   ├── .dockerignore
    │   │   │   ├── Dockerfile
    │   │   │   ├── app/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   ├── hooks/
    │   │   │   │   │   │   ├── docker-setup
    │   │   │   │   │   │   ├── post-app-boot
    │   │   │   │   │   │   ├── post-deploy
    │   │   │   │   │   │   ├── post-proxy-reboot
    │   │   │   │   │   │   ├── pre-app-boot
    │   │   │   │   │   │   ├── pre-build
    │   │   │   │   │   │   ├── pre-connect
    │   │   │   │   │   │   ├── pre-deploy
    │   │   │   │   │   │   └── pre-proxy-reboot
    │   │   │   │   │   ├── secrets
    │   │   │   │   │   └── secrets-common
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   ├── busybox.conf
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_custom_certificate/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── certs/
    │   │   │   │   │   ├── cert.pem
    │   │   │   │   │   └── key.pem
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_destinations/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   ├── deploy.production.yml
    │   │   │   │   │   ├── deploy.staging.yml
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_parallel_roles/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   ├── default.conf
    │   │   │   │   └── error_pages/
    │   │   │   │       └── 503.html
    │   │   │   ├── app_with_proxied_accessory/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_roles/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   ├── hooks/
    │   │   │   │   │   │   ├── docker-setup
    │   │   │   │   │   │   ├── post-deploy
    │   │   │   │   │   │   ├── post-proxy-reboot
    │   │   │   │   │   │   ├── pre-build
    │   │   │   │   │   │   ├── pre-connect
    │   │   │   │   │   │   ├── pre-deploy
    │   │   │   │   │   │   └── pre-proxy-reboot
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   ├── default.conf
    │   │   │   │   └── error_pages/
    │   │   │   │       └── 503.html
    │   │   │   ├── app_with_traefik/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── boot.sh
    │   │   │   ├── break_app.sh
    │   │   │   ├── setup.sh
    │   │   │   └── update_app_rev.sh
    │   │   ├── load_balancer/
    │   │   │   ├── Dockerfile
    │   │   │   └── default.conf
    │   │   ├── registry/
    │   │   │   ├── Dockerfile
    │   │   │   └── boot.sh
    │   │   ├── shared/
    │   │   │   ├── .dockerignore
    │   │   │   ├── Dockerfile
    │   │   │   ├── boot.sh
    │   │   │   └── registry-dns.conf
    │   │   └── vm/
    │   │       ├── Dockerfile
    │   │       └── boot.sh
    │   ├── docker-compose.yml
    │   ├── integration_test.rb
    │   ├── lock_test.rb
    │   ├── main_test.rb
    │   └── proxy_test.rb
    ├── secrets/
    │   ├── aws_secrets_manager_adapter_test.rb
    │   ├── bitwarden_adapter_test.rb
    │   ├── bitwarden_secrets_manager_adapter_test.rb
    │   ├── doppler_adapter_test.rb
    │   ├── dotenv_inline_command_substitution_test.rb
    │   ├── enpass_adapter_test.rb
    │   ├── gcp_secret_manager_adapter_test.rb
    │   ├── last_pass_adapter_test.rb
    │   ├── one_password_adapter_test.rb
    │   └── passbolt_adapter_test.rb
    ├── secrets_test.rb
    ├── sshkit_dns_retry_test.rb
    ├── test_helper.rb
    └── utils_test.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  rubocop:
    name: RuboCop
    runs-on: ubuntu-latest
    env:
      BUNDLE_ONLY: rubocop
    steps:
      - name: Checkout code
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
      - name: Setup Ruby and install gems
        uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
        with:
          ruby-version: 3.3.0
          bundler-cache: true
      - name: Run Rubocop
        run: bundle exec rubocop --parallel
  tests:
    strategy:
      fail-fast: false
      matrix:
        ruby-version:
          - "3.2"
          - "3.3"
          - "3.4"
          - "4.0"
        gemfile:
          - Gemfile
          - gemfiles/rails_edge.gemfile
        exclude:
          - ruby-version: "3.2"
            gemfile: gemfiles/rails_edge.gemfile
    name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
    runs-on: ubuntu-latest
    env:
      BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

      - name: Remove gemfile.lock
        run: rm Gemfile.lock

      - name: Install Ruby
        uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
        with:
          ruby-version: ${{ matrix.ruby-version }}
          bundler-cache: true

      - name: Configure Docker with VFS storage driver
        run: |
          sudo systemctl stop docker
          sudo systemctl stop docker.socket
          sudo mkdir -p /etc/docker /mnt/docker
          cat <<'EOF' | sudo tee /etc/docker/daemon.json
          {
            "storage-driver": "vfs",
            "data-root": "/mnt/docker"
          }
          EOF
          sudo rm -rf /var/lib/docker/* /mnt/docker/*
          sudo systemctl start docker
          timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done'
          df -h

      - name: Run tests
        run: bin/test
        env:
          RUBYOPT: ${{ startsWith(matrix.ruby-version, '3.4.') && '--enable=frozen-string-literal' || '' }}

      - name: Check disk usage
        if: always()
        run: |
          df -h
          sudo du -sh /mnt/docker


================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Docker

on:
  workflow_dispatch:
    inputs:
      tagInput:
        description: 'Tag'
        required: true
    
  release:
    types: [created]
    tags:
      - 'v*'

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      -
        name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
      -
        name: Login to GitHub Container Registry
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Determine version tag
        id: version-tag
        run: |
          INPUT_VALUE="${{ github.event.inputs.tagInput }}"
          if [ -z "$INPUT_VALUE" ]; then
            INPUT_VALUE="${{ github.ref_name }}"
          fi
          echo "::set-output name=value::$INPUT_VALUE"
      -
        name: Build and push
        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/basecamp/kamal:latest
            ghcr.io/basecamp/kamal:${{ steps.version-tag.outputs.value }}


================================================
FILE: .gitignore
================================================
.byebug_history
*.gem
coverage/*
.DS_Store
gemfiles/*.lock


================================================
FILE: .rubocop.yml
================================================
inherit_gem:
  rubocop-rails-omakase: rubocop.yml


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct

As contributors and maintainers of the Kamal project, we pledge to create a welcoming and inclusive environment for everyone. We value the participation of each member of our community and want all contributors to feel respected and valued.

We are committed to providing a harassment-free experience for everyone, regardless of gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, age, or religion (or lack thereof). We do not tolerate harassment of participants in any form.

This code of conduct applies to all Kamal project spaces, including but not limited to project code, issue trackers, chat rooms, and mailing lists. Violations of this code of conduct may result in removal from the project community.

## Our standards

Examples of behavior that contributes to creating a positive environment include:

- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting

## Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Reporting

If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a project maintainer. All reports will be kept confidential and will be reviewed and investigated promptly.

We will investigate every complaint and take appropriate action. We reserve the right to remove any content that violates this Code of Conduct, or to temporarily or permanently ban any contributor for other behaviors that we deem inappropriate, threatening, offensive, or harmful.

## Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Kamal development

Thank you for considering contributing to Kamal! This document outlines some guidelines for contributing to this open source project.

Please make sure to review our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing to Kamal.

There are several ways you can contribute to the betterment of the project:

- **Report an issue?** - If the issue isn’t reported, we can’t fix it. Please report any bugs, feature, and/or improvement requests on the [Kamal GitHub Issues tracker](https://github.com/basecamp/kamal/issues).
- **Submit patches** - Do you have a new feature or a fix you'd like to share? [Submit a pull request](https://github.com/basecamp/kamal/pulls)!
- **Write blog articles** - Are you using Kamal? We'd love to hear how you're using it with your projects. Write a tutorial and post it on your blog!

## Issues

If you encounter any issues with the project, please check the [existing issues](https://github.com/basecamp/kamal/issues) first to see if the issue has already been reported. If the issue hasn't been reported, please open a new issue with a clear description of the problem and steps to reproduce it.

## Pull Requests

Please keep the following guidelines in mind when opening a pull request:

- Ensure that your code passes the project's minitests by running ./bin/test.
- Provide a clear and detailed description of your changes.
- Keep your changes focused on a single concern.
- Write clean and readable code that follows the project's code style.
- Use descriptive variable and function names.
- Write clear and concise commit messages.
- Add tests for your changes, if possible.
- Ensure that your changes don't break existing functionality.

#### Commit message guidelines

A good commit message should describe what changed and why.

## Development

The `main` branch is regularly built and tested, but it is not guaranteed to be completely stable. Tags are created regularly from release branches to indicate new official, stable release versions of Kamal.

Kamal is written in Ruby. You should have Ruby 3.2+ installed on your machine in order to work on Kamal. If that's already setup, run `bundle` in the root directory to install all dependencies. Then you can run `bin/test` to run all tests.

1. Fork the project repository.
2. Create a new branch for your contribution.
3. Write your code or make the desired changes.
4. **Ensure that your code passes the project's minitests by running ./bin/test.**
5. Commit your changes and push them to your forked repository.
6. [Open a pull request](https://github.com/basecamp/kamal/pulls) to the main project repository with a detailed description of your changes.

## License

Kamal is released under the MIT License. By contributing to this project, you agree to license your contributions under the same license.


================================================
FILE: Dockerfile
================================================
FROM ruby:3.4-alpine

# Install docker/buildx-bin
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx

# Set the working directory to /kamal
WORKDIR /kamal

# Copy the Gemfile, Gemfile.lock into the container
COPY Gemfile Gemfile.lock kamal.gemspec ./

# Required in kamal.gemspec
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb

# Install system dependencies
RUN apk add --no-cache build-base git docker-cli openssh-client-default yaml-dev \
    && gem install bundler --version=2.6.5 \
    && bundle install

# Copy the rest of our application code into the container.
# We do this after bundle install, to avoid having to run bundle
# every time we do small fixes in the source code.
COPY . .

# Install the gem locally from the project folder
RUN gem build kamal.gemspec && \
    gem install ./kamal-*.gem --no-document

# Set the working directory to /workdir
WORKDIR /workdir

# Tell git it's safe to access /workdir/.git even if
# the directory is owned by a different user
RUN git config --global --add safe.directory '*'

# Set the entrypoint to run the installed binary in /workdir
# Example:  docker run -it -v "$PWD:/workdir" kamal init
ENTRYPOINT ["kamal"]


================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gemspec

group :rubocop do
  gem "rubocop-rails-omakase", require: false
end


================================================
FILE: MIT-LICENSE
================================================
Copyright (c) 2023 David Heinemeier Hansson

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# Kamal: Deploy web apps anywhere

From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.

➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).

## Contributing to the documentation

Please help us improve Kamal's documentation on [the basecamp/kamal-site repository](https://github.com/basecamp/kamal-site).

## License

Kamal is released under the [MIT License](https://opensource.org/licenses/MIT).


================================================
FILE: bin/docs
================================================
#!/usr/bin/env ruby
require "stringio"

def usage
  puts "Usage: #{$0} <kamal_site_repo>"
  exit 1
end

usage if ARGV.size != 1

kamal_site_repo = ARGV[0]

if !File.directory?(kamal_site_repo)
  puts "Error: #{kamal_site_repo} is not a directory"
  exit 1
end

DOCS = {
  "accessory" => "Accessories",
  "alias" => "Aliases",
  "boot" => "Booting",
  "builder" => "Builders",
  "configuration" => "Configuration overview",
  "env" => "Environment variables",
  "logging" => "Logging",
  "proxy" => "Proxy",
  "registry" => "Docker Registry",
  "role" => "Roles",
  "servers" => "Servers",
  "ssh" => "SSH",
  "sshkit" => "SSHKit"
}
DOCS_PATH = "lib/kamal/configuration/docs"

class DocWriter
  attr_reader :from_file, :to_file, :key, :heading, :body, :output, :in_yaml

  def initialize(from_file, to_dir)
    @from_file = from_file
    @key = File.basename(from_file, ".yml")
    @to_file = File.join(to_dir, "#{linkify(DOCS[key])}.md")
    @body = File.readlines(from_file)
    @heading = body.shift.chomp("\n")
    @output = nil
  end

  def write
    puts "Writing #{to_file}"
    generate_markdown
    File.write(to_file, output.string)
  end

  private
    def generate_markdown
      @output = StringIO.new

      generate_header

      place = :in_section

      loop do
        line = body.shift&.chomp("\n")
        break if line.nil?

        case place
        when :new_section, :in_section
          if line.empty?
            output.puts
            place = :new_section
          elsif line =~ /^ *#/
            generate_line(line, heading: place == :new_section)
            place = :in_section
          else
            output.puts
            output.puts "```yaml"
            output.puts line
            place = :in_yaml
          end
        when :in_yaml, :in_empty_line_yaml
          if line =~ /^ {0,4}#/
            output.puts "```"
            output.puts
            generate_line(line, heading: place == :in_empty_line_yaml)
            place = :in_section
          elsif line.empty?
            place = :in_empty_line_yaml
          else
            output.puts line
          end
        end
      end

      output.puts "```" if place == :in_yaml
    end

    def generate_header
      output.puts "---"
      output.puts "# This file has been generated from the Kamal source, do not edit directly."
      output.puts "# Find the source of this file at #{DOCS_PATH}/#{key}.yml in the Kamal repository."
      output.puts "title: #{heading[2..-1]}"
      output.puts "---"
      output.puts
      output.puts heading
    end

    def generate_line(line, heading: false)
      line = line.gsub(/^ *#\s?/, "")

      if line =~ /(.*)kamal docs ([a-z]*)(.*)/
        line = "#{$1}[#{DOCS[$2]}](../#{linkify(DOCS[$2])})#{$3}"
      end

      if line =~ /(.*)https:\/\/kamal-deploy.org([a-z\/-]*)(.*)/
        line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
      end

      if heading
        output.puts "## [#{line}](##{linkify(line)})"
      else
        output.puts line
      end
    end

    def linkify(text)
      if text == "Configuration overview"
        "overview"
      else
        text.downcase.gsub(" ", "-")
      end
    end

    def titlify(text)
      text.capitalize.gsub("-", " ")
    end
end

from_dir = File.join(File.dirname(__FILE__), "../#{DOCS_PATH}")
to_dir = File.join(kamal_site_repo, "docs/configuration")
Dir.glob("#{from_dir}/*") do |from_file|
  DocWriter.new(from_file, to_dir).write
end


================================================
FILE: bin/kamal
================================================
#!/usr/bin/env ruby

# Prevent failures from being reported twice.
Thread.report_on_exception = false

require "kamal"

begin
  Kamal::Cli::Main.start(ARGV)
rescue SSHKit::Runner::ExecuteError => e
  puts "  \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
  puts e.cause.backtrace if ENV["VERBOSE"]
  exit 1
rescue => e
  puts "  \e[31mERROR (#{e.class}): #{e.message}\e[0m"
  puts e.backtrace if ENV["VERBOSE"]
  exit 1
end


================================================
FILE: bin/release
================================================
#!/usr/bin/env bash

VERSION=$1

printf "module Kamal\n  VERSION = \"$VERSION\"\nend\n" > ./lib/kamal/version.rb
bundle
git add Gemfile.lock lib/kamal/version.rb
git commit -m "Bump version for $VERSION"
git push
git tag v$VERSION
git push --tags
gem build kamal.gemspec
gem push "kamal-$VERSION.gem" --host https://rubygems.org
rm "kamal-$VERSION.gem"


================================================
FILE: bin/test
================================================
#!/usr/bin/env ruby
$: << File.expand_path("../test", __dir__)

require "bundler/setup"
require "rails/plugin/test"


================================================
FILE: gemfiles/rails_edge.gemfile
================================================
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

git "https://github.com/rails/rails.git" do
  gem "railties"
  gem "activesupport"
end

gemspec path: "../"


================================================
FILE: kamal.gemspec
================================================
require_relative "lib/kamal/version"

Gem::Specification.new do |spec|
  spec.name        = "kamal"
  spec.version     = Kamal::VERSION
  spec.authors     = [ "David Heinemeier Hansson" ]
  spec.email       = "dhh@hey.com"
  spec.homepage    = "https://github.com/basecamp/kamal"
  spec.summary     = "Deploy web apps in containers to servers running Docker with zero downtime."
  spec.license     = "MIT"
  spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
  spec.executables = %w[ kamal ]

  spec.add_dependency "activesupport", ">= 7.0"
  spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
  spec.add_dependency "net-ssh", "~> 7.3"
  spec.add_dependency "thor", "~> 1.3"
  spec.add_dependency "dotenv", "~> 3.1"
  spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0"
  spec.add_dependency "ed25519", "~> 1.4"
  spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
  spec.add_dependency "concurrent-ruby", "~> 1.2"
  spec.add_dependency "base64", "~> 0.2"

  spec.add_development_dependency "debug"
  spec.add_development_dependency "minitest", "< 6"
  spec.add_development_dependency "mocha"
  spec.add_development_dependency "railties"
end


================================================
FILE: lib/kamal/cli/accessory.rb
================================================
require "active_support/core_ext/array/conversions"
require "concurrent/array"

class Kamal::Cli::Accessory < Kamal::Cli::Base
  desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
  def boot(name, prepare: true)
    with_lock do
      if name == "all"
        KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
      else
        prepare(name) if prepare

        with_accessory(name) do |accessory, hosts|
          booted_hosts = Concurrent::Array.new
          on(hosts) do |host|
            booted_hosts << host.to_s if capture_with_info(*accessory.info(all: true, quiet: true)).strip.presence
          end

          if booted_hosts.any?
            say "Skipping booting `#{name}` on #{booted_hosts.sort.join(", ")}, a container already exists", :yellow
            hosts -= booted_hosts
          end

          directories(name)
          upload(name)

          on(hosts) do |host|
            execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
            execute *accessory.ensure_env_directory
            upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
            execute *accessory.run(host: host)

            if accessory.running_proxy?
              target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
              execute *accessory.deploy(target: target)
            end
          end
        end
      end
    end
  end

  desc "upload [NAME]", "Upload accessory files to host", hide: true
  def upload(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          accessory.files.each do |(local, config)|
            remote = config[:host_path]
            accessory.ensure_local_file_present(local)

            execute *accessory.make_directory_for(remote)
            upload! local, remote
            execute :chmod, config[:mode], remote
            execute :chown, config[:owner], remote if config[:owner]
          end
        end
      end
    end
  end

  desc "directories [NAME]", "Create accessory directories on host", hide: true
  def directories(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          accessory.directories.each do |(local, config)|
            execute *accessory.make_directory(local)
            execute :chmod, config[:mode], local if config[:mode]
            execute :chown, config[:owner], local if config[:owner]
          end
        end
      end
    end
  end

  desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
  def reboot(name)
    with_lock do
      if name == "all"
        KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
      else
        prepare(name)
        pull_image(name)
        stop(name)
        remove_container(name)
        boot(name, prepare: false)
      end
    end
  end

  desc "start [NAME]", "Start existing accessory container on host"
  def start(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
          execute *accessory.start
          if accessory.running_proxy?
            target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
            execute *accessory.deploy(target: target)
          end
        end
      end
    end
  end

  desc "stop [NAME]", "Stop existing accessory container on host"
  def stop(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
          execute *accessory.stop, raise_on_non_zero_exit: false

          if accessory.running_proxy?
            target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
            execute *accessory.remove if target
          end
        end
      end
    end
  end

  desc "restart [NAME]", "Restart existing accessory container on host"
  def restart(name)
    with_lock do
      stop(name)
      start(name)
    end
  end

  desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
  def details(name)
    quiet = options[:quiet]
    if name == "all"
      KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
    else
      type = "Accessory #{name}"
      with_accessory(name) do |accessory, hosts|
        on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type, quiet: quiet }
      end
    end
  end

  desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
  def exec(name, *cmd)
    pre_connect_if_required

    cmd = Kamal::Utils.join_commands(cmd)
    quiet = options[:quiet]

    with_accessory(name) do |accessory, hosts|
      case
      when options[:interactive] && options[:reuse]
        say "Launching interactive command via SSH from existing container...", :magenta
        run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }

      when options[:interactive]
        say "Launching interactive command via SSH from new container...", :magenta
        on(accessory.hosts.first) { execute *KAMAL.registry.login }
        run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }

      when options[:reuse]
        say "Launching command from existing container...", :magenta
        on(hosts) do |host|
          execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
          puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd)), quiet: quiet
        end

      else
        say "Launching command from new container...", :magenta
        on(hosts) do |host|
          execute *KAMAL.registry.login
          execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
          puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd)), quiet: quiet
        end
      end
    end
  end

  desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
  option :grep_options, desc: "Additional options supplied to grep"
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
  def logs(name)
    with_accessory(name) do |accessory, hosts|
      grep = options[:grep]
      grep_options = options[:grep_options]
      timestamps = !options[:skip_timestamps]

      if options[:follow]
        run_locally do
          info "Following logs on #{hosts}..."
          info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
          exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
        end
      else
        since = options[:since]
        lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set

        on(hosts) do
          puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
        end
      end
    end
  end

  desc "pull_image [NAME]", "Pull accessory image on host", hide: true
  def pull_image(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
          execute *accessory.pull_image
        end
      end
    end
  end

  desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  def remove(name)
    confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
      with_lock do
        if name == "all"
          KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
        else
          remove_accessory(name)
        end
      end
    end
  end

  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
  def remove_container(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
          execute *accessory.remove_container
        end
      end
    end
  end

  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
  def remove_image(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
          execute *accessory.remove_image
        end
      end
    end
  end

  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
  def remove_service_directory(name)
    with_lock do
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *accessory.remove_service_directory
        end
      end
    end
  end

  desc "upgrade", "Upgrade accessories from Kamal 1.x to 2.0 (restart them in 'kamal' network)"
  option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  def upgrade(name)
    confirming "This will restart all accessories" do
      with_lock do
        host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
        host_groups.each do |hosts|
          host_list = Array(hosts).join(",")
          KAMAL.with_specific_hosts(hosts) do
            say "Upgrading #{name} accessories on #{host_list}...", :magenta
            reboot name
            say "Upgraded #{name} accessories on #{host_list}...", :magenta
          end
        end
      end
    end
  end

  private
    def with_accessory(name)
      if KAMAL.config.accessory(name)
        accessory = KAMAL.accessory(name)
        yield accessory, accessory_hosts(accessory)
      else
        error_on_missing_accessory(name)
      end
    end

    def error_on_missing_accessory(name)
      options = KAMAL.accessory_names.presence

      error \
        "No accessory by the name of '#{name}'" +
        (options ? " (options: #{options.to_sentence})" : "")
    end

    def accessory_hosts(accessory)
      KAMAL.accessory_hosts & accessory.hosts
    end

    def remove_accessory(name)
      stop(name)
      remove_container(name)
      remove_image(name)
      remove_service_directory(name)
    end

    def prepare(name)
      with_accessory(name) do |accessory, hosts|
        on(hosts) do
          execute *KAMAL.registry.login(registry_config: accessory.registry)
          execute *KAMAL.docker.create_network
        rescue SSHKit::Command::Failed => e
          raise unless e.message.include?("already exists")
        end
      end
    end
end


================================================
FILE: lib/kamal/cli/alias/command.rb
================================================
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
  def run(instance, args = [])
    if (command = KAMAL.resolve_alias(name))
      KAMAL.reset
      Kamal::Cli::Main.start(Shellwords.split(command) + ARGV[1..-1])
    else
      super
    end
  end
end


================================================
FILE: lib/kamal/cli/app/assets.rb
================================================
class Kamal::Cli::App::Assets
  attr_reader :host, :role, :sshkit
  delegate :execute, :capture_with_info, :info, to: :sshkit
  delegate :assets?, to: :role

  def initialize(host, role, sshkit)
    @host = host
    @role = role
    @sshkit = sshkit
  end

  def run
    if assets?
      execute *app.extract_assets
      old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
      execute *app.sync_asset_volumes(old_version: old_version)
    end
  end

  private
    def app
      @app ||= KAMAL.app(role: role, host: host)
    end
end


================================================
FILE: lib/kamal/cli/app/boot.rb
================================================
class Kamal::Cli::App::Boot
  attr_reader :host, :role, :version, :barrier, :sshkit
  delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit
  delegate :assets?, :running_proxy?, to: :role

  def initialize(host, role, sshkit, version, barrier)
    @host = host
    @role = role
    @version = version
    @barrier = barrier
    @sshkit = sshkit
  end

  def run
    old_version = old_version_renamed_if_clashing

    wait_at_barrier if queuer?

    begin
      start_new_version
    rescue => e
      close_barrier if gatekeeper?
      stop_new_version
      raise
    end

    release_barrier if gatekeeper?

    if old_version
      stop_old_version(old_version)
    end
  end

  private
    def old_version_renamed_if_clashing
      if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
        renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
        info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
        audit("Renaming container #{version} to #{renamed_version}")
        execute *app.rename_container(version: version, new_version: renamed_version)
      end

      capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
    end

    def start_new_version
      audit "Booted app version #{version}"
      hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"

      execute *app.ensure_env_directory
      upload! role.secrets_io(host), role.secrets_path, mode: "0600"

      execute *app.run(hostname: hostname)
      if running_proxy?
        endpoint = capture_with_info(*app.container_id_for_version(version)).strip
        raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
        execute *app.deploy(target: endpoint)
      else
        Kamal::Cli::Healthcheck::Poller.wait_for_healthy { capture_with_info(*app.status(version: version)) }
      end
    rescue => e
      error "Failed to boot #{role} on #{host}"
      raise e
    end

    def stop_new_version
      execute *app.stop(version: version), raise_on_non_zero_exit: false
    end

    def stop_old_version(version)
      execute *app.stop(version: version), raise_on_non_zero_exit: false
      execute *app.clean_up_assets if assets?
      execute *app.clean_up_error_pages if KAMAL.config.error_pages_path
    end

    def release_barrier
      if barrier.open
        info "First #{KAMAL.primary_role} container is healthy on #{host}, booting any other roles"
      end
    end

    def wait_at_barrier
      info "Waiting for the first healthy #{KAMAL.primary_role} container before booting #{role} on #{host}..."
      barrier.wait
      info "First #{KAMAL.primary_role} container is healthy, booting #{role} on #{host}..."
    rescue Kamal::Cli::Healthcheck::Error
      info "First #{KAMAL.primary_role} container is unhealthy, not booting #{role} on #{host}"
      raise
    end

    def close_barrier
      if barrier.close
        info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
        begin
          error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
          error capture_with_info(*app.container_health_log(version: version))
        rescue SSHKit::Command::Failed
          error "Could not fetch logs for #{version}"
        end
      end
    end

    def barrier_role?
      role == KAMAL.primary_role
    end

    def app
      @app ||= KAMAL.app(role: role, host: host)
    end

    def auditor
      @auditor = KAMAL.auditor(role: role)
    end

    def audit(message)
      execute *auditor.record(message), verbosity: :debug
    end

    def gatekeeper?
      barrier && barrier_role?
    end

    def queuer?
      barrier && !barrier_role?
    end
end


================================================
FILE: lib/kamal/cli/app/error_pages.rb
================================================
class Kamal::Cli::App::ErrorPages
  ERROR_PAGES_GLOB = "{4??.html,5??.html}"

  attr_reader :host, :sshkit
  delegate :upload!, :execute, to: :sshkit

  def initialize(host, sshkit)
    @host = host
    @sshkit = sshkit
  end

  def run
    if KAMAL.config.error_pages_path
      with_error_pages_tmpdir do |local_error_pages_dir|
        execute *KAMAL.app.create_error_pages_directory
        upload! local_error_pages_dir, KAMAL.config.proxy_boot.error_pages_directory, mode: "0700", recursive: true
      end
    end
  end

  private
    def with_error_pages_tmpdir
      Dir.mktmpdir("kamal-error-pages") do |tmpdir|
        error_pages_dir = File.join(tmpdir, KAMAL.config.version)
        FileUtils.mkdir(error_pages_dir)

        if (files = Dir[File.join(KAMAL.config.error_pages_path, ERROR_PAGES_GLOB)]).any?
          FileUtils.cp(files, error_pages_dir)
          yield error_pages_dir
        end
      end
    end
end


================================================
FILE: lib/kamal/cli/app/ssl_certificates.rb
================================================
class Kamal::Cli::App::SslCertificates
  attr_reader :host, :role, :sshkit
  delegate :execute, :info, :upload!, to: :sshkit

  def initialize(host, role, sshkit)
    @host = host
    @role = role
    @sshkit = sshkit
  end

  def run
    if role.running_proxy? && role.proxy.custom_ssl_certificate?
      info "Writing SSL certificates for #{role.name} on #{host}"
      execute *app.create_ssl_directory
      if cert_content = role.proxy.certificate_pem_content
        upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
      end
      if key_content = role.proxy.private_key_pem_content
        upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
      end
    end
  end

  private
    def app
      @app ||= KAMAL.app(role: role, host: host)
    end
end


================================================
FILE: lib/kamal/cli/app.rb
================================================
class Kamal::Cli::App < Kamal::Cli::Base
  desc "boot", "Boot app on servers (or reboot app if already running)"
  def boot
    with_lock do
      say "Get most recent version available as an image...", :magenta unless options[:version]
      using_version(version_or_latest) do |version|
        say "Start container with version #{version} (or reboot if already running)...", :magenta

        # Assets are prepared in a separate step to ensure they are on all hosts before booting
        on(KAMAL.app_hosts) do
          Kamal::Cli::App::ErrorPages.new(host, self).run

          KAMAL.roles_on(host).each do |role|
            Kamal::Cli::App::Assets.new(host, role, self).run
            Kamal::Cli::App::SslCertificates.new(host, role, self).run
          end
        end

        # Primary hosts and roles are returned first, so they can open the barrier
        barrier = Kamal::Cli::Healthcheck::Barrier.new

        host_boot_groups.each do |hosts|
          host_list = Array(hosts).join(",")
          run_hook "pre-app-boot", hosts: host_list

          on_roles(KAMAL.roles, hosts: hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
            Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
          end

          run_hook "post-app-boot", hosts: host_list
          sleep KAMAL.config.boot.wait if KAMAL.config.boot.wait
        end

        # Tag once the app booted on all hosts
        on(KAMAL.app_hosts) do |host|
          execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
          execute *KAMAL.app.tag_latest_image
        end
      end
    end
  end

  desc "start", "Start existing app container on servers"
  def start
    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
        app = KAMAL.app(role: role, host: host)
        execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
        execute *app.start, raise_on_non_zero_exit: false

        if role.running_proxy?
          version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
          endpoint = capture_with_info(*app.container_id_for_version(version)).strip
          raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?

          execute *app.deploy(target: endpoint)
        end
      end
    end
  end

  desc "stop", "Stop app container on servers"
  def stop
    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
        app = KAMAL.app(role: role, host: host)
        execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug

        if role.running_proxy?
          version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
          endpoint = capture_with_info(*app.container_id_for_version(version)).strip
          if endpoint.present?
            execute *app.remove, raise_on_non_zero_exit: false
          end
        end

        execute *app.stop, raise_on_non_zero_exit: false
      end
    end
  end

  # FIXME: Drop in favor of just containers?
  desc "details", "Show details about app containers"
  def details
    quiet = options[:quiet]
    on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
      puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info), quiet: quiet
    end
  end

  desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
  option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
  def exec(*cmd)
    pre_connect_if_required

    if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
      raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
    end

    if cmd.empty?
      raise ArgumentError, "No command provided. You must specify a command to execute."
    end

    cmd = Kamal::Utils.join_commands(cmd)
    env = options[:env]
    detach = options[:detach]
    quiet = options[:quiet]
    case
    when options[:interactive] && options[:reuse]
      say "Get current version of running container...", :magenta unless options[:version]
      using_version(options[:version] || current_running_version) do |version|
        say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
        run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
      end

    when options[:interactive]
      say "Get most recent version available as an image...", :magenta unless options[:version]
      using_version(version_or_latest) do |version|
        say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
        on(KAMAL.primary_host) { execute *KAMAL.registry.login }
        run_locally do
          exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
        end
      end

    when options[:reuse]
      say "Get current version of running container...", :magenta unless options[:version]
      using_version(options[:version] || current_running_version) do |version|
        say "Launching command with version #{version} from existing container...", :magenta

        on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
          execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
          puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env)), quiet: quiet
        end
      end

    else
      say "Get most recent version available as an image...", :magenta unless options[:version]
      using_version(version_or_latest) do |version|
        say "Launching command with version #{version} from new container...", :magenta
        on(KAMAL.app_hosts) { execute *KAMAL.registry.login }

        on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
          execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
          puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach)), quiet: quiet
        end
      end
    end
  end

  desc "containers", "Show app containers on servers"
  def containers
    quiet = options[:quiet]
    on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers), quiet: quiet }
  end

  desc "stale_containers", "Detect app stale containers"
  option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
  def stale_containers
    quiet = options[:quiet]
    stop = options[:stop]

    with_lock_if_stopping do
      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
        app = KAMAL.app(role: role, host: host)
        versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
        versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]

        versions.each do |version|
          if stop
            puts_by_host host, "Stopping stale container for role #{role} with version #{version}", quiet: quiet
            execute *app.stop(version: version), raise_on_non_zero_exit: false
          else
            puts_by_host host,  "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)", quiet: quiet
          end
        end
      end
    end
  end

  desc "images", "Show app images on servers"
  def images
    quiet = options[:quiet]
    on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images), quiet: quiet }
  end

  desc "logs", "Show log lines from app on servers (use --help to show options)"
  option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
  option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
  option :grep_options, desc: "Additional options supplied to grep"
  option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
  option :container_id, desc: "Docker container ID to fetch logs"
  def logs
    # FIXME: Catch when app containers aren't running

    grep = options[:grep]
    grep_options = options[:grep_options]
    since = options[:since]
    container_id = options[:container_id]
    timestamps = !options[:skip_timestamps]
    quiet = options[:quiet]

    if options[:follow]
      lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set

      run_locally do
        info "Following logs on #{KAMAL.primary_host}..."

        KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
        role = KAMAL.roles_on(KAMAL.primary_host).first

        app = KAMAL.app(role: role, host: host)
        info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
        exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
      end
    else
      lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set

      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
        begin
          puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)), quiet: quiet
        rescue SSHKit::Command::Failed
          puts_by_host host, "Nothing found", quiet: quiet
        end
      end
    end
  end

  desc "remove", "Remove app containers and images from servers"
  def remove
    with_lock do
      stop
      remove_containers
      remove_images
      remove_app_directories
    end
  end

  desc "live", "Set the app to live mode"
  def live
    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
        execute *KAMAL.app(role: role, host: host).live if role.running_proxy?
      end
    end
  end

  desc "maintenance", "Set the app to maintenance mode"
  option :drain_timeout, type: :numeric, desc: "How long to allow in-flight requests to complete (defaults to drain_timeout from config)"
  option :message, type: :string, desc: "Message to display to clients while stopped"
  def maintenance
    maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] }

    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
        execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy?
      end
    end
  end

  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
  def remove_container(version)
    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
        execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
        execute *KAMAL.app(role: role, host: host).remove_container(version: version)
      end
    end
  end

  desc "remove_containers", "Remove all app containers from servers", hide: true
  def remove_containers
    with_lock do
      on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
        execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
        execute *KAMAL.app(role: role, host: host).remove_containers
      end
    end
  end

  desc "remove_images", "Remove all app images from servers", hide: true
  def remove_images
    with_lock do
      on(hosts_removing_all_roles) do
        execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
        execute *KAMAL.app.remove_images
      end
    end
  end

  desc "remove_app_directories", "Remove the app directories from servers", hide: true
  def remove_app_directories
    with_lock do
      on(hosts_removing_all_roles) do |host|
        execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
        execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug
        execute *KAMAL.app.remove_proxy_app_directory, raise_on_non_zero_exit: false
      end
    end
  end

  desc "version", "Show app version currently running on servers"
  def version
    quiet = options[:quiet]
    on(KAMAL.app_hosts) do |host|
      role = KAMAL.roles_on(host).first
      puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip, quiet: quiet
    end
  end

  private
    def hosts_removing_all_roles
      KAMAL.app_hosts.select { |host| KAMAL.roles_on(host).map(&:name).sort == KAMAL.config.host_roles(host.to_s).map(&:name).sort }
    end

    def using_version(new_version)
      if new_version
        begin
          old_version = KAMAL.config.version
          KAMAL.config.version = new_version
          yield new_version
        ensure
          KAMAL.config.version = old_version
        end
      else
        yield KAMAL.config.version
      end
    end

    def current_running_version(host: KAMAL.primary_host)
      version = nil
      on(host) do
        role = KAMAL.roles_on(host).first
        version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
      end
      version.presence
    end

    def version_or_latest
      options[:version] || KAMAL.config.latest_tag
    end

    def with_lock_if_stopping
      if options[:stop]
        with_lock { yield }
      else
        yield
      end
    end

    def host_boot_groups
      KAMAL.config.boot.limit ? KAMAL.app_hosts.each_slice(KAMAL.config.boot.limit).to_a : [ KAMAL.app_hosts ]
    end
end


================================================
FILE: lib/kamal/cli/base.rb
================================================
require "thor"
require "kamal/sshkit_with_ext"

module Kamal::Cli
  class Base < Thor
    include SSHKit::DSL

    VERBOSITY = { verbose: :debug, quiet: :error }.freeze

    def self.exit_on_failure?() true end
    def self.dynamic_command_class() Kamal::Cli::Alias::Command end

    class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
    class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"

    class_option :version, desc: "Run commands against a specific app version"

    class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
    class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
    class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"

    class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
    class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"

    class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"

    def initialize(args = [], local_options = {}, config = {})
      if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
        # When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
        # For our purposes, it means the arguments are passed in args rather than local_options.
        super([], args, config)
      else
        super
      end

      initialize_commander unless KAMAL.configured?
    end

    private
      def options_with_subcommand_class_options
        options.merge(@_initializer.last[:class_options] || {})
      end

      def initialize_commander
        KAMAL.tap do |commander|
          if options[:verbose]
            ENV["VERBOSE"] = "1" # For backtraces via cli/start
            commander.verbosity = VERBOSITY[:verbose]
          end

          if options[:quiet]
            commander.verbosity = VERBOSITY[:quiet]
          end

          commander.configure \
            config_file: Pathname.new(File.expand_path(options[:config_file])),
            destination: options[:destination],
            version: options[:version]

          commander.specific_hosts    = options[:hosts]&.split(",")
          commander.specific_roles    = options[:roles]&.split(",")
          commander.specific_primary! if options[:primary]
        end
      end

      def print_runtime
        started_at = Time.now
        yield
        Time.now - started_at
      ensure
        runtime = Time.now - started_at
        puts "  Finished all in #{sprintf("%.1f seconds", runtime)}"
      end

      def with_lock
        if KAMAL.holding_lock?
          yield
        else
          acquire_lock

          begin
            yield
          rescue
            begin
              release_lock
            rescue => e
              say "Error releasing the deploy lock: #{e.message}", :red
            end
            raise
          end

          release_lock
        end
      end

      def confirming(question)
        return yield if options[:confirmed]

        if ask(question, limited_to: %w[ y N ], default: "N") == "y"
          yield
        else
          say "Aborted", :red
        end
      end

      def acquire_lock
        ensure_run_directory

        raise_if_locked do
          say "Acquiring the deploy lock...", :magenta
          on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
        end

        KAMAL.holding_lock = true
      end

      def release_lock
        say "Releasing the deploy lock...", :magenta
        on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }

        KAMAL.holding_lock = false
      end

      def raise_if_locked
        yield
      rescue SSHKit::Runner::ExecuteError => e
        if e.message =~ /cannot create directory/
          say "Deploy lock already in place!", :red
          on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
          raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
        else
          raise e
        end
      end

      def run_hook(hook, **extra_details)
        if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
          details = {
            hosts: KAMAL.hosts.join(","),
            roles: KAMAL.specific_roles&.join(","),
            lock: KAMAL.holding_lock?.to_s,
            command: command,
            subcommand: subcommand
          }.compact

          hooks_output = KAMAL.config.hooks_output_for(hook)

          # CLI flags override config: -q hides all, -v shows all
          # Config setting :verbose forces output, :quiet forces silence
          hook_verbosity = if KAMAL.verbosity == :info && hooks_output
            VERBOSITY.fetch(hooks_output)
          else
            KAMAL.verbosity
          end

          with_env KAMAL.hook.env(**details, **extra_details) do
            KAMAL.with_verbosity(hook_verbosity) do
              run_locally do
                execute *KAMAL.hook.run(hook)
              end
            end
          rescue SSHKit::Command::Failed => e
            raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
          end
        end
      end

      def on(*args, &block)
        pre_connect_if_required

        super
      end

      def pre_connect_if_required
        if !KAMAL.connected?
          run_hook "pre-connect", secrets: true unless options[:skip_hooks]
          KAMAL.connected = true
        end
      end

      def command
        @kamal_command ||= begin
          invocation_class, invocation_commands = *first_invocation
          if invocation_class == Kamal::Cli::Main
            invocation_commands[0]
          else
            Kamal::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
          end
        end
      end

      def subcommand
        @kamal_subcommand ||= begin
          invocation_class, invocation_commands = *first_invocation
          invocation_commands[0] if invocation_class != Kamal::Cli::Main
        end
      end

      def first_invocation
        instance_variable_get("@_invocations").first
      end

      def reset_invocation(cli_class)
        instance_variable_get("@_invocations")[cli_class].pop
      end

      def ensure_run_directory
        on(KAMAL.hosts) do
          execute(*KAMAL.server.ensure_run_directory)
        end
      end

      def with_env(env)
        current_env = ENV.to_h.dup
        ENV.update(env)
        yield
      ensure
        ENV.clear
        ENV.update(current_env)
      end

      def ensure_docker_installed
        run_locally do
          begin
            execute *KAMAL.builder.ensure_docker_installed
          rescue SSHKit::Command::Failed => e
            error = e.message =~ /command not found/ ?
              "Docker is not installed locally" :
              "Docker buildx plugin is not installed locally"

            raise DependencyError, error
          end
        end
      end
  end
end


================================================
FILE: lib/kamal/cli/build/clone.rb
================================================
class Kamal::Cli::Build::Clone
  attr_reader :sshkit
  delegate :info, :error, :execute, :capture_with_info, to: :sshkit

  def initialize(sshkit)
    @sshkit = sshkit
  end

  def prepare
    begin
      clone_repo
    rescue SSHKit::Command::Failed => e
      if e.message =~ /already exists and is not an empty directory/
        reset
      else
        raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
      end
    end

    validate!
  rescue Kamal::Cli::Build::BuildError => e
    error "Error preparing clone: #{e.message}, deleting and retrying..."

    FileUtils.rm_rf KAMAL.config.builder.clone_directory
    clone_repo
    validate!
  end

  private
    def clone_repo
      info "Cloning repo into build directory `#{KAMAL.config.builder.build_directory}`..."

      FileUtils.mkdir_p KAMAL.config.builder.clone_directory
      execute *KAMAL.builder.clone
    end

    def reset
      info "Resetting local clone as `#{KAMAL.config.builder.build_directory}` already exists..."

      KAMAL.builder.clone_reset_steps.each { |step| execute *step }
    rescue SSHKit::Command::Failed => e
      raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
    end

    def validate!
      status = capture_with_info(*KAMAL.builder.clone_status).strip

      unless status.empty?
        raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is dirty, #{status}"
      end

      revision = capture_with_info(*KAMAL.builder.clone_revision).strip
      if revision != Kamal::Git.revision
        raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is not on the correct revision, expected `#{Kamal::Git.revision}` but got `#{revision}`"
      end
    rescue SSHKit::Command::Failed => e
      raise Kamal::Cli::Build::BuildError, "Failed to validate clone: #{e.message}"
    end
end


================================================
FILE: lib/kamal/cli/build/port_forwarding.rb
================================================
require "concurrent/atomic/count_down_latch"

class Kamal::Cli::Build::PortForwarding
  attr_reader :hosts, :port, :ssh_options

  def initialize(hosts, port, **ssh_options)
    @hosts = hosts
    @port = port
    @ssh_options = ssh_options
  end

  def forward
    @done = false
    forward_ports

    yield
  ensure
    stop
  end

  private
    def stop
      @done = true
      @threads.to_a.each(&:join)
    end

    def forward_ports
      ready = Concurrent::CountDownLatch.new(hosts.size)

      @threads = hosts.map do |host|
        Thread.new do
          begin
            Net::SSH.start(host, ssh_options[:user], **ssh_options.except(:user)) do |ssh|
              ssh.forward.remote(port, "localhost", port, "127.0.0.1") do |remote_port, bind_address|
                if remote_port == :error
                  raise "Failed to establish port forward on #{host}"
                else
                  ready.count_down
                end
              end

              ssh.loop(0.1) do
                if @done
                  ssh.forward.cancel_remote(port, "127.0.0.1")
                  break
                else
                  true
                end
              end
            end
          rescue Exception => e
            error "Error setting up port forwarding to #{host}: #{e.class}: #{e.message}"
            error e.backtrace.join("\n")

            raise
          end
        end
      end

      raise "Timed out waiting for port forwarding to be established" unless ready.wait(30)
    end

    def error(message)
      SSHKit.config.output.error(message)
    end
end


================================================
FILE: lib/kamal/cli/build.rb
================================================
class Kamal::Cli::Build < Kamal::Cli::Base
  class BuildError < StandardError; end

  desc "deliver", "Build app and push app image to registry then pull image on servers"
  def deliver
    invoke :push
    invoke :pull
  end

  desc "push", "Build and push app image to registry"
  option :output, type: :string, default: "registry", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
  def push
    cli = self

    # Ensure pre-connect hooks run before the build, they may be needed for a remote builder
    # or the pre-build hooks.
    pre_connect_if_required

    ensure_docker_installed
    login_to_registry_locally if KAMAL.builder.login_to_registry_locally?

    run_hook "pre-build"

    uncommitted_changes = Kamal::Git.uncommitted_changes

    if KAMAL.config.builder.git_clone?
      if uncommitted_changes.present?
        say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
      end

      run_locally do
        Clone.new(self).prepare
      end
    elsif uncommitted_changes.present?
      say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
    end

    forward_local_registry_port_for_remote_builder do
      with_env(KAMAL.config.builder.secrets) do
        run_locally do
          begin
            execute *KAMAL.builder.inspect_builder
          rescue SSHKit::Command::Failed => e
            if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
              warn "Missing compatible builder, so creating a new one first"
              begin
                cli.remove
              rescue SSHKit::Command::Failed
                raise unless e.message =~ /(context not found|no builder|does not exist)/
              end
              cli.create
            else
              raise
            end
          end

          # Get the command here to ensure the Dir.chdir doesn't interfere with it
          push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])

          KAMAL.with_verbosity(:debug) do
            Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
          end
        end
      end
    end
  end

  desc "pull", "Pull app image from registry onto servers"
  def pull
    login_to_registry_remotely unless KAMAL.registry.local?

    forward_local_registry_port(KAMAL.hosts, **KAMAL.config.ssh.options) do
      if (first_hosts = mirror_hosts).any?
        #  Pull on a single host per mirror first to seed them
        say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
        pull_on_hosts(first_hosts)
        say "Pulling image on remaining hosts...", :magenta
        pull_on_hosts(KAMAL.app_hosts - first_hosts)
      else
        pull_on_hosts(KAMAL.app_hosts)
      end
    end
  end

  desc "create", "Create a build setup"
  def create
    if (remote_host = KAMAL.config.builder.remote)
      connect_to_remote_host(remote_host)
    end

    run_locally do
      begin
        debug "Using builder: #{KAMAL.builder.name}"
        execute *KAMAL.builder.create
      rescue SSHKit::Command::Failed => e
        if e.message =~ /stderr=(.*)/
          error "Couldn't create remote builder: #{$1}"
          false
        else
          raise
        end
      end
    end
  end

  desc "remove", "Remove build setup"
  def remove
    run_locally do
      debug "Using builder: #{KAMAL.builder.name}"
      execute *KAMAL.builder.remove
    end
  end

  desc "details", "Show build setup"
  def details
    run_locally do
      puts "Builder: #{KAMAL.builder.name}"
      puts capture(*KAMAL.builder.info)
    end
  end

  desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
  option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
  def dev
    cli = self

    ensure_docker_installed

    docker_included_files = Set.new(Kamal::Docker.included_files)
    git_uncommitted_files = Set.new(Kamal::Git.uncommitted_files)
    git_untracked_files = Set.new(Kamal::Git.untracked_files)

    docker_uncommitted_files = docker_included_files & git_uncommitted_files
    if docker_uncommitted_files.any?
      say "WARNING: Files with uncommitted changes will be present in the dev container:", :yellow
      docker_uncommitted_files.sort.each { |f| say "  #{f}", :yellow }
      say
    end

    docker_untracked_files = docker_included_files & git_untracked_files
    if docker_untracked_files.any?
      say "WARNING: Untracked files will be present in the dev container:", :yellow
      docker_untracked_files.sort.each { |f| say "  #{f}", :yellow }
      say
    end

    with_env(KAMAL.config.builder.secrets) do
      run_locally do
        build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
        KAMAL.with_verbosity(:debug) do
          execute(*build)
        end
      end
    end
  end

  private
    def connect_to_remote_host(remote_host)
      remote_uri = URI.parse(remote_host)
      if remote_uri.scheme == "ssh"
        host = SSHKit::Host.new(
          hostname: remote_uri.host,
          ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
        )
        on(host, options) do
          execute "true"
        end
      end
    end

    def mirror_hosts
      if KAMAL.app_hosts.many?
        mirror_hosts = Concurrent::Hash.new
        on(KAMAL.app_hosts) do |host|
          first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
          mirror_hosts[first_mirror] ||= host.to_s if first_mirror
        rescue SSHKit::Command::Failed => e
          raise unless e.message =~ /error calling index: reflect: slice index out of range/
        end
        mirror_hosts.values
      else
        []
      end
    end

    def pull_on_hosts(hosts)
      on(hosts) do
        execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
        execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
        execute *KAMAL.builder.pull
        execute *KAMAL.builder.validate_image
      end
    end

    def login_to_registry_locally
      run_locally do
        if KAMAL.registry.local?
          execute *KAMAL.registry.setup
        else
          execute *KAMAL.registry.login
        end
      end
    end

    def login_to_registry_remotely
      on(KAMAL.app_hosts) do
        execute *KAMAL.registry.login
      end
    end

    def forward_local_registry_port_for_remote_builder(&block)
      if KAMAL.builder.remote?
        remote_uri = URI(KAMAL.config.builder.remote)
        forward_local_registry_port([ remote_uri.host ], **remote_builder_ssh_options(remote_uri), &block)
      else
        yield
      end
    end

    def forward_local_registry_port(hosts, **ssh_options, &block)
      if KAMAL.config.registry.local?
        say "Setting up local registry port forwarding to #{hosts.join(', ')}..."
        PortForwarding.new(hosts, KAMAL.config.registry.local_port, **ssh_options).forward(&block)
      else
        yield
      end
    end

    def remote_builder_ssh_options(remote_uri)
      { user: remote_uri.user,
        port: remote_uri.port,
        keepalive: KAMAL.config.ssh.options[:keepalive],
        keepalive_interval: KAMAL.config.ssh.options[:keepalive_interval],
        logger: KAMAL.config.ssh.options[:logger]
      }.compact
    end
end


================================================
FILE: lib/kamal/cli/healthcheck/barrier.rb
================================================
require "concurrent/ivar"

class Kamal::Cli::Healthcheck::Barrier
  def initialize
    @ivar = Concurrent::IVar.new
  end

  def close
    set(false)
  end

  def open
    set(true)
  end

  def wait
    unless opened?
      raise Kamal::Cli::Healthcheck::Error.new("Halted at barrier")
    end
  end

  private
    def opened?
      @ivar.value
    end

    def set(value)
      @ivar.set(value)
      true
    rescue Concurrent::MultipleAssignmentError
      false
    end
end


================================================
FILE: lib/kamal/cli/healthcheck/error.rb
================================================
class Kamal::Cli::Healthcheck::Error < StandardError
end


================================================
FILE: lib/kamal/cli/healthcheck/poller.rb
================================================
module Kamal::Cli::Healthcheck::Poller
  extend self

  def wait_for_healthy(&block)
    attempt = 1
    timeout_at = Time.now + KAMAL.config.deploy_timeout
    readiness_delay = KAMAL.config.readiness_delay

    begin
      status = block.call

      if status == "running"
        # Wait for the readiness delay and confirm it is still running
        if readiness_delay > 0
          info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
          sleep readiness_delay
          status = block.call
        end
      end

      unless %w[ running healthy ].include?(status)
        raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})"
      end
    rescue Kamal::Cli::Healthcheck::Error => e
      time_left = timeout_at - Time.now
      if time_left > 0
        sleep [ attempt, time_left ].min
        attempt += 1
        retry
      else
        raise
      end
    end

    info "Container is healthy!"
  end

  private
    def info(message)
      SSHKit.config.output.info(message)
    end
end


================================================
FILE: lib/kamal/cli/lock.rb
================================================
class Kamal::Cli::Lock < Kamal::Cli::Base
  desc "status", "Report lock status"
  def status
    handle_missing_lock do
      on(KAMAL.primary_host) do
        puts capture_with_debug(*KAMAL.lock.status)
      end
    end
  end

  desc "acquire", "Acquire the deploy lock"
  option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
  def acquire
    message = options[:message]
    ensure_run_directory

    raise_if_locked do
      on(KAMAL.primary_host) do
        execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
      end
      say "Acquired the deploy lock"
    end
  end

  desc "release", "Release the deploy lock"
  def release
    handle_missing_lock do
      on(KAMAL.primary_host) do
        execute *KAMAL.lock.release, verbosity: :debug
      end
      say "Released the deploy lock"
    end
  end

  private
    def handle_missing_lock
      yield
    rescue SSHKit::Runner::ExecuteError => e
      if e.message =~ /No such file or directory/
        say "There is no deploy lock"
      else
        raise
      end
    end
end


================================================
FILE: lib/kamal/cli/main.rb
================================================
class Kamal::Cli::Main < Kamal::Cli::Base
  desc "setup", "Setup all accessories, push the env, and deploy app to servers"
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
  def setup
    print_runtime do
      with_lock do
        invoke_options = deploy_options

        say "Ensure Docker is installed...", :magenta
        invoke "kamal:cli:server:bootstrap", [], invoke_options

        deploy(boot_accessories: true)
      end
    end
  end

  desc "deploy", "Deploy app to servers"
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
  def deploy(boot_accessories: false)
    runtime = print_runtime do
      invoke_options = deploy_options

      if options[:skip_push]
        say "Pull app image...", :magenta
        invoke "kamal:cli:build:pull", [], invoke_options
      else
        say "Build and push app image...", :magenta
        invoke "kamal:cli:build:deliver", [], invoke_options
      end

      with_lock do
        run_hook "pre-deploy", secrets: true

        say "Ensure kamal-proxy is running...", :magenta
        invoke "kamal:cli:proxy:boot", [], invoke_options

        invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories

        say "Detect stale containers...", :magenta
        invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)

        invoke "kamal:cli:app:boot", [], invoke_options

        say "Prune old containers and images...", :magenta
        invoke "kamal:cli:prune:all", [], invoke_options
      end
    end

    run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
  end

  desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
  def redeploy
    runtime = print_runtime do
      invoke_options = deploy_options

      if options[:skip_push]
        say "Pull app image...", :magenta
        invoke "kamal:cli:build:pull", [], invoke_options
      else
        say "Build and push app image...", :magenta
        invoke "kamal:cli:build:deliver", [], invoke_options
      end

      with_lock do
        run_hook "pre-deploy", secrets: true

        say "Detect stale containers...", :magenta
        invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)

        invoke "kamal:cli:app:boot", [], invoke_options
      end
    end

    run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
  end

  desc "rollback [VERSION]", "Rollback app to VERSION"
  def rollback(version)
    rolled_back = false
    runtime = print_runtime do
      with_lock do
        invoke_options = deploy_options

        KAMAL.config.version = version
        old_version = nil

        if container_available?(version)
          run_hook "pre-deploy", secrets: true

          invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
          rolled_back = true
        else
          say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
        end
      end
    end

    run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
  end

  desc "details", "Show details about all containers"
  def details
    invoke "kamal:cli:proxy:details"
    invoke "kamal:cli:app:details"
    invoke "kamal:cli:accessory:details", [ "all" ]
  end

  desc "audit", "Show audit log from servers"
  def audit
    quiet = options[:quiet]
    on(KAMAL.hosts) do |host|
      puts_by_host host, capture_with_info(*KAMAL.auditor.reveal), quiet: quiet
    end
  end

  desc "config", "Show combined config (including secrets!)"
  def config
    run_locally do
      puts Kamal::Utils.redacted(KAMAL.config.to_h).to_yaml
    end
  end

  desc "docs [SECTION]", "Show Kamal configuration documentation"
  def docs(section = nil)
    case section
    when NilClass
      puts Kamal::Configuration.validation_doc
    else
      puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
    end
  rescue NameError
    puts "No documentation found for #{section}"
  end

  desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
  option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
  def init
    require "fileutils"

    if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist?
      puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
    else
      FileUtils.mkdir_p deploy_file.dirname
      FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
      puts "Created configuration file in config/deploy.yml"
    end

    unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist?
      FileUtils.mkdir_p secrets_file.dirname
      FileUtils.cp_r Pathname.new(File.expand_path("templates/secrets", __dir__)), secrets_file
      puts "Created .kamal/secrets file"
    end

    unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
      hooks_dir.mkpath
      Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
        FileUtils.cp sample_hook, hooks_dir, preserve: true
      end
      puts "Created sample hooks in .kamal/hooks"
    end

    if options[:bundle]
      if (binstub = Pathname.new(File.expand_path("bin/kamal"))).exist?
        puts "Binstub already exists in bin/kamal (remove first to create a new one)"
      else
        puts "Adding Kamal to Gemfile and bundle..."
        run_locally do
          execute :bundle, :add, :kamal
          execute :bundle, :binstubs, :kamal
        end
        puts "Created binstub file in bin/kamal"
      end
    end
  end

  desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  def remove
    confirming "This will remove all containers and images. Are you sure?" do
      with_lock do
        invoke "kamal:cli:app:remove", [], options.without(:confirmed)
        invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
        invoke "kamal:cli:accessory:remove", [ "all" ], options
        invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
      end
    end
  end

  desc "upgrade", "Upgrade from Kamal 1.x to 2.0"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
  def upgrade
    confirming "This will replace Traefik with kamal-proxy and restart all accessories" do
      with_lock do
        if options[:rolling]
          KAMAL.hosts.each do |host|
            KAMAL.with_specific_hosts(host) do
              say "Upgrading #{host}...", :magenta
              if KAMAL.app_hosts.include?(host)
                invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true, rolling: false)
                reset_invocation(Kamal::Cli::Proxy)
              end
              if KAMAL.accessory_hosts.include?(host)
                invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true, rolling: false)
                reset_invocation(Kamal::Cli::Accessory)
              end
              say "Upgraded #{host}", :magenta
            end
          end
        else
          say "Upgrading all hosts...", :magenta
          invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true)
          invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true)
          say "Upgraded all hosts", :magenta
        end
      end
    end
  end

  desc "version", "Show Kamal version"
  def version
    puts Kamal::VERSION
  end

  desc "accessory", "Manage accessories (db/redis/search)"
  subcommand "accessory", Kamal::Cli::Accessory

  desc "app", "Manage application"
  subcommand "app", Kamal::Cli::App

  desc "build", "Build application image"
  subcommand "build", Kamal::Cli::Build

  desc "lock", "Manage the deploy lock"
  subcommand "lock", Kamal::Cli::Lock

  desc "proxy", "Manage kamal-proxy"
  subcommand "proxy", Kamal::Cli::Proxy

  desc "prune", "Prune old application images and containers"
  subcommand "prune", Kamal::Cli::Prune

  desc "registry", "Login and -out of the image registry"
  subcommand "registry", Kamal::Cli::Registry

  desc "secrets", "Helpers for extracting secrets"
  subcommand "secrets", Kamal::Cli::Secrets

  desc "server", "Bootstrap servers with curl and Docker"
  subcommand "server", Kamal::Cli::Server

  private
    def container_available?(version)
      begin
        on(KAMAL.app_hosts) do
          KAMAL.roles_on(host).each do |role|
            container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
            raise "Container not found" unless container_id.present?
          end
        end
      rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
        if e.message =~ /Container not found/
          say "Error looking for container version #{version}: #{e.message}"
          return false
        else
          raise
        end
      end

      true
    end

    def deploy_options
      base_options = options.without("skip_push")
      base_options = base_options.except("no_cache") unless base_options["no_cache"]
      { "version" => KAMAL.config.version }.merge(base_options)
    end
end


================================================
FILE: lib/kamal/cli/proxy.rb
================================================
class Kamal::Cli::Proxy < Kamal::Cli::Base
  desc "boot", "Boot proxy on servers"
  def boot
    with_lock do
      on(KAMAL.hosts) do |host|
        execute *KAMAL.docker.create_network
      rescue SSHKit::Command::Failed => e
        raise unless e.message.include?("already exists")
      end

      on(KAMAL.proxy_hosts) do |host|
        execute *KAMAL.registry.login

        version = capture_with_info(*KAMAL.proxy(host).version).strip.presence

        if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
          raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION}"
        end
        execute *KAMAL.proxy(host).ensure_apps_config_directory
        execute *KAMAL.proxy(host).start_or_run
      end
    end
  end

  desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
  option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
  option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
  option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
  option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
  option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
  option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image"
  option :repository, type: :string, default: nil, desc: "Repository for the proxy image"
  option :image_version, type: :string, default: nil, desc: "Version of the proxy to run"
  option :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on"
  option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode"
  option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
  def boot_config(subcommand)
    say "The proxy boot_config command is deprecated - set the config in the deploy YAML at proxy/run instead", :yellow
    proxy_boot_config = KAMAL.config.proxy_boot

    case subcommand
    when "set"
      boot_options = [
        *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]),
        *(proxy_boot_config.logging_args(options[:log_max_size])),
        *("--expose=#{options[:metrics_port]}" if options[:metrics_port]),
        *options[:docker_options].map { |option| "--#{option}" }
      ]

      image = [
        options[:registry].presence,
        options[:repository].presence || proxy_boot_config.repository_name,
        proxy_boot_config.image_name
      ].compact.join("/")

      image_version = options[:image_version]

      run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact
      run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any?

      on(KAMAL.proxy_hosts) do |host|
        proxy = KAMAL.proxy(host)
        execute(*proxy.ensure_proxy_directory)
        if boot_options != proxy_boot_config.default_boot_options
          upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.options_file
        else
          execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
        end

        if image != proxy_boot_config.image_default
          upload! StringIO.new(image), proxy_boot_config.image_file
        else
          execute *proxy.reset_image, raise_on_non_zero_exit: false
        end

        if image_version
          upload! StringIO.new(image_version), proxy_boot_config.image_version_file
        else
          execute *proxy.reset_image_version, raise_on_non_zero_exit: false
        end

        if run_command
          upload! StringIO.new(run_command), proxy_boot_config.run_command_file
        else
          execute *proxy.reset_run_command, raise_on_non_zero_exit: false
        end
      end
    when "get"

      on(KAMAL.proxy_hosts) do |host|
        puts "Host #{host}: #{capture_with_info(*KAMAL.proxy(host).boot_config)}"
      end
    when "reset"
      on(KAMAL.proxy_hosts) do |host|
        proxy = KAMAL.proxy(host)
        execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
        execute *proxy.reset_image, raise_on_non_zero_exit: false
        execute *proxy.reset_image_version, raise_on_non_zero_exit: false
        execute *proxy.reset_run_command, raise_on_non_zero_exit: false
      end
    else
      raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
    end
  end

  desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
  option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  def reboot
    confirming "This will cause a brief outage on each host. Are you sure?" do
      with_lock do
        host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
        host_groups.each do |hosts|
          host_list = Array(hosts).join(",")
          run_hook "pre-proxy-reboot", hosts: host_list
          on(hosts) do |host|
            proxy = KAMAL.proxy(host)
            execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
            execute *KAMAL.registry.login

            "Stopping and removing kamal-proxy on #{host}, if running..."
            execute *proxy.stop, raise_on_non_zero_exit: false
            execute *proxy.remove_container
            execute *proxy.ensure_apps_config_directory

            execute *proxy.run
          end
          run_hook "post-proxy-reboot", hosts: host_list
        end
      end
    end
  end

  desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
  option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
  def upgrade
    invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)

    confirming "This will cause a brief outage on each host. Are you sure?" do
      host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
      host_groups.each do |hosts|
        host_list = Array(hosts).join(",")
        say "Upgrading proxy on #{host_list}...", :magenta
        run_hook "pre-proxy-reboot", hosts: host_list
        on(hosts) do |host|
          proxy = KAMAL.proxy(host)
          execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
          execute *KAMAL.registry.login

          "Stopping and removing Traefik on #{host}, if running..."
          execute *proxy.cleanup_traefik

          "Stopping and removing kamal-proxy on #{host}, if running..."
          execute *proxy.stop, raise_on_non_zero_exit: false
          execute *proxy.remove_container
          execute *proxy.remove_image
        end

        KAMAL.with_specific_hosts(hosts) do
          invoke "kamal:cli:proxy:boot", [], invoke_options
          reset_invocation(Kamal::Cli::Proxy)
          invoke "kamal:cli:app:boot", [], invoke_options
          reset_invocation(Kamal::Cli::App)
          invoke "kamal:cli:prune:all", [], invoke_options
          reset_invocation(Kamal::Cli::Prune)
        end

        run_hook "post-proxy-reboot", hosts: host_list
        say "Upgraded proxy on #{host_list}", :magenta
      end
    end
  end

  desc "start", "Start existing proxy container on servers"
  def start
    with_lock do
      on(KAMAL.proxy_hosts) do |host|
        execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
        execute *KAMAL.proxy(host).start
      end
    end
  end

  desc "stop", "Stop existing proxy container on servers"
  def stop
    with_lock do
      on(KAMAL.proxy_hosts) do |host|
        execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
        execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
      end
    end
  end

  desc "restart", "Restart existing proxy container on servers"
  def restart
    with_lock do
      stop
      start
    end
  end

  desc "details", "Show details about proxy container from servers"
  def details
    quiet = options[:quiet]
    on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy(host).info), type: "Proxy", quiet: quiet }
  end

  desc "logs", "Show log lines from proxy on servers"
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
  def logs
    grep = options[:grep]
    timestamps = !options[:skip_timestamps]

    if options[:follow]
      run_locally do
        proxy = KAMAL.proxy(KAMAL.primary_host)
        info "Following logs on #{KAMAL.primary_host}..."
        info proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
        exec proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
      end
    else
      since = options[:since]
      lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set

      on(KAMAL.proxy_hosts) do |host|
        puts_by_host host, capture(*KAMAL.proxy(host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
      end
    end
  end

  desc "remove", "Remove proxy container and image from servers"
  option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
  def remove
    with_lock do
      if removal_allowed?(options[:force])
        stop
        remove_container
        remove_image
        remove_proxy_directory
      end
    end
  end

  desc "remove_container", "Remove proxy container from servers", hide: true
  def remove_container
    with_lock do
      on(KAMAL.proxy_hosts) do
        execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
        execute *KAMAL.proxy(host).remove_container
      end
    end
  end

  desc "remove_image", "Remove proxy image from servers", hide: true
  def remove_image
    with_lock do
      on(KAMAL.proxy_hosts) do
        execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
        execute *KAMAL.proxy(host).remove_image
      end
    end
  end

  desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
  def remove_proxy_directory
    with_lock do
      on(KAMAL.proxy_hosts) do
        execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
      end
    end
  end

  private
    def removal_allowed?(force)
      on(KAMAL.proxy_hosts) do |host|
        app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
        raise "The are other applications installed on #{host}" if app_count > 0
      end

      true
    rescue SSHKit::Runner::ExecuteError => e
      raise unless e.message.include?("The are other applications installed on")

      if force
        say "Forcing, so removing the proxy, even though other apps are installed", :magenta
      else
        say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
      end

      force
    end
end


================================================
FILE: lib/kamal/cli/prune.rb
================================================
class Kamal::Cli::Prune < Kamal::Cli::Base
  desc "all", "Prune unused images and stopped containers"
  def all
    with_lock do
      containers
      images
    end
  end

  desc "images", "Prune unused images"
  def images
    with_lock do
      on(KAMAL.hosts) do
        execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
        execute *KAMAL.prune.dangling_images
        execute *KAMAL.prune.tagged_images
      end
    end
  end

  desc "containers", "Prune all stopped containers, except the last n (default 5)"
  option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
  def containers
    retain = options.fetch(:retain, KAMAL.config.retain_containers)
    raise "retain must be at least 1" if retain < 1

    with_lock do
      on(KAMAL.hosts) do
        execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
        execute *KAMAL.prune.app_containers(retain: retain)
      end
    end
  end
end


================================================
FILE: lib/kamal/cli/registry.rb
================================================
class Kamal::Cli::Registry < Kamal::Cli::Base
  desc "setup", "Setup local registry or log in to remote registry locally and remotely"
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
  def setup
    ensure_docker_installed unless options[:skip_local]

    if KAMAL.registry.local?
      run_locally    { execute *KAMAL.registry.setup } unless options[:skip_local]
    else
      run_locally    { execute *KAMAL.registry.login } unless options[:skip_local]
      on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
    end
  end

  desc "remove", "Remove local registry or log out of remote registry locally and remotely"
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
  def remove
    if KAMAL.registry.local?
      run_locally    { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
    else
      run_locally    { execute *KAMAL.registry.logout } unless options[:skip_local]
      on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
    end
  end

  desc "login", "Log in to remote registry locally and remotely"
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
  def login
    if KAMAL.registry.local?
      raise "Cannot use login command with a local registry. Use `kamal registry setup` instead."
    end

    setup
  end

  desc "logout", "Log out of remote registry locally and remotely"
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
  def logout
    if KAMAL.registry.local?
      raise "Cannot use logout command with a local registry. Use `kamal registry remove` instead."
    end

    remove
  end
end


================================================
FILE: lib/kamal/cli/secrets.rb
================================================
class Kamal::Cli::Secrets < Kamal::Cli::Base
  desc "fetch [SECRETS...]", "Fetch secrets from a vault"
  option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
  option :account, type: :string, required: false, desc: "The account identifier or username"
  option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
  option :inline, type: :boolean, required: false, hidden: true
  def fetch(*secrets)
    adapter = initialize_adapter(options[:adapter])

    if adapter.requires_account? && options[:account].blank?
      return puts "No value provided for required options '--account'"
    end

    results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
    json = JSON.dump(results)

    return_or_puts options[:inline] ? json.shellescape : json, inline: options[:inline]
  end

  desc "extract", "Extract a single secret from the results of a fetch call"
  option :inline, type: :boolean, required: false, hidden: true
  def extract(name, secrets)
    parsed_secrets = JSON.parse(secrets)
    value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last

    raise "Could not find secret #{name}" if value.nil?

    return_or_puts value, inline: options[:inline]
  end

  desc "print", "Print the secrets (for debugging)"
  def print
    KAMAL.config.secrets.to_h.each do |key, value|
      puts "#{key}=#{value}"
    end
  end

  private
    def initialize_adapter(adapter)
      Kamal::Secrets::Adapters.lookup(adapter)
    end

    def return_or_puts(value, inline: nil)
      if inline
        value
      else
        puts value
      end
    end
end


================================================
FILE: lib/kamal/cli/server.rb
================================================
class Kamal::Cli::Server < Kamal::Cli::Base
  desc "exec", "Run a custom command on the server (use --help to show options)"
  option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
  def exec(*cmd)
    pre_connect_if_required

    cmd = Kamal::Utils.join_commands(cmd)
    hosts = KAMAL.hosts
    quiet = options[:quiet]

    case
    when options[:interactive]
      host = KAMAL.primary_host

      say "Running '#{cmd}' on #{host} interactively...", :magenta

      run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
    else
      say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta

      on(hosts) do |host|
        execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
        puts_by_host host, capture_with_info(cmd), quiet: quiet
      end
    end
  end

  desc "bootstrap", "Set up Docker to run Kamal apps"
  def bootstrap
    with_lock do
      missing = []

      on(KAMAL.hosts) do |host|
        unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
          if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
            info "Missing Docker on #{host}. Installing…"
            execute *KAMAL.docker.install

            unless execute(*KAMAL.docker.root?, raise_on_non_zero_exit: false) ||
                   execute(*KAMAL.docker.in_docker_group?, raise_on_non_zero_exit: false)
              execute *KAMAL.docker.add_to_docker_group
              begin
                execute *KAMAL.docker.refresh_session
              rescue IOError
                info "Session refreshed due to group change."
              end
            end
          else
            missing << host
          end
        end
      end

      if missing.any?
        raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
      end

      run_hook "docker-setup"
    end
  end
end


================================================
FILE: lib/kamal/cli/templates/deploy.yml
================================================
# Name of your application. Used to uniquely configure containers.
service: my-app

# Name of the container image.
image: my-user/my-app

# Deploy to these servers.
servers:
  web:
    - 192.168.0.1
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
  ssl: true
  host: app.example.com
  # Proxy connects to your container on port 80 by default.
  # app_port: 3000

# Credentials for your image host.
registry:
  server: localhost:5555
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  # username: my-user

  # Always use an access token rather than real password (pulled from .kamal/secrets).
  # password:
  #   - KAMAL_REGISTRY_PASSWORD

# Configure builder setup.
builder:
  arch: amd64
  # Pass in additional build args needed for your Dockerfile.
  # args:
  #   RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>

# Inject ENV variables into containers (secrets come from .kamal/secrets).
#
# env:
#   clear:
#     DB_HOST: 192.168.0.2
#   secret:
#     - RAILS_MASTER_KEY

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal app logs -r job" will tail logs from the first server in the job section.
#
# aliases:
#   shell: app exec --interactive --reuse "bash"

# Use a different ssh user than root
#
# ssh:
#   user: app

# Use a persistent storage volume.
#
# volumes:
#   - "app_storage:/app/storage"

# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# asset_path: /app/public/assets

# Configure rolling deploys by setting a wait time between batches of restarts.
#
# boot:
#   limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
#   wait: 2

# Use accessory services (secrets come from .kamal/secrets).
#
# accessories:
#   db:
#     image: mysql:8.0
#     host: 192.168.0.2
#     port: 3306
#     env:
#       clear:
#         MYSQL_ROOT_HOST: '%'
#       secret:
#         - MYSQL_ROOT_PASSWORD
#     files:
#       - config/mysql/production.cnf:/etc/mysql/my.cnf
#       - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
#     directories:
#       - data:/var/lib/mysql
#   redis:
#     image: valkey/valkey:8
#     host: 192.168.0.2
#     port: 6379
#     directories:
#       - data:/data


================================================
FILE: lib/kamal/cli/templates/sample_hooks/docker-setup.sample
================================================
#!/bin/sh

echo "Docker set up on $KAMAL_HOSTS..."


================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-app-boot.sample
================================================
#!/bin/sh

echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."


================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-deploy.sample
================================================
#!/bin/sh

# A sample post-deploy hook
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME

echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"


================================================
FILE: lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample
================================================
#!/bin/sh

echo "Rebooted kamal-proxy on $KAMAL_HOSTS"


================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample
================================================
#!/bin/sh

echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."


================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-build.sample
================================================
#!/bin/sh

# A sample pre-build hook
#
# Checks:
# 1. We have a clean checkout
# 2. A remote is configured
# 3. The branch has been pushed to the remote
# 4. The version we are deploying matches the remote
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)

if [ -n "$(git status --porcelain)" ]; then
  echo "Git checkout is not clean, aborting..." >&2
  git status --porcelain >&2
  exit 1
fi

first_remote=$(git remote)

if [ -z "$first_remote" ]; then
  echo "No git remote set, aborting..." >&2
  exit 1
fi

current_branch=$(git branch --show-current)

if [ -z "$current_branch" ]; then
  echo "Not on a git branch, aborting..." >&2
  exit 1
fi

remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)

if [ -z "$remote_head" ]; then
  echo "Branch not pushed to remote, aborting..." >&2
  exit 1
fi

if [ "$KAMAL_VERSION" != "$remote_head" ]; then
  echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
  exit 1
fi

exit 0


================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-connect.sample
================================================
#!/usr/bin/env ruby

# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME

hosts = ENV["KAMAL_HOSTS"].split(",")
results = nil
max = 3

elapsed = Benchmark.realtime do
  results = hosts.map do |host|
    Thread.new do
      tries = 1

      begin
        Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
      rescue SocketError
        if tries < max
          puts "Retrying DNS warmup: #{host}"
          tries += 1
          sleep rand
          retry
        else
          puts "DNS warmup failed: #{host}"
          host
        end
      end

      tries
    end
  end.map(&:value)
end

retries = results.sum - hosts.size
nopes = results.count { |r| r == max }

puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]


================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-deploy.sample
================================================
#!/usr/bin/env ruby

# A sample pre-deploy hook
#
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
#
# Fails unless the combined status is "success"
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_COMMAND
# KAMAL_SUBCOMMAND
# KAMAL_ROLES (if set)
# KAMAL_DESTINATION (if set)

# Only check the build status for production deployments
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
  exit 0
end

require "bundler/inline"

# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
  source "https://rubygems.org"

  gem "octokit"
  gem "faraday-retry"
end

MAX_ATTEMPTS = 72
ATTEMPTS_GAP = 10

def exit_with_error(message)
  $stderr.puts message
  exit 1
end

class GithubStatusChecks
  attr_reader :remote_url, :git_sha, :github_client, :combined_status

  def initialize
    @remote_url = github_repo_from_remote_url
    @git_sha = `git rev-parse HEAD`.strip
    @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
    refresh!
  end

  def refresh!
    @combined_status = github_client.combined_status(remote_url, git_sha)
  end

  def state
    combined_status[:state]
  end

  def first_status_url
    first_status = combined_status[:statuses].find { |status| status[:state] == state }
    first_status && first_status[:target_url]
  end

  def complete_count
    combined_status[:statuses].count { |status| status[:state] != "pending"}
  end

  def total_count
    combined_status[:statuses].count
  end

  def current_status
    if total_count > 0
      "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
    else
      "Build not started..."
    end
  end

  private
    def github_repo_from_remote_url
      url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
      if url.start_with?("https://github.com/")
        url.delete_prefix("https://github.com/")
      elsif url.start_with?("git@github.com:")
        url.delete_prefix("git@github.com:")
      else
        url
      end
    end
end


$stdout.sync = true

begin
  puts "Checking build status..."

  attempts = 0
  checks = GithubStatusChecks.new

  loop do
    case checks.state
    when "success"
      puts "Checks passed, see #{checks.first_status_url}"
      exit 0
    when "failure"
      exit_with_error "Checks failed, see #{checks.first_status_url}"
    when "pending"
      attempts += 1
    end

    exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS

    puts checks.current_status
    sleep(ATTEMPTS_GAP)
    checks.refresh!
  end
rescue Octokit::NotFound
  exit_with_error "Build status could not be found"
end


================================================
FILE: lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample
================================================
#!/bin/sh

echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."


================================================
FILE: lib/kamal/cli/templates/secrets
================================================
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.

# Option 1: Read secrets from the environment
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)

# Option 3: Read secrets via kamal secrets helpers
# These will handle logging in and fetching the secrets in as few calls as possible
# There are adapters for 1Password, LastPass + Bitwarden
#
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)


================================================
FILE: lib/kamal/cli.rb
================================================
module Kamal::Cli
  class BootError < StandardError; end
  class HookError < StandardError; end
  class LockError < StandardError; end
  class DependencyError < StandardError; end
end

# SSHKit uses instance eval, so we need a global const for ergonomics
KAMAL = Kamal::Commander.new


================================================
FILE: lib/kamal/commander/specifics.rb
================================================
class Kamal::Commander::Specifics
  attr_reader :primary_host, :primary_role, :hosts, :roles
  delegate :stable_sort!, to: Kamal::Utils

  def initialize(config, specific_hosts, specific_roles)
    @config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles

    @roles, @hosts = specified_roles, specified_hosts

    @primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
    @primary_role = primary_or_first_role(roles_on(primary_host))

    stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
    sort_primary_role_hosts_first!(hosts)
  end

  def roles_on(host)
    roles.select { |role| role.hosts.include?(host.to_s) }
  end

  def app_hosts
    @app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts)
  end

  def proxy_hosts
    config.proxy_hosts & specified_hosts
  end

  def accessory_hosts
    config.accessories.flat_map(&:hosts) & specified_hosts
  end

  private
    attr_reader :config, :specific_hosts, :specific_roles

    def primary_specific_role
      primary_or_first_role(specific_roles) if specific_roles.present?
    end

    def primary_or_first_role(roles)
      roles.detect { |role| role == config.primary_role } || roles.first
    end

    def specified_roles
      (specific_roles || config.roles) \
        .select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
    end

    def specified_hosts
      specified_hosts = specific_hosts || config.all_hosts

      if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
        specified_hosts.select { |host| specific_role_hosts.include?(host) }
      else
        specified_hosts
      end
    end

    def sort_primary_role_hosts_first!(hosts)
      stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
    end
end


================================================
FILE: lib/kamal/commander.rb
================================================
require "active_support/core_ext/enumerable"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/object/blank"

class Kamal::Commander
  attr_accessor :verbosity, :holding_lock, :connected
  attr_reader :specific_roles, :specific_hosts
  delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics

  def initialize
    reset
  end

  def reset
    self.verbosity = :info
    self.holding_lock = ENV["KAMAL_LOCK"] == "true"
    self.connected = false
    @specifics = @specific_roles = @specific_hosts = nil
    @config = @config_kwargs = nil
    @commands = {}
  end

  def config
    @config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
      @config_kwargs = nil
      configure_sshkit_with(config)
    end
  end

  def configure(**kwargs)
    @config, @config_kwargs = nil, kwargs
  end

  def configured?
    @config || @config_kwargs
  end

  def specific_primary!
    @specifics = nil
    if specific_roles.present?
      self.specific_hosts = [ specific_roles.first.primary_host ]
    else
      self.specific_hosts = [ config.primary_host ]
    end
  end

  def specific_roles=(role_names)
    @specifics = nil
    @specific_roles = if role_names.present?
      filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
      raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
      filtered
    end
  end

  def specific_hosts=(hosts)
    @specifics = nil
    @specific_hosts = if hosts.present?
      filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
      raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
      filtered
    end
  end

  def with_specific_hosts(hosts)
    original_hosts, self.specific_hosts = specific_hosts, hosts
    yield
  ensure
    self.specific_hosts = original_hosts
  end

  def accessory_names
    config.accessories&.collect(&:name) || []
  end

  def app(role: nil, host: nil)
    Kamal::Commands::App.new(config, role: role, host: host)
  end

  def accessory(name)
    Kamal::Commands::Accessory.new(config, name: name)
  end

  def auditor(**details)
    Kamal::Commands::Auditor.new(config, **details)
  end

  def builder
    @commands[:builder] ||= Kamal::Commands::Builder.new(config)
  end

  def docker
    @commands[:docker] ||= Kamal::Commands::Docker.new(config)
  end

  def hook
    @commands[:hook] ||= Kamal::Commands::Hook.new(config)
  end

  def lock
    @commands[:lock] ||= Kamal::Commands::Lock.new(config)
  end

  def proxy(host)
    Kamal::Commands::Proxy.new(config, host: host)
  end

  def prune
    @commands[:prune] ||= Kamal::Commands::Prune.new(config)
  end

  def registry
    @commands[:registry] ||= Kamal::Commands::Registry.new(config)
  end

  def server
    @commands[:server] ||= Kamal::Commands::Server.new(config)
  end

  def alias(name)
    config.aliases[name]
  end

  def resolve_alias(name)
    if @config
      @config.aliases[name]&.command
    else
      raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
      raw_config[:aliases]&.dig(name)
    end
  end

  def with_verbosity(level)
    old_level = self.verbosity

    self.verbosity = level
    SSHKit.config.output_verbosity = level

    yield
  ensure
    self.verbosity = old_level
    SSHKit.config.output_verbosity = old_level
  end

  def holding_lock?
    self.holding_lock
  end

  def connected?
    self.connected
  end

  private
    # Lazy setup of SSHKit
    def configure_sshkit_with(config)
      SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
      SSHKit::Backend::Netssh.configure do |sshkit|
        sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
        sshkit.dns_retries = config.sshkit.dns_retries
        sshkit.ssh_options = config.ssh.options
      end
      SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
      SSHKit.config.output_verbosity = verbosity
    end

    def specifics
      @specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
    end
end


================================================
FILE: lib/kamal/commands/accessory/proxy.rb
================================================
module Kamal::Commands::Accessory::Proxy
  delegate :container_name, to: :"config.proxy_boot", prefix: :proxy

  def deploy(target:)
    proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
  end

  def remove
    proxy_exec :remove, service_name
  end

  private
    def proxy_exec(*command)
      docker :exec, proxy_container_name, "kamal-proxy", *command
    end
end


================================================
FILE: lib/kamal/commands/accessory.rb
================================================
class Kamal::Commands::Accessory < Kamal::Commands::Base
  include Proxy

  attr_reader :accessory_config
  delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
           :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
           :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
           to: :accessory_config

  def initialize(config, name:)
    super(config)
    @accessory_config = config.accessory(name)
  end

  def run(host: nil)
    docker :run,
      "--name", service_name,
      "--detach",
      "--restart", "unless-stopped",
      *network_args,
      *config.logging_args,
      *publish_args,
      *([ "--env", "KAMAL_HOST=\"#{host}\"" ] if host),
      *env_args,
      *volume_args,
      *label_args,
      *option_args,
      image,
      cmd
  end

  def start
    docker :container, :start, service_name
  end

  def stop
    docker :container, :stop, service_name
  end

  def info(all: false, quiet: false)
    docker :ps, *("-a" if all), *("-q" if quiet), *service_filter
  end

  def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
    pipe \
      docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
      ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
  end

  def follow_logs(timestamps: true, grep: nil, grep_options: nil)
    run_over_ssh \
      pipe \
        docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
        (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
  end

  def execute_in_existing_container(*command, interactive: false)
    docker :exec,
      (docker_interactive_args if interactive),
      service_name,
      *command
  end

  def execute_in_new_container(*command, interactive: false)
    docker :run,
      (docker_interactive_args if interactive),
      "--rm",
      *network_args,
      *env_args,
      *volume_args,
      *option_args,
      image,
      *command
  end

  def execute_in_existing_container_over_ssh(*command)
    run_over_ssh execute_in_existing_container(*command, interactive: true)
  end

  def execute_in_new_container_over_ssh(*command)
    run_over_ssh execute_in_new_container(*command, interactive: true)
  end

  def run_over_ssh(command)
    super command, host: hosts.first
  end

  def ensure_local_file_present(local_file)
    if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
      raise "Missing file: #{local_file}"
    end
  end

  def pull_image
    docker :image, :pull, image
  end

  def remove_service_directory
    [ :rm, "-rf", service_name ]
  end

  def remove_container
    docker :container, :prune, "--force", *service_filter
  end

  def remove_image
    docker :image, :rm, "--force", image
  end

  def ensure_env_directory
    make_directory env_directory
  end

  private
    def service_filter
      [ "--filter", "label=service=#{service_name}" ]
    end
end


================================================
FILE: lib/kamal/commands/app/assets.rb
================================================
module Kamal::Commands::App::Assets
  def extract_assets
    asset_container = "#{role.container_prefix}-assets"

    combine \
      make_directory(role.asset_extracted_directory),
      [ *docker(:container, :rm, asset_container, "2> /dev/null"), "|| true" ],
      docker(:container, :create, "--name", asset_container, config.absolute_image),
      docker(:container, :cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
      docker(:container, :rm, asset_container),
      by: "&&"
  end

  def sync_asset_volumes(old_version: nil)
    new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path
    if old_version.present?
      old_extracted_path, old_volume_path = role.asset_extracted_directory(old_version), role.asset_volume(old_version).host_path
    end

    commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ]

    if old_version.present?
      commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
      commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
    end

    chain *commands
  end

  def clean_up_assets
    chain \
      find_and_remove_older_siblings(role.asset_extracted_directory),
      find_and_remove_older_siblings(role.asset_volume_directory)
  end

  private
    def find_and_remove_older_siblings(path)
      [
        :find,
        Pathname.new(path).dirname.to_s,
        "-maxdepth 1",
        "-name", "'#{role.name}-*'",
        "!", "-name", Pathname.new(path).basename.to_s,
        "-exec rm -rf \"{}\" +"
      ]
    end

    def copy_contents(source, destination, continue_on_error: false)
      [ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error) ]
    end
end


================================================
FILE: lib/kamal/commands/app/containers.rb
================================================
module Kamal::Commands::App::Containers
  DOCKER_HEALTH_LOG_FORMAT    = "'{{json .State.Health}}'"

  def list_containers
    docker :container, :ls, "--all", *container_filter_args
  end

  def list_container_names
    [ *list_containers, "--format", "'{{ .Names }}'" ]
  end

  def remove_container(version:)
    pipe \
      container_id_for(container_name: container_name(version)),
      xargs(docker(:container, :rm))
  end

  def rename_container(version:, new_version:)
    docker :rename, container_name(version), container_name(new_version)
  end

  def remove_containers
    docker :container, :prune, "--force", *container_filter_args
  end

  def container_health_log(version:)
    pipe \
      container_id_for(container_name: container_name(version)),
      xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
  end
end


================================================
FILE: lib/kamal/commands/app/error_pages.rb
================================================
module Kamal::Commands::App::ErrorPages
  def create_error_pages_directory
    make_directory(config.proxy_boot.error_pages_directory)
  end

  def clean_up_error_pages
    [ :find, config.proxy_boot.error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ]
  end
end


================================================
FILE: lib/kamal/commands/app/execution.rb
================================================
module Kamal::Commands::App::Execution
  def execute_in_existing_container(*command, interactive: false, env:)
    docker :exec,
      (docker_interactive_args if interactive),
      *argumentize("--env", env),
      container_name,
      *command
  end

  def execute_in_new_container(*command, interactive: false, detach: false, env:)
    docker :run,
      (docker_interactive_args if interactive),
      ("--detach" if detach),
      ("--rm" unless detach),
      "--name", container_name_for_exec,
      "--network", "kamal",
      *role&.env_args(host),
      *argumentize("--env", env),
      *role.logging_args,
      *config.volume_args,
      *role&.option_args,
      config.absolute_image,
      *command
  end

  def execute_in_existing_container_over_ssh(*command, env:)
    run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
  end

  def execute_in_new_container_over_ssh(*command, env:)
    run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
  end

  private
    def container_name_for_exec
      [ role.container_prefix, "exec", config.version, SecureRandom.hex(3) ].compact.join("-")
    end
end


================================================
FILE: lib/kamal/commands/app/images.rb
================================================
module Kamal::Commands::App::Images
  def list_images
    docker :image, :ls, config.repository
  end

  def remove_images
    docker :image, :prune, "--all", "--force", *image_filter_args
  end

  def tag_latest_image
    docker :tag, config.absolute_image, config.latest_image
  end
end


================================================
FILE: lib/kamal/commands/app/logging.rb
================================================
module Kamal::Commands::App::Logging
  def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
    pipe \
      container_id_command(container_id),
      "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
      ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
  end

  def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
    run_over_ssh \
      pipe(
        container_id_command(container_id),
        "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
        (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
      ),
      host: host
  end

  private

  def container_id_command(container_id)
    case container_id
    when Array then container_id
    when String, Symbol then "echo #{container_id}"
    else current_running_container_id
    end
  end
end


================================================
FILE: lib/kamal/commands/app/proxy.rb
================================================
module Kamal::Commands::App::Proxy
  delegate :container_name, to: :"config.proxy_boot", prefix: :proxy

  def deploy(target:)
    proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
  end

  def remove
    proxy_exec :remove, role.container_prefix
  end

  def live
    proxy_exec :resume, role.container_prefix
  end

  def maintenance(**options)
    proxy_exec :stop, role.container_prefix, *role.proxy.stop_command_args(**options)
  end

  def remove_proxy_app_directory
    remove_directory config.proxy_boot.app_directory
  end

  def create_ssl_directory
    make_directory(File.join(config.proxy_boot.tls_directory, role.name))
  end

  private
    def proxy_exec(*command)
      docker :exec, proxy_container_name, "kamal-proxy", *command
    end
end


================================================
FILE: lib/kamal/commands/app.rb
================================================
class Kamal::Commands::App < Kamal::Commands::Base
  include Assets, Containers, ErrorPages, Execution, Images, Logging, Proxy

  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]

  attr_reader :role, :host

  delegate :container_name, to: :role

  def initialize(config, role: nil, host: nil)
    super(config)
    @role = role
    @host = host
  end

  def run(hostname: nil)
    docker :run,
      "--detach",
      "--restart unless-stopped",
      "--name", container_name,
      "--network", "kamal",
      *([ "--hostname", hostname ] if hostname),
      "--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
      "--env", "KAMAL_VERSION=\"#{config.version}\"",
      "--env", "KAMAL_HOST=\"#{host}\"",
      *([ "--env", "KAMAL_DESTINATION=\"#{config.destination}\"" ] if config.destination),
      *role.env_args(host),
      *role.logging_args,
      *config.volume_args,
      *role.asset_volume_args,
      *role.label_args,
      *role.option_args,
      config.absolute_image,
      role.cmd
  end

  def start
    docker :start, container_name
  end

  def status(version:)
    pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
  end

  def stop(version: nil)
    pipe \
      version ? container_id_for_version(version) : current_running_container_id,
      xargs(docker(:stop, *role.stop_args))
  end

  def info
    docker :ps, *container_filter_args
  end


  def current_running_container_id
    current_running_container(format: "--quiet")
  end

  def container_id_for_version(version, only_running: false)
    container_id_for(container_name: container_name(version), only_running: only_running)
  end

  def current_running_version
    pipe \
      current_running_container(format: "--format '{{.Names}}'"),
      extract_version_from_name
  end

  def list_versions(*docker_args, statuses: nil)
    pipe \
      docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
      extract_version_from_name
  end

  def ensure_env_directory
    make_directory role.env_directory
  end

  private
    def latest_image_id
      docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
    end

    def current_running_container(format:)
      pipe \
        shell(chain(latest_image_container(format: format), latest_container(format: format))),
        [ :head, "-1" ]
    end

    def latest_image_container(format:)
      latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
    end

    def latest_container(format:, filters: nil)
      docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
    end

    def container_filter_args(statuses: nil)
      argumentize "--filter", container_filters(statuses: statuses)
    end

    def image_filter_args
      argumentize "--filter", image_filters
    end

    def extract_version_from_name
      # Extract SHA from "service-role-dest-SHA"
      %(while read line; do echo ${line##{role.container_prefix}-}; done)
    end

    def container_filters(statuses: nil)
      [ "label=service=#{config.service}" ].tap do |filters|
        filters << "label=destination=#{config.destination}"
        filters << "label=role=#{role}" if role
        statuses&.each do |status|
          filters << "status=#{status}"
        end
      end
    end

    def image_filters
      [ "label=service=#{config.service}" ]
    end
end


================================================
FILE: lib/kamal/commands/auditor.rb
================================================
class Kamal::Commands::Auditor < Kamal::Commands::Base
  attr_reader :details
  delegate :escape_shell_value, to: Kamal::Utils

  def initialize(config, **details)
    super(config)
    @details = details
  end

  # Runs remotely
  def record(line, **details)
    combine \
      make_run_directory,
      append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file)
  end

  def reveal
    [ :tail, "-n", 50, audit_log_file ]
  end

  private
    def audit_log_file
      file = [ config.service, config.destination, "audit.log" ].compact.join("-")

      File.join(config.run_directory, file)
    end

    def audit_tags(**details)
      tags(**self.details, **details)
    end

    def make_run_directory
      [ :mkdir, "-p", config.run_directory ]
    end

    def audit_line(line, **details)
      "#{audit_tags(**details).except(:version, :service_version, :service)} #{line}"
    end
end


================================================
FILE: lib/kamal/commands/base.rb
================================================
module Kamal::Commands
  class Base
    delegate :sensitive, :argumentize, to: Kamal::Utils

    DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"

    attr_accessor :config

    def initialize(config)
      @config = config
    end

    def run_over_ssh(*command, host:)
      "ssh#{ssh_config_args}#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
    end

    def container_id_for(container_name:, only_running: false)
      docker :container, :ls, *("--all" unless only_running), "--filter", "'name=^#{container_name}$'", "--quiet"
    end

    def make_directory_for(remote_file)
      make_directory Pathname.new(remote_file).dirname.to_s
    end

    def make_directory(path)
      [ :mkdir, "-p", path ]
    end

    def remove_directory(path)
      [ :rm, "-r", path ]
    end

    def remove_file(path)
      [ :rm, path ]
    end

    def ensure_docker_installed
      combine \
        ensure_local_docker_installed,
        ensure_local_buildx_installed
    end

    private
      def combine(*commands, by: "&&")
        commands
          .compact
          .collect { |command| Array(command) + [ by ] }.flatten # Join commands
          .tap     { |commands| commands.pop } # Remove trailing combiner
      end

      def chain(*commands)
        combine *commands, by: ";"
      end

      def pipe(*commands)
        combine *commands, by: "|"
      end

      def append(*commands)
        combine *commands, by: ">>"
      end

      def write(*commands)
        combine *commands, by: ">"
      end

      def any(*commands)
        combine *commands, by: "||"
      end

      def substitute(*commands)
        "\$\(#{commands.join(" ")}\)"
      end

      def xargs(command)
        [ :xargs, command ].flatten
      end

      def shell(command)
        [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
      end

      def docker(*args)
        args.compact.unshift :docker
      end

      def pack(*args)
        args.compact.unshift :pack
      end

      def git(*args, path: nil)
        [ :git, *([ "-C", path ] if path), *args.compact ]
      end

      def grep(*args)
        args.compact.unshift :grep
      end

      def tags(**details)
        Kamal::Tags.from_config(config, **details)
      end

      def ssh_config_args
        case config.ssh.config
        when Array
          config.ssh.config.map { |file| " -F #{file}" }.join
        when String
          " -F #{config.ssh.config}"
        when true
          "" # Use default SSH config
        when false
          " -F /dev/null" # Ignore SSH config
        end
      end

      def ssh_proxy_args
        case config.ssh.proxy
        when Net::SSH::Proxy::Jump
          " -J #{config.ssh.proxy.jump_proxies}"
        when Net::SSH::Proxy::Command
          " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
        end
      end

      def ssh_keys_args
        "#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
      end

      def ssh_keys
        config.ssh.keys&.map do |key|
          " -i #{key}"
        end
      end

      def ensure_local_docker_installed
        docker "--version"
      end

      def ensure_local_buildx_installed
        docker :buildx, "version"
      end

      def docker_interactive_args
        STDIN.isatty ? "-it" : "-i"
      end
  end
end


================================================
FILE: lib/kamal/commands/builder/base.rb
================================================
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
  class BuilderError < StandardError; end

  ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"

  delegate :argumentize, to: Kamal::Utils
  delegate \
    :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
    :pack?, :pack_builder, :pack_buildpacks,
    :cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
    to: :builder_config

  def clean
    docker :image, :rm, "--force", config.absolute_image
  end

  def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
    docker :buildx, :build,
      "--output=type=#{export_action}",
      *platform_options(arches),
      *([ "--builder", builder_name ] unless docker_driver?),
      *build_tag_options(tag_as_dirty: tag_as_dirty),
      *build_options,
      *([ "--no-cache" ] if no_cache),
      build_context,
      "2>&1"
  end

  def pull
    docker :pull, config.absolute_image
  end

  def info
    combine \
      docker(:context, :ls),
      docker(:buildx, :ls)
  end

  def inspect_builder
    docker :buildx, :inspect, builder_name unless docker_driver?
  end

  def build_options
    [ *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
  end

  def build_context
    config.builder.context
  end

  def validate_image
    pipe \
      docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
      any(
        [ :grep, "-x", config.service ],
        "(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
      )
  end

  def first_mirror
    docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
  end

  def login_to_registry_locally?
    true
  end

  def push_env
    {}
  end

  private
    def build_tag_names(tag_as_dirty: false)
      tag_names = [ config.absolute_image, config.latest_image ]
      tag_names.map! { |t| "#{t}-dirty" } if tag_as_dirty
      tag_names
    end

    def build_tag_options(tag_as_dirty: false)
      build_tag_names(tag_as_dirty: tag_as_dirty).flat_map { |name| [ "-t", name ] }
    end

    def build_cache
      if cache_to && cache_from
        [ "--cache-to", cache_to,
          "--cache-from", cache_from ]
      end
    end

    def build_labels
      argumentize "--label", { service: config.service }
    end

    def build_args
      argumentize "--build-arg", args, sensitive: true
    end

    def build_secrets
      argumentize "--secret", secrets.keys.collect { |secret| [ "id", secret ] }
    end

    def build_dockerfile
      if Pathname.new(File.expand_path(dockerfile)).exist?
        argumentize "--file", dockerfile
      else
        raise BuilderError, "Missing #{dockerfile}"
      end
    end

    def build_target
      argumentize "--target", target if target.present?
    end

    def build_ssh
      argumentize "--ssh", ssh if ssh.present?
    end

    def builder_provenance
      argumentize "--provenance", provenance unless provenance.nil?
    end

    def builder_sbom
      argumentize "--sbom", sbom unless sbom.nil?
    end

    def builder_config
      config.builder
    end

    def registry_config
      config.registry
    end

    def driver_options
      if registry_config.local?
        [ "--driver-opt", "network=host" ]
      end
    end

    def platform_options(arches)
      argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
    end
end


================================================
FILE: lib/kamal/commands/builder/clone.rb
================================================
module Kamal::Commands::Builder::Clone
  def clone
    git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
  end

  def clone_reset_steps
    [
      git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
      git(:fetch, :origin, path: escaped_build_directory),
      git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
      git(:clean, "-fdx", path: escaped_build_directory),
      git(:submodule, :update, "--init", path: escaped_build_directory)
    ]
  end

  def clone_status
    git :status, "--porcelain", path: escaped_build_directory
  end

  def clone_revision
    git :"rev-parse", :HEAD, path: escaped_build_directory
  end

  def escaped_root
    Kamal::Git.root.shellescape
  end

  def escaped_build_directory
    config.builder.build_directory.shellescape
  end
end


================================================
FILE: lib/kamal/commands/builder/cloud.rb
================================================
class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base
  # Expects `driver` to be of format "cloud docker-org-name/builder-name"

  def create
    docker :buildx, :create, "--driver", driver
  end

  def remove
    docker :buildx, :rm, builder_name
  end

  private
    def builder_name
      driver.gsub(/[ \/]/, "-")
    end

    def inspect_buildx
      pipe \
        docker(:buildx, :inspect, builder_name),
        grep("-q", "Endpoint:.*cloud://.*")
    end
end


================================================
FILE: lib/kamal/commands/builder/hybrid.rb
================================================
class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
  def create
    combine \
      create_local_buildx,
      create_remote_context,
      append_remote_buildx
  end

  private
    def builder_name
      "kamal-hybrid-#{driver}-#{remote_builder_name_suffix}"
    end

    def create_local_buildx
      docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}", *driver_options
    end

    def append_remote_buildx
      docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, *driver_options, remote_context_name
    end
end


================================================
FILE: lib/kamal/commands/builder/local.rb
================================================
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
  def create
    return if docker_driver?

    docker :buildx, :create, "--name", builder_name, "--driver=#{driver}", *driver_options
  end

  def remove
    docker :buildx, :rm, builder_name unless docker_driver?
  end

  private
    def builder_name
      if registry_config.local?
        "kamal-local-registry-#{driver}"
      else
        "kamal-local-#{driver}"
      end
    end
end


================================================
FILE: lib/kamal/commands/builder/pack.rb
================================================
class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
  def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
    combine \
      build(tag_as_dirty: tag_as_dirty, no_cache: no_cache),
      export(export_action)
  end

  def remove;end

  def info
    pack :builder, :inspect, pack_builder
  end
  alias_method :inspect_builder, :info

  private
    def build(tag_as_dirty: false, no_cache: false)
      pack(:build,
        config.repository,
        "--platform", platform,
        "--creation-time", "now",
        "--builder", pack_builder,
        buildpacks,
        *build_tag_options(tag_as_dirty: tag_as_dirty),
        *([ "--clear-cache" ] if no_cache),
        "--env", "BP_IMAGE_LABELS=service=#{config.service}",
        *argumentize("--env", args),
        *argumentize("--env", secrets, sensitive: true),
        "--path", build_context)
    end

    def export(export_action)
      return unless export_action == "registry"

      combine \
        docker(:push, config.absolute_image),
        docker(:push, config.latest_image)
    end

    def platform
      "linux/#{local_arches.first}"
    end

    def buildpacks
      (pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| [ "--buildpack", buildpack ] }
    end
end


================================================
FILE: lib/kamal/commands/builder/remote.rb
================================================
class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
  def create
    chain \
      create_remote_context,
      create_buildx
  end

  def remove
    chain \
      remove_remote_context,
      remove_buildx
  end

  def info
    chain \
      docker(:context, :ls),
      docker(:buildx, :ls)
  end

  def inspect_builder
    combine \
      combine(inspect_buildx, inspect_remote_context),
      [ "(echo no compatible builder && exit 1)" ],
      by: "||"
  end

  def login_to_registry_locally?
    false
  end

  def push_env
    { "BUILDKIT_NO_CLIENT_TOKEN" => "1" }
  end

  private
    def builder_name
      "kamal-remote-#{remote_builder_name_suffix}"
    end

    def remote_context_name
      "#{builder_name}-context"
    end

    def remote_builder_name_suffix
      "#{remote.gsub(/[^a-z0-9_-]/, "-")}#{registry_config.local? ? "-local-registry" : "" }"
    end

    def inspect_buildx
      pipe \
        docker(:buildx, :inspect, builder_name),
        grep("-q", "Endpoint:.*#{remote_context_name}")
    end

    def inspect_remote_context
      pipe \
        docker(:context, :inspect, remote_context_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT),
        grep("-xq", remote)
    end

    def create_remote_context
      docker :context, :create, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
    end

    def remove_remote_context
      docker :context, :rm, remote_context_name
    end

    def create_buildx
      docker :buildx, :create, "--name", builder_name, *driver_options, remote_context_name
    end

    def remove_buildx
      docker :buildx, :rm, builder_name
    end
end


================================================
FILE: lib/kamal/commands/builder.rb
================================================
require "active_support/core_ext/string/filters"

class Kamal::Commands::Builder < Kamal::Commands::Base
  delegate \
    :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder,
    :validate_image, :first_mirror, :login_to_registry_locally?, :push_env,
    to: :target

  delegate \
    :local?, :remote?, :pack?, :cloud?,
    to: "config.builder"

  include Clone

  def name
    target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
  end

  def target
    if remote?
      if local?
        hybrid
      else
        remote
      end
    elsif pack?
      pack
    elsif cloud?
      cloud
    else
      local
    end
  end

  def remote
    @remote ||= Kamal::Commands::Builder::Remote.new(config)
  end

  def local
    @local ||= Kamal::Commands::Builder::Local.new(config)
  end

  def hybrid
    @hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
  end

  def pack
    @pack ||= Kamal::Commands::Builder::Pack.new(config)
  end

  def cloud
    @cloud ||= Kamal::Commands::Builder::Cloud.new(config)
  end
end


================================================
FILE: lib/kamal/commands/docker.rb
================================================
class Kamal::Commands::Docker < Kamal::Commands::Base
  # Install Docker using the https://github.com/docker/docker-install convenience script.
  def install
    pipe get_docker, :sh
  end

  # Checks the Docker client version. Fails if Docker is not installed.
  def installed?
    docker "-v"
  end

  # Checks the Docker server version. Fails if Docker is not running.
  def running?
    docker :version
  end

  # Do we have superuser access to install Docker and start system services?
  def superuser?
    [ '[ "${EUID:-$(id -u)}" -eq 0 ] || sudo -nl usermod >/dev/null' ]
  end

  def root?
    [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
  end

  def in_docker_group?
    [ 'id -nG "${USER:-$(id -un)}" | grep -qw docker' ]
  end

  def add_to_docker_group
    [ 'sudo -n usermod -aG docker "${USER:-$(id -un)}"' ]
  end

  def refresh_session
    [ "kill -HUP $PPID" ]
  end

  def create_network
    docker :network, :create, :kamal
  end

  private
    def get_docker
      shell \
        any \
          [ :curl, "-fsSL", "https://get.docker.com" ],
          [ :wget, "-O -", "https://get.docker.com" ],
          [ :echo, "\"exit 1\"" ]
    end
end


================================================
FILE: lib/kamal/commands/hook.rb
================================================
class Kamal::Commands::Hook < Kamal::Commands::Base
  def run(hook)
    [ hook_file(hook) ]
  end

  def env(secrets: false, **details)
    tags(**details).env.tap do |env|
      env.merge!(config.secrets.to_h) if secrets
    end
  end

  def hook_exists?(hook)
    Pathname.new(hook_file(hook)).exist?
  end

  private
    def hook_file(hook)
      File.join(config.hooks_path, hook)
    end
end


================================================
FILE: lib/kamal/commands/lock.rb
================================================
require "active_support/duration"
require "time"
require "base64"

class Kamal::Commands::Lock < Kamal::Commands::Base
  def acquire(message, version)
    combine \
      [ :mkdir, lock_dir ],
      write_lock_details(message, version)
  end

  def release
    combine \
      [ :rm, lock_details_file ],
      [ :rm, "-r", lock_dir ]
  end

  def status
    combine \
      stat_lock_dir,
      read_lock_details
  end

  def ensure_locks_directory
    [ :mkdir, "-p", locks_dir ]
  end

  private
    def write_lock_details(message, version)
      write \
        [ :echo, "\"#{Base64.encode64(lock_details(message, version))}\"" ],
        lock_details_file
    end

    def read_lock_details
      pipe \
        [ :cat, lock_details_file ],
        [ :base64, "-d" ]
    end

    def stat_lock_dir
      write \
        [ :stat, lock_dir ],
        "/dev/null"
    end

    def lock_dir
      dir_name = [ "lock", config.service, config.destination ].compact.join("-")

      File.join(config.run_directory, dir_name)
    end

    def lock_details_file
      File.join(lock_dir, "details")
    end

    def lock_details(message, version)
      <<~DETAILS.strip
        Locked by: #{locked_by} at #{Time.now.utc.iso8601}
        Version: #{version}
        Message: #{message}
      DETAILS
    end

    def locked_by
      Kamal::Git.user_name
    rescue Errno::ENOENT
      "Unknown"
    end
end


================================================
FILE: lib/kamal/commands/proxy.rb
================================================
class Kamal::Commands::Proxy < Kamal::Commands::Base
  delegate :argumentize, :optionize, to: Kamal::Utils
  attr_reader :proxy_run_config

  def initialize(config, host:)
    super(config)
    @proxy_run_config = config.proxy_run(host)
  end

  def run
    if proxy_run_config
      docker \
        :run,
        "--name", container_name,
        "--network", "kamal",
        "--detach",
        "--restart", "unless-stopped",
        "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
        *proxy_run_config.docker_options_args,
        *proxy_run_config.image,
        *proxy_run_config.run_command
    else
      pipe boot_config, xargs(docker_run)
    end
  end

  def start
    docker :container, :start, container_name
  end

  def stop(name: container_name)
    docker :container, :stop, name
  end

  def start_or_run
    combine start, run, by: "||"
  end

  def info
    docker :ps, "--filter", "'name=^#{container_name}$'"
  end

  def version
    pipe \
      docker(:inspect, container_name, "--format '{{.Config.Image}}'"),
      [ :awk, "-F:", "'{print \$NF}'" ]
  end

  def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
    pipe \
      docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
      ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
  end

  def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
    run_over_ssh pipe(
      docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
      (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
    ).join(" "), host: host
  end

  def remove_container
    docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
  end

  def remove_image
    docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
  end

  def cleanup_traefik
    chain \
      docker(:container, :stop, "traefik"),
      combine(
        docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"),
        docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik")
      )
  end

  def ensure_proxy_directory
    make_directory config.proxy_boot.host_directory
  end

  def remove_proxy_directory
    remove_directory config.proxy_boot.host_directory
  end

  def ensure_apps_config_directory
    make_directory config.proxy_boot.apps_directory
  end

  def boot_config
    [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
  end

  def read_boot_options
    read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" "))
  end

  def read_image
    read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default)
  end

  def read_image_version
    read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
  end

  def read_run_command
    read_file(config.proxy_boot.run_command_file)
  end

  def reset_boot_options
    remove_file config.proxy_boot.options_file
  end

  def reset_image
    remove_file config.proxy_boot.image_file
  end

  def reset_image_version
    remove_file config.proxy_boot.image_version_file
  end

  def reset_run_command
    remove_file config.proxy_boot.run_command_file
  end

  private
    def container_name
      config.proxy_boot.container_name
    end

    def read_file(file, default: nil)
      combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
    end

    def docker_run
      docker \
        :run,
        "--name", container_name,
        "--network", "kamal",
        "--detach",
        "--restart", "unless-stopped",
        "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
        *config.proxy_boot.apps_volume.docker_args
    end
end


================================================
FILE: lib/kamal/commands/prune.rb
================================================
require "active_support/duration"
require "active_support/core_ext/numeric/time"

class Kamal::Commands::Prune < Kamal::Commands::Base
  def dangling_images
    docker :image, :prune, "--force", "--filter", "label=service=#{config.service}"
  end

  def tagged_images
    pipe \
      docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
      grep("-v -w \"#{active_image_list}\""),
      "while read image tag; do docker rmi $tag; done"
  end

  def app_containers(retain:)
    pipe \
      docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
      "tail -n +#{retain + 1}",
      "while read container_id; do docker rm $container_id; done"
  end

  private
    def stopped_containers_filters
      [ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] }
    end

    def active_image_list
      # Pull the images that are used by any containers
      # Append repo:latest - to avoid deleting the latest tag
      # Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
      "$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
    end

    def service_filter
      [ "--filter", "label=service=#{config.service}" ]
    end
end


================================================
FILE: lib/kamal/commands/registry.rb
================================================
class Kamal::Commands::Registry < Kamal::Commands::Base
  def login(registry_config: nil)
    registry_config ||= config.registry

    return if registry_config.local?

    docker :login,
      registry_config.server,
      "-u", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
      "-p", sensitive(Kamal::Utils.escape_shell_value(registry_config.password))
  end

  def logout(registry_config: nil)
    registry_config ||= config.registry

    docker :logout, registry_config.server
  end

  def setup(registry_config: nil)
    registry_config ||= config.registry

    combine \
      docker(:start, "kamal-docker-registry"),
      docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", "kamal-docker-registry", "registry:3"),
      by: "||"
  end

  def remove
    combine \
      docker(:stop, "kamal-docker-registry"),
      docker(:rm, "kamal-docker-registry"),
      by: "&&"
  end

  def local?
    config.registry.local?
  end
end


================================================
FILE: lib/kamal/commands/server.rb
================================================
class Kamal::Commands::Server < Kamal::Commands::Base
  def ensure_run_directory
    make_directory config.run_directory
  end

  def remove_app_directory
    remove_directory config.app_directory
  end

  def app_directory_count
    pipe \
      [ :ls, config.apps_directory ],
      [ :wc, "-l" ]
  end
end


================================================
FILE: lib/kamal/commands.rb
================================================
module Kamal::Commands
end


================================================
FILE: lib/kamal/configuration/accessory.rb
================================================
class Kamal::Configuration::Accessory
  include Kamal::Configuration::Validation

  DEFAULT_NETWORK = "kamal"

  delegate :argumentize, :optionize, to: Kamal::Utils

  attr_reader :name, :env, :proxy, :registry

  def initialize(name, config:)
    @name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]

    validate! \
      accessory_config,
      example: validation_yml["accessories"]["mysql"],
      context: "accessories/#{name}",
      with: Kamal::Configuration::Validator::Accessory

    ensure_valid_roles

    @env = initialize_env
    @proxy = initialize_proxy if running_proxy?
    @registry = initialize_registry if accessory_config["registry"].present?
  end

  def service_name
    accessory_config["service"] || "#{config.service}-#{name}"
  end

  def image
    [ registry&.server, accessory_config["image"] ].compact.join("/")
  end

  def hosts
    hosts_from_host || hosts_from_hosts || hosts_from_roles || hosts_from_tags
  end

  def port
    if port = accessory_config["port"]&.to_s
      port.include?(":") ? port : "#{port}:#{port}"
    end
  end

  def network_args
    argumentize "--network", network
  end

  def publish_args
    argumentize "--publish", port if port
  end

  def labels
    default_labels.merge(accessory_config["labels"] || {})
  end

  def label_args
    argumentize "--label", labels
  end

  def env_args
    [ *env.clear_args, *argumentize("--env-file", secrets_path) ]
  end

  def env_directory
    File.join(config.env_directory, "accessories")
  end

  def secrets_io
    env.secrets_io
  end

  def secrets_path
    File.join(config.env_directory, "accessories", "#{name}.env")
  end

  def files
    accessory_config["files"]&.to_h do |config|
      parse_path_config(config, default_mode: "755") do |local, remote|
        {
          key: expand_local_file(local),
          host_path: expand_remote_file(remote),
          container_path: remote
        }
      end
    end || {}
  end

  def directories
    accessory_config["directories"]&.to_h do |config|
      parse_path_config(config, default_mode: nil) do |local, remote|
        {
          key: expand_host_path(local),
          host_path: expand_host_path_for_volume(local),
          container_path: remote
        }
      end
    end || {}
  end

  def volume_args
    argumentize("--volume", specific_volumes) + (path_volumes(files) + path_volumes(directories)).flat_map(&:docker_args)
  end

  def option_args
    if args = accessory_config["options"]
      optionize args
    else
      []
    end
  end

  def cmd
    accessory_config["cmd"]
  end

  def running_proxy?
    accessory_config["proxy"].present?
  end

  private
    attr_reader :config, :accessory_config

    def initialize_env
      Kamal::Configuration::Env.new \
        config: accessory_config.fetch("env", {}),
        secrets: config.secrets,
        context: "accessories/#{name}/env"
    end

    def initialize_proxy
      Kamal::Configuration::Proxy.new \
        config: config,
        proxy_config: accessory_config["proxy"],
        context: "accessories/#{name}/proxy",
        secrets: config.secrets
    end

    def initialize_registry
      Kamal::Configuration::Registry.new \
        config: accessory_config,
        secrets: config.secrets,
        context: "accessories/#{name}/registry"
    end

    def default_labels
      { "service" => service_name }
    end

    def expand_local_file(local_file)
      if local_file.end_with?("erb")
        with_env_loaded { read_dynamic_file(local_file) }
      else
        Pathname.new(File.expand_path(local_file)).to_s
      end
    end

    def with_env_loaded
      env.to_h.each { |k, v| ENV[k] = v }
      yield
    ensure
      env.to_h.each { |k, v| ENV.delete(k) }
    end

    def read_dynamic_file(local_file)
      StringIO.new(ERB.new(File.read(local_file)).result)
    end

    def expand_remote_file(remote_file)
      service_name + remote_file
    end

    def specific_volumes
      accessory_config["volumes"] || []
    end

    def path_volumes(paths)
      paths.map do |local, config|
        Kamal::Configuration::Volume.new \
          host_path: config[:host_path],
          container_path: config[:container_path],
          options: config[:options]
      end
    end

    def parse_path_config(config, default_mode:)
      if config.is_a?(Hash)
        local, remote = config["local"], config["remote"]
        expanded = yield(local, remote)
        [
          expanded[:key],
          expanded.except(:key).merge(
            options: config["options"],
            mode: config["mode"] || default_mode,
            owner: config["owner"]
          )
        ]
      else
        local, remote, options = config.split(":", 3)
        expanded = yield(local, remote)
        [
          expanded[:key],
          expanded.except(:key).merge(
            options: options,
            mode: default_mode,
            owner: nil
          )
        ]
      end
    end

    def expand_host_path(host_path)
      absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
    end

    def expand_host_path_for_volume(host_path)
      absolute_path?(host_path) ? host_path : File.join(service_name, host_path)
    end

    def absolute_path?(path)
      Pathname.new(path).absolute?
    end

    def service_data_directory
      "$PWD/#{service_name}"
    end

    def hosts_from_host
      [ accessory_config["host"] ] if accessory_config.key?("host")
    end

    def hosts_from_hosts
      accessory_config["hosts"] if accessory_config.key?("hosts")
    end

    def hosts_from_roles
      if accessory_config.key?("role")
       config.role(accessory_config["role"])&.hosts
      elsif accessory_config.key?("roles")
        accessory_config["roles"].flat_map { |role| config.role(role)&.hosts }
      end
    end

    def hosts_from_tags
      if accessory_config.key?("tag")
        extract_hosts_from_config_with_tag(accessory_config["tag"])
      elsif accessory_config.key?("tags")
        accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) }
      end
    end

    def extract_hosts_from_config_with_tag(tag)
      if (servers_with_roles = config.raw_config.servers).is_a?(Hash)
        servers_with_roles.flat_map do |role, servers_in_role|
          servers_in_role.filter_map do |host|
            host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag)
          end
        end
      end
    end

    def network
      accessory_config["network"] || DEFAULT_NETWORK
    end

    def ensure_valid_roles
      if accessory_config["roles"] && (missing_roles = accessory_config["roles"] - config.roles.map(&:name)).any?
        raise Kamal::ConfigurationError, "accessories/#{name}: unknown roles #{missing_roles.join(", ")}"
      elsif accessory_config["role"] && !config.role(accessory_config["role"])
        raise Kamal::ConfigurationError, "accessories/#{name}: unknown role #{accessory_config["role"]}"
      end
    end
end


================================================
FILE: lib/kamal/configuration/alias.rb
================================================
class Kamal::Configuration::Alias
  include Kamal::Configuration::Validation

  attr_reader :name, :command

  def initialize(name, config:)
    @name, @command = name.inquiry, config.raw_config["aliases"][name]

    validate! \
      command,
      example: validation_yml["aliases"]["uname"],
      context: "aliases/#{name}",
      with: Kamal::Configuration::Validator::Alias
  end
end


================================================
FILE: lib/kamal/configuration/boot.rb
================================================
class Kamal::Configuration::Boot
  include Kamal::Configuration::Validation

  attr_reader :boot_config, :host_count

  def initialize(config:)
    @boot_config = config.raw_config.boot || {}
    @host_count = config.all_hosts.count
    validate! boot_config
  end

  def limit
    limit = boot_config["limit"]

    if limit.to_s.end_with?("%")
      [ host_count * limit.to_i / 100, 1 ].max
    else
      limit
    end
  end

  def wait
    boot_config["wait"]
  end

  def parallel_roles
    boot_config["parallel_roles"]
  end
end


================================================
FILE: lib/kamal/configuration/builder.rb
================================================
class Kamal::Configuration::Builder
  include Kamal::Configuration::Validation

  attr_reader :config, :builder_config
  delegate :image, :service, to: :config
  delegate :server, to: :"config.registry"

  def initialize(config:)
    @config = config
    @builder_config = config.raw_config.builder || {}
    @image = config.image
    @server = config.registry.server
    @service = config.service

    validate! builder_config, with: Kamal::Configuration::Validator::Builder
  end

  def to_h
    builder_config
  end

  def remote
    builder_config["remote"]
  end

  def arches
    Array(builder_config.fetch("arch", default_arch))
  end

  def local_arches
    @local_arches ||= if local_disabled?
      []
    elsif remote
      arches & [ Kamal::Utils.docker_arch ]
    else
      arches
    end
  end

  def remote_arches
    @remote_arches ||= if remote
      arches - local_arches
    else
      []
    end
  end

  def remote?
    remote_arches.any?
  end

  def local?
    !local_disabled? && (arches.empty? || local_arches.any?)
  end

  def cloud?
    driver.start_with? "cloud"
  end

  def cached?
    !!builder_config["cache"]
  end

  def pack?
    !!builder_config["pack"]
  end

  def args
    builder_config["args"] || {}
  end

  def secrets
    (builder_config["secrets"] || []).to_h { |key| [ key, config.secrets[key] ] }
  end

  def dockerfile
    builder_config["dockerfile"] || "Dockerfile"
  end

  def target
    builder_config["target"]
  end

  def context
    builder_config["context"] || "."
  end

  def driver
    builder_config.fetch("driver", "docker-container")
  end

  def pack_builder
    builder_config["pack"]["builder"] if pack?
  end

  def pack_buildpacks
    builder_config["pack"]["buildpacks"] if pack?
  end

  def local_disabled?
    builder_config["local"] == false
  end

  def cache_from
    if cached?
      case builder_config["cache"]["type"]
      when "gha"
        cache_from_config_for_gha
      when "registry"
        cache_from_config_for_registry
      end
    end
  end

  def cache_to
    if cached?
      case builder_config["cache"]["type"]
      when "gha"
        cache_to_config_for_gha
      when "registry"
        cache_to_config_for_registry
      end
    end
  end

  def ssh
    builder_config["ssh"]
  end

  def provenance
    builder_config["provenance"]
  end

  def sbom
    builder_config["sbom"]
  end

  def git_clone?
    Kamal::Git.used? && builder_config["context"].nil?
  end

  def clone_directory
    @clone_directory ||= File.join Dir.tmpdir, "kamal-clones", [ service, pwd_sha ].compact.join("-")
  end

  def build_directory
    @build_directory ||=
      if git_clone?
        File.join clone_directory, repo_basename, repo_relative_pwd
      else
        "."
      end
  end

  def docker_driver?
    driver == "docker"
  end

  private
    def valid?
      if docker_driver?
        raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote
        raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached?
        raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many?
      end

      if @options["cache"] && @options["cache"]["type"]
        raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
      end
    end

    def cache_image
      builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
    end

    def cache_image_ref
      [ server, cache_image ].compact.join("/")
    end

    def cache_options
      builder_config["cache"]&.fetch("options", nil)
    end

    def cache_from_config_for_gha
      individual_options = cache_options&.split(",") || []
      allowed_options = individual_options.select { |option| option =~ /^(url|url_v2|token|scope|timeout)=/ }

      [ "type=gha", *allowed_options ].compact.join(",")
    end

    def cache_from_config_for_registry
      [ "type=registry", "ref=#{cache_image_ref}" ].compact.join(",")
    end

    def cache_to_config_for_gha
      [ "type=gha", cache_options ].compact.join(",")
    end

    def cache_to_config_for_registry
      [ "type=registry", "ref=#{cache_image_ref}", cache_options ].compact.join(",")
    end

    def repo_basename
      File.basename(Kamal::Git.root)
    end

    def repo_relative_pwd
      Dir.pwd.delete_prefix(Kamal::Git.root)
    end

    def pwd_sha
      Digest::SHA256.hexdigest(Dir.pwd)[0..12]
    end

    def default_arch
      docker_driver? ? [] : [ "amd64", "arm64" ]
    end
end


================================================
FILE: lib/kamal/configuration/docs/accessory.yml
================================================
# Accessories
#
# Accessories can be booted on a single host, a list of hosts, or on specific roles.
# The hosts do not need to be defined in the Kamal servers configuration.
#
# Accessories are managed separately from the main service — they are not updated
# when you deploy, and they do not have zero-downtime deployments.
#
# Run `kamal accessory boot <accessory>` to boot an accessory.
# See `kamal accessory --help` for more information.

# Configuring accessories
#
# First, define the accessory in the `accessories`:
accessories:
  mysql:

    # Service name
    #
    # This is used in the service label and defaults to `<service>-<accessory>`,
    # where `<service>` is the main service name from the root configuration:
    service: mysql

    # Image
    #
    # The Docker image to use.
    # Prefix it with its server when using root level registry different from Docker Hub.
    # Define registry directly or via anchors when it differs from root level registry.
    image: mysql:8.0

    # Registry
    #
    # By default accessories use Docker Hub registry.
    # You can specify different registry per accessory with this option.
    # Don't prefix image with this registry server.
    # Use anchors if you need to set the same specific registry for several accessories.
    #
    # ```yml
    # registry:
    #   <<: *specific-registry
    # ```
    #
    # See kamal docs registry for more information:
    registry:
      ...

    # Accessory hosts
    #
    # Specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`:
    host: mysql-db1
    hosts:
      - mysql-db1
      - mysql-db2
    role: mysql
    roles:
      - mysql
    tag: writer
    tags:
      - writer
      - reader

    # Custom command
    #
    # You can set a custom command to run in the container if you do not want to use the default:
    cmd: "bin/mysqld"

    # Port mappings
    #
    # See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
    # especially note the warning about the security implications of exposing ports publicly.
    port: "127.0.0.1:3306:3306"

    # Labels
    labels:
      app: myapp

    # Options
    #
    # These are passed to the Docker run command in the form `--<name> <value>`:
    options:
      restart: always
      cpus: 2

    # Environment variables
    #
    # See kamal docs env for more information:
    env:
      ...

    # Copying files
    #
    # You can specify files to mount into the container.
    #
    # They will be uploaded from the local repo to the host and then mounted.
    # ERB files will be evaluated before being copied.
    #
    # You can use the string format: `local:remote` or `local:remote:options`
    # where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
    files:
      - config/my.cnf.erb:/etc/mysql/my.cnf
      - config/myoptions.cnf:/etc/mysql/myoptions.cnf:ro
      - config/certs:/etc/mysql/certs:ro,Z
    #
    # Or you can use the hash format for custom mode and ownership.
    #
    # Note: Setting `owner` requires root access:
    files:
      - local: config/secret.key
        remote: /etc/mysql/secret.key
        mode: "0600"
        owner: "mysql:mysql"
      - local: config/ca-cert.pem
        remote: /etc/mysql/certs/ca-cert.pem
        mode: "0644"
        owner: "1000:1000"
        options: "Z"

    # Directories
    #
    # You can specify directories to mount into the container. They will be created on the host
    # before being mounted.
    #
    # You can use the string format: `local:remote` or `local:remote:options`
    # where the options can be `ro` for read-only or `z`/`Z` for SELinux labels
    directories:
      - mysql-logs:/var/log/mysql
      - mysql-data:/var/lib/mysql:z
    #
    # Or you can use the hash format for custom mode and ownership.
    #
    # Note: Setting `owner` requires root access:
    directories:
      - local: mysql-data
        remote: /var/lib/mysql
        mode: "0750"
        owner: "mysql:mysql"
      - local: mysql-logs
        remote: /var/log/mysql
        mode: "0755"
        options: "z"

    # Volumes
    #
    # Any other volumes to mount, in addition to the files and directories.
    # They are not created or copied before mounting:
    volumes:
      - /path/to/mysql-logs:/var/log/mysql

    # Network
    #
    # The network the accessory will be attached to.
    #
    # Defaults to kamal:
    network: custom

    # Proxy
    #
    # You can run your accessory behind the Kamal proxy. See kamal docs proxy for more information
    proxy:
      ...


================================================
FILE: lib/kamal/configuration/docs/alias.yml
================================================
# Aliases
#
# Aliases are shortcuts for Kamal commands.
#
# For example, for a Rails app, you might open a console with:
#
# ```shell
# kamal app exec -i --reuse "bin/rails console"
# ```
#
# By defining an alias, like this:
aliases:
  console: app exec -i --reuse "bin/rails console"
# You can now open the console with:
#
# ```shell
# kamal console
# ```

# Configuring aliases
#
# Aliases are defined in the root config under the alias key.
#
# Each alias is named and can only contain lowercase letters, numbers, dashes, and underscores:
aliases:
  uname: app exec -p -q -r web "uname -a"
#
# Aliases can include a destination with the `-d` flag:
  staging_deploy: deploy -d staging


================================================
FILE: lib/kamal/configuration/docs/boot.yml
================================================
# Booting
#
# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
#
# Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.

boot:

  # The number or percentage of hosts to boot at a time.
  # This can be an integer (e.g., 3) or a percentage string (e.g., 25%).
  limit: 25%

  # The number of seconds to wait between booting each group of hosts.
  wait: 10

  # Whether to boot roles in parallel on a host.
  #
  # If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
  #
  # Defaults to false.
  parallel_roles: true


================================================
FILE: lib/kamal/configuration/docs/builder.yml
================================================
# Builder
#
# The builder configuration controls how the application is built with `docker build`.
#
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information.

# Builder options
#
# Options go under the builder key in the root configuration.
builder:

  # Arch
  #
  # The architectures to build for — you can set an array or just a single value.
  #
  # Allowed values are `amd64` and `arm64`:
  arch:
    - amd64

  # Remote
  #
  # The connection string for a remote builder. If supplied, Kamal will use this
  # for builds that do not match the local architecture of the deployment host.
  remote: ssh://docker@docker-builder

  # Local
  #
  # If set to false, Kamal will always use the remote builder even when building
  # the local architecture.
  #
  # Defaults to true:
  local: true

  # Buildpack configuration
  #
  # The build configuration for using pack to build a Cloud Native Buildpack image.
  #
  # For additional buildpack customization options you can create a project descriptor
  # file(project.toml) that the Pack CLI will automatically use.
  # See https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/ for more information.
  pack:
    builder: heroku/builder:24
    buildpacks:
      - heroku/ruby
      - heroku/procfile

  # Builder cache
  #
  # The type must be either 'gha' or 'registry'.
  #
  # The image is only used for registry cache and is not compatible with the Docker driver:
  cache:
    type: registry
    options: mode=max
    image: kamal-app-build-cache

  # Build context
  #
  # If this is not set, then a local Git clone of the repo is used.
  # This ensures a clean build with no uncommitted changes.
  #
  # To use the local checkout instead, you can set the context to `.`, or a path to another directory.
  context: .

  # Dockerfile
  #
  # The Dockerfile to use for building, defaults to `Dockerfile`:
  dockerfile: Dockerfile.production

  # Build target
  #
  # If not set, then the default target is used:
  target: production

  # Build arguments
  #
  # Any additional build arguments, passed to `docker build` with `--build-arg <key>=<value>`:
  args:
    ENVIRONMENT: production

  # Referencing build arguments
  #
  # ```shell
  # ARG RUBY_VERSION
  # FROM ruby:$RUBY_VERSION-slim as base
  # ```

  # Build secrets
  #
  # Values are read from `.kamal/secrets`:
  secrets:
    - SECRET1
    - SECRET2

  # Referencing build secrets
  #
  # ```shell
  # # Copy Gemfiles
  # COPY Gemfile Gemfile.lock ./
  #
  # # Install dependencies, including private repositories via access token
  # # Then remove bundle cache with exposed GITHUB_TOKEN
  # RUN --mount=type=secret,id=GITHUB_TOKEN \
  #   BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
  #   bundle install && \
  #   rm -rf /usr/local/bundle/cache
  # ```

  # SSH
  #
  # SSH agent socket or keys to expose to the build:
  ssh: default=$SSH_AUTH_SOCK

  # Driver
  #
  # The build driver to use, defaults to `docker-container`:
  driver: docker
  #
  # If you want to use Docker Build Cloud (https://www.docker.com/products/build-cloud/), you can set the driver to:
  driver: cloud org-name/builder-name

  # Provenance
  #
  # It is used to configure provenance attestations for the build result.
  # The value can also be a boolean to enable or disable provenance attestations.
  provenance: mode=max

  # SBOM (Software Bill of Materials)
  #
  # It is used to configure SBOM generation for the build result.
  # The value can also be a boolean to enable or disable SBOM generation.
  sbom: true


================================================
FILE: lib/kamal/configuration/docs/configuration.yml
================================================
# Kamal Configuration
#
# Configuration is read from the `config/deploy.yml`.

# Destinations
#
# When running commands, you can specify a destination with the `-d` flag,
# e.g., `kamal deploy -d staging`.
#
# In this case, the configuration will also be read from `config/deploy.staging.yml`
# and merged with the base configuration.

# Extensions
#
# Kamal will not accept unrecognized keys in the configuration file.
#
# However, you might want to declare a configuration block using YAML anchors
# and aliases to avoid repetition.
#
# You can prefix a configuration section with `x-` to indicate that it is an
# extension. Kamal will ignore the extension and not raise an error.

# The service name
#
# This is a required value. It is used as the container name prefix.
service: myapp

# The Docker image name
#
# The image will be pushed to the configured registry.
image: my-image

# Labels
#
# Additional labels to add to the container:
labels:
  my-label: my-value

# Volumes
#
# Additional volumes to mount into the container:
volumes:
  - /path/on/host:/path/in/container:ro

# Registry
#
# The Docker registry configuration, see kamal docs registry:
registry:
  ...

# Servers
#
# The servers to deploy to, optionally with custom roles, see kamal docs servers:
servers:
  ...

# Environment variables
#
# See kamal docs env:
env:
  ...

# Asset path
#
# Used for asset bridging across deployments, default to `nil`.
#
# If there are changes to CSS or JS files, we may get requests
# for the old versions on the new container, and vice versa.
#
# To avoid 404s, we can specify an asset path.
# Kamal will replace that path in the container with a mapped
# volume containing both sets of files.
# This requires that file names change when the contents change
# (e.g., by including a hash of the contents in the name).
#
# To configure this, set the path to the assets.
#
# You can also specify mount options after a colon, such as `ro` for read-only
# or `z`/`Z` for SELinux labels
asset_path: /path/to/assets

# Hooks path
#
# Path to hooks, defaults to `.kamal/hooks`.
# See https://kamal-deploy.org/docs/hooks for more information:
hooks_path: /user_home/kamal/hooks

# Hook output
#
# Hook output visibility. Can be set globally or per-hook.
# CLI flags (`-v`, `-q`) override these settings.
#
# - `:quiet` - hook output is hidden
# - `:verbose` - hook output is shown
#
# With no setting, hook output follows CLI verbosity flags.
#
# Note: Failed hooks always show output in the error message regardless of setting.
#
# Global setting for all hooks:
hooks_output: :verbose

# Or per-hook settings:
hooks_output:
  pre-deploy: :verbose
  pre-build: :quiet

# Secrets path
#
# Path to secrets, defaults to `.kamal/secrets`.
# Kamal will look for `<secrets_path>-common` and `<secrets_path>` (or `<secrets_path>.<destination>` when using destinations):
secrets_path: /user_home/kamal/secrets

# Error pages
#
# A directory relative to the app root to find error pages for the proxy to serve.
# Any files in the format 4xx.html or 5xx.html will be copied to the hosts.
error_pages_path: public

# Require destinations
#
# Whether deployments require a destination to be specified, defaults to `false`:
require_destination: true

# Primary role
#
# This defaults to `web`, but if you have no web role, you can change this:
primary_role: workers

# Allowing empty roles
#
# Whether roles with no servers are allowed. Defaults to `false`:
allow_empty_roles: false

# Retain containers
#
# How many old containers and images we retain, defaults to 5:
retain_containers: 3

# Minimum version
#
# The minimum version of Kamal required to deploy this configuration, defaults to `nil`:
minimum_version: 1.3.0

# Readiness delay
#
# Seconds to wait for a container to boot after it is running, default 7.
#
# This only applies to containers that do not run a proxy or specify a healthcheck:
readiness_delay: 4

# Deploy timeout
#
# How long to wait for a container to become ready, default 30:
deploy_timeout: 10

# Drain timeout
#
# How long to wait for a container to drain, default 30:
drain_timeout: 10

# Run directory
#
# Directory to store kamal runtime files in on the host, default `.kamal`:
run_directory: /etc/kamal

# SSH options
#
# See kamal docs ssh:
ssh:
  ...

# Builder options
#
# See kamal docs builder:
builder:
  ...

# Accessories
#
# Additional services to run in Docker, see kamal docs accessory:
accessories:
  ...

# Proxy
#
# Configuration for kamal-proxy, see kamal docs proxy:
proxy:
  ...

# SSHKit
#
# See kamal docs sshkit:
sshkit:
  ...

# Boot options
#
# See kamal docs boot:
boot:
  ...

# Logging
#
# Docker logging configuration, see kamal docs logging:
logging:
  ...

# Aliases
#
# Alias configuration, see kamal docs alias:
aliases:
  ...


================================================
FILE: lib/kamal/configuration/docs/env.yml
================================================
# Environment variables
#
# Environment variables can be set directly in the Kamal configuration or
# read from `.kamal/secrets`.

# Reading environment variables from the configuration
#
# Environment variables can be set directly in the configuration file.
#
# These are passed to the `docker run` command when deploying.
env:
  DATABASE_HOST: mysql-db1
  DATABASE_PORT: 3306

# Secrets
#
# Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file.
#
# If you are using destinations, secrets will instead be read from `.kamal/secrets.<DESTINATION>` if
# it exists.
#
# Common secrets across all destinations can be set in `.kamal/secrets-common`.
#
# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords.
# You can use variable or command substitution in the secrets file.
#
# ```shell
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# RAILS_MASTER_KEY=$(cat config/master.key)
# ```
#
# You can also use [secret helpers](../../commands/secrets) for some common password managers.
#
# ```shell
# SECRETS=$(kamal secrets fetch ...)
#
# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
# ```
#
# If you store secrets directly in `.kamal/secrets`, ensure that it is not checked into version control.
#
# To pass the secrets, you should list them under the `secret` key. When you do this, the
# other variables need to be moved under the `clear` key.
#
# Unlike clear values, secrets are not passed directly to the container
# but are stored in an env file on the host:
env:
  clear:
    DB_USER: app
  secret:
    - DB_PASSWORD

# Aliased secrets
#
# You can also alias secrets to other secrets using a `:` separator.
#
# This is useful when the ENV name is different from the secret name. For example, if you have two
# places where you need to define the ENV variable `DB_PASSWORD`, but the value is different depending
# on the context.
#
# ```shell
# SECRETS=$(kamal secrets fetch ...)
#
# MAIN_DB_PASSWORD=$(kamal secrets extract MAIN_DB_PASSWORD $SECRETS)
# SECONDARY_DB_PASSWORD=$(kamal secrets extract SECONDARY_DB_PASSWORD $SECRETS)
# ```
env:
  secret:
    - DB_PASSWORD:MAIN_DB_PASSWORD
  tags:
    secondary_db:
      secret:
        - DB_PASSWORD:SECONDARY_DB_PASSWORD
accessories:
  main_db_accessory:
    env:
      secret:
        - DB_PASSWORD:MAIN_DB_PASSWORD
  secondary_db_accessory:
    env:
      secret:
        - DB_PASSWORD:SECONDARY_DB_PASSWORD

# Tags
#
# Tags are used to add extra env variables to specific hosts.
# See kamal docs servers for how to tag hosts.
#
# Tags are only allowed in the top-level env configuration (i.e., not under a role-specific env).
#
# The env variables can be specified with secret and clear values as explained above.
env:
  tags:
    <tag1>:
      MYSQL_USER: monitoring
    <tag2>:
      clear:
        MYSQL_USER: readonly
      secret:
        - MYSQL_PASSWORD

# Example configuration
env:
  clear:
    MYSQL_USER: app
  secret:
    - MYSQL_PASSWORD
  tags:
    monitoring:
      MYSQL_USER: monitoring
    replica:
      clear:
        MYSQL_USER: readonly
      secret:
        - READONLY_PASSWORD


================================================
FILE: lib/kamal/configuration/docs/logging.yml
================================================
# Custom logging configuration
#
# Set these to control the Docker logging driver and options.

# Logging settings
#
# These go under the logging key in the configuration file.
#
# This can be specified at the root level or for a specific role.
logging:

  # Driver
  #
  # The logging driver to use, passed to Docker via `--log-driver`:
  driver: json-file

  # Options
  #
  # Any logging options to pass to the driver, passed to Docker via `--log-opt`:
  options:
    max-size: 100m


================================================
FILE: lib/kamal/configuration/docs/proxy.yml
================================================
# Proxy
#
# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide
# gapless deployments. It runs on ports 80 and 443 and forwards req
Download .txt
gitextract_hqeups_8/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── docker-publish.yml
├── .gitignore
├── .rubocop.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Gemfile
├── MIT-LICENSE
├── README.md
├── bin/
│   ├── docs
│   ├── kamal
│   ├── release
│   └── test
├── gemfiles/
│   └── rails_edge.gemfile
├── kamal.gemspec
├── lib/
│   ├── kamal/
│   │   ├── cli/
│   │   │   ├── accessory.rb
│   │   │   ├── alias/
│   │   │   │   └── command.rb
│   │   │   ├── app/
│   │   │   │   ├── assets.rb
│   │   │   │   ├── boot.rb
│   │   │   │   ├── error_pages.rb
│   │   │   │   └── ssl_certificates.rb
│   │   │   ├── app.rb
│   │   │   ├── base.rb
│   │   │   ├── build/
│   │   │   │   ├── clone.rb
│   │   │   │   └── port_forwarding.rb
│   │   │   ├── build.rb
│   │   │   ├── healthcheck/
│   │   │   │   ├── barrier.rb
│   │   │   │   ├── error.rb
│   │   │   │   └── poller.rb
│   │   │   ├── lock.rb
│   │   │   ├── main.rb
│   │   │   ├── proxy.rb
│   │   │   ├── prune.rb
│   │   │   ├── registry.rb
│   │   │   ├── secrets.rb
│   │   │   ├── server.rb
│   │   │   └── templates/
│   │   │       ├── deploy.yml
│   │   │       ├── sample_hooks/
│   │   │       │   ├── docker-setup.sample
│   │   │       │   ├── post-app-boot.sample
│   │   │       │   ├── post-deploy.sample
│   │   │       │   ├── post-proxy-reboot.sample
│   │   │       │   ├── pre-app-boot.sample
│   │   │       │   ├── pre-build.sample
│   │   │       │   ├── pre-connect.sample
│   │   │       │   ├── pre-deploy.sample
│   │   │       │   └── pre-proxy-reboot.sample
│   │   │       └── secrets
│   │   ├── cli.rb
│   │   ├── commander/
│   │   │   └── specifics.rb
│   │   ├── commander.rb
│   │   ├── commands/
│   │   │   ├── accessory/
│   │   │   │   └── proxy.rb
│   │   │   ├── accessory.rb
│   │   │   ├── app/
│   │   │   │   ├── assets.rb
│   │   │   │   ├── containers.rb
│   │   │   │   ├── error_pages.rb
│   │   │   │   ├── execution.rb
│   │   │   │   ├── images.rb
│   │   │   │   ├── logging.rb
│   │   │   │   └── proxy.rb
│   │   │   ├── app.rb
│   │   │   ├── auditor.rb
│   │   │   ├── base.rb
│   │   │   ├── builder/
│   │   │   │   ├── base.rb
│   │   │   │   ├── clone.rb
│   │   │   │   ├── cloud.rb
│   │   │   │   ├── hybrid.rb
│   │   │   │   ├── local.rb
│   │   │   │   ├── pack.rb
│   │   │   │   └── remote.rb
│   │   │   ├── builder.rb
│   │   │   ├── docker.rb
│   │   │   ├── hook.rb
│   │   │   ├── lock.rb
│   │   │   ├── proxy.rb
│   │   │   ├── prune.rb
│   │   │   ├── registry.rb
│   │   │   └── server.rb
│   │   ├── commands.rb
│   │   ├── configuration/
│   │   │   ├── accessory.rb
│   │   │   ├── alias.rb
│   │   │   ├── boot.rb
│   │   │   ├── builder.rb
│   │   │   ├── docs/
│   │   │   │   ├── accessory.yml
│   │   │   │   ├── alias.yml
│   │   │   │   ├── boot.yml
│   │   │   │   ├── builder.yml
│   │   │   │   ├── configuration.yml
│   │   │   │   ├── env.yml
│   │   │   │   ├── logging.yml
│   │   │   │   ├── proxy.yml
│   │   │   │   ├── registry.yml
│   │   │   │   ├── role.yml
│   │   │   │   ├── servers.yml
│   │   │   │   ├── ssh.yml
│   │   │   │   └── sshkit.yml
│   │   │   ├── env/
│   │   │   │   └── tag.rb
│   │   │   ├── env.rb
│   │   │   ├── logging.rb
│   │   │   ├── proxy/
│   │   │   │   ├── boot.rb
│   │   │   │   └── run.rb
│   │   │   ├── proxy.rb
│   │   │   ├── registry.rb
│   │   │   ├── role.rb
│   │   │   ├── servers.rb
│   │   │   ├── ssh.rb
│   │   │   ├── sshkit.rb
│   │   │   ├── validation.rb
│   │   │   ├── validator/
│   │   │   │   ├── accessory.rb
│   │   │   │   ├── alias.rb
│   │   │   │   ├── builder.rb
│   │   │   │   ├── configuration.rb
│   │   │   │   ├── env.rb
│   │   │   │   ├── proxy.rb
│   │   │   │   ├── registry.rb
│   │   │   │   ├── role.rb
│   │   │   │   └── servers.rb
│   │   │   ├── validator.rb
│   │   │   └── volume.rb
│   │   ├── configuration.rb
│   │   ├── docker.rb
│   │   ├── env_file.rb
│   │   ├── git.rb
│   │   ├── secrets/
│   │   │   ├── adapters/
│   │   │   │   ├── aws_secrets_manager.rb
│   │   │   │   ├── base.rb
│   │   │   │   ├── bitwarden.rb
│   │   │   │   ├── bitwarden_secrets_manager.rb
│   │   │   │   ├── doppler.rb
│   │   │   │   ├── enpass.rb
│   │   │   │   ├── gcp_secret_manager.rb
│   │   │   │   ├── last_pass.rb
│   │   │   │   ├── one_password.rb
│   │   │   │   ├── passbolt.rb
│   │   │   │   └── test.rb
│   │   │   ├── adapters.rb
│   │   │   └── dotenv/
│   │   │       └── inline_command_substitution.rb
│   │   ├── secrets.rb
│   │   ├── sshkit_with_ext.rb
│   │   ├── tags.rb
│   │   ├── utils/
│   │   │   └── sensitive.rb
│   │   ├── utils.rb
│   │   └── version.rb
│   └── kamal.rb
└── test/
    ├── cli/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── build_test.rb
    │   ├── cli_test_case.rb
    │   ├── lock_test.rb
    │   ├── main_test.rb
    │   ├── proxy_test.rb
    │   ├── prune_test.rb
    │   ├── registry_test.rb
    │   ├── secrets_test.rb
    │   └── server_test.rb
    ├── commander_test.rb
    ├── commands/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── auditor_test.rb
    │   ├── builder_test.rb
    │   ├── docker_test.rb
    │   ├── hook_test.rb
    │   ├── lock_test.rb
    │   ├── proxy_test.rb
    │   ├── prune_test.rb
    │   ├── registry_test.rb
    │   └── server_test.rb
    ├── configuration/
    │   ├── accessory_test.rb
    │   ├── boot_test.rb
    │   ├── builder_test.rb
    │   ├── env/
    │   │   └── tags_test.rb
    │   ├── env_test.rb
    │   ├── proxy/
    │   │   └── boot_test.rb
    │   ├── proxy_test.rb
    │   ├── role_test.rb
    │   ├── ssh_test.rb
    │   ├── sshkit_test.rb
    │   ├── validation_test.rb
    │   └── volume_test.rb
    ├── configuration_test.rb
    ├── env_file_test.rb
    ├── fixtures/
    │   ├── deploy.elsewhere.yml
    │   ├── deploy.erb.yml
    │   ├── deploy.yml
    │   ├── deploy2.yml
    │   ├── deploy_for_dest.mars.yml
    │   ├── deploy_for_dest.world.yml
    │   ├── deploy_for_dest.yml
    │   ├── deploy_for_required_dest.world.yml
    │   ├── deploy_for_required_dest.yml
    │   ├── deploy_primary_web_role_override.yml
    │   ├── deploy_simple.yml
    │   ├── deploy_with_accessories.yml
    │   ├── deploy_with_accessories_on_independent_server.yml
    │   ├── deploy_with_accessories_with_different_registries.yml
    │   ├── deploy_with_aliases.yml
    │   ├── deploy_with_assets.yml
    │   ├── deploy_with_boot_strategy.yml
    │   ├── deploy_with_cloud_builder.yml
    │   ├── deploy_with_env_tags.yml
    │   ├── deploy_with_error_pages.yml
    │   ├── deploy_with_extensions.yml
    │   ├── deploy_with_hybrid_builder.yml
    │   ├── deploy_with_local_registry.yml
    │   ├── deploy_with_local_registry_and_accessories.yml
    │   ├── deploy_with_local_registry_and_remote_builder.yml
    │   ├── deploy_with_local_registry_and_remote_builder_with_port.yml
    │   ├── deploy_with_multiple_proxy_roles.yml
    │   ├── deploy_with_only_workers.yml
    │   ├── deploy_with_parallel_roles.yml
    │   ├── deploy_with_proxy.yml
    │   ├── deploy_with_proxy_roles.yml
    │   ├── deploy_with_proxy_run_config.yml
    │   ├── deploy_with_proxy_run_config_conflicts.yml
    │   ├── deploy_with_remote_builder.yml
    │   ├── deploy_with_remote_builder_and_custom_ports.yml
    │   ├── deploy_with_roles.yml
    │   ├── deploy_with_roles_workers_primary.yml
    │   ├── deploy_with_secrets.yml
    │   ├── deploy_with_single_accessory.yml
    │   ├── deploy_with_two_roles_one_host.yml
    │   ├── deploy_with_uncommon_hostnames.yml
    │   ├── deploy_without_clone.yml
    │   ├── deploy_without_parallel_roles.yml
    │   └── files/
    │       ├── my.cnf
    │       └── structure.sql.erb
    ├── git_test.rb
    ├── integration/
    │   ├── accessory_test.rb
    │   ├── app_test.rb
    │   ├── broken_deploy_test.rb
    │   ├── docker/
    │   │   ├── deployer/
    │   │   │   ├── .dockerignore
    │   │   │   ├── Dockerfile
    │   │   │   ├── app/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   ├── hooks/
    │   │   │   │   │   │   ├── docker-setup
    │   │   │   │   │   │   ├── post-app-boot
    │   │   │   │   │   │   ├── post-deploy
    │   │   │   │   │   │   ├── post-proxy-reboot
    │   │   │   │   │   │   ├── pre-app-boot
    │   │   │   │   │   │   ├── pre-build
    │   │   │   │   │   │   ├── pre-connect
    │   │   │   │   │   │   ├── pre-deploy
    │   │   │   │   │   │   └── pre-proxy-reboot
    │   │   │   │   │   ├── secrets
    │   │   │   │   │   └── secrets-common
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   ├── busybox.conf
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_custom_certificate/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── certs/
    │   │   │   │   │   ├── cert.pem
    │   │   │   │   │   └── key.pem
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_destinations/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   ├── deploy.production.yml
    │   │   │   │   │   ├── deploy.staging.yml
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_parallel_roles/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   ├── default.conf
    │   │   │   │   └── error_pages/
    │   │   │   │       └── 503.html
    │   │   │   ├── app_with_proxied_accessory/
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── app_with_roles/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   ├── hooks/
    │   │   │   │   │   │   ├── docker-setup
    │   │   │   │   │   │   ├── post-deploy
    │   │   │   │   │   │   ├── post-proxy-reboot
    │   │   │   │   │   │   ├── pre-build
    │   │   │   │   │   │   ├── pre-connect
    │   │   │   │   │   │   ├── pre-deploy
    │   │   │   │   │   │   └── pre-proxy-reboot
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   ├── default.conf
    │   │   │   │   └── error_pages/
    │   │   │   │       └── 503.html
    │   │   │   ├── app_with_traefik/
    │   │   │   │   ├── .kamal/
    │   │   │   │   │   └── secrets
    │   │   │   │   ├── Dockerfile
    │   │   │   │   ├── config/
    │   │   │   │   │   └── deploy.yml
    │   │   │   │   └── default.conf
    │   │   │   ├── boot.sh
    │   │   │   ├── break_app.sh
    │   │   │   ├── setup.sh
    │   │   │   └── update_app_rev.sh
    │   │   ├── load_balancer/
    │   │   │   ├── Dockerfile
    │   │   │   └── default.conf
    │   │   ├── registry/
    │   │   │   ├── Dockerfile
    │   │   │   └── boot.sh
    │   │   ├── shared/
    │   │   │   ├── .dockerignore
    │   │   │   ├── Dockerfile
    │   │   │   ├── boot.sh
    │   │   │   └── registry-dns.conf
    │   │   └── vm/
    │   │       ├── Dockerfile
    │   │       └── boot.sh
    │   ├── docker-compose.yml
    │   ├── integration_test.rb
    │   ├── lock_test.rb
    │   ├── main_test.rb
    │   └── proxy_test.rb
    ├── secrets/
    │   ├── aws_secrets_manager_adapter_test.rb
    │   ├── bitwarden_adapter_test.rb
    │   ├── bitwarden_secrets_manager_adapter_test.rb
    │   ├── doppler_adapter_test.rb
    │   ├── dotenv_inline_command_substitution_test.rb
    │   ├── enpass_adapter_test.rb
    │   ├── gcp_secret_manager_adapter_test.rb
    │   ├── last_pass_adapter_test.rb
    │   ├── one_password_adapter_test.rb
    │   └── passbolt_adapter_test.rb
    ├── secrets_test.rb
    ├── sshkit_dns_retry_test.rb
    ├── test_helper.rb
    └── utils_test.rb
Download .txt
SYMBOL INDEX (1275 symbols across 162 files)

FILE: lib/kamal.rb
  type Kamal (line 1) | module Kamal
    class ConfigurationError (line 2) | class ConfigurationError < StandardError; end

FILE: lib/kamal/cli.rb
  type Kamal::Cli (line 1) | module Kamal::Cli
    class BootError (line 2) | class BootError < StandardError; end
    class HookError (line 3) | class HookError < StandardError; end
    class LockError (line 4) | class LockError < StandardError; end
    class DependencyError (line 5) | class DependencyError < StandardError; end

FILE: lib/kamal/cli/accessory.rb
  class Kamal::Cli::Accessory (line 4) | class Kamal::Cli::Accessory < Kamal::Cli::Base
    method boot (line 6) | def boot(name, prepare: true)
    method upload (line 44) | def upload(name)
    method directories (line 63) | def directories(name)
    method reboot (line 78) | def reboot(name)
    method start (line 93) | def start(name)
    method stop (line 109) | def stop(name)
    method restart (line 126) | def restart(name)
    method details (line 134) | def details(name)
    method exec (line 149) | def exec(name, *cmd)
    method logs (line 191) | def logs(name)
    method pull_image (line 215) | def pull_image(name)
    method remove (line 228) | def remove(name)
    method remove_container (line 241) | def remove_container(name)
    method remove_image (line 253) | def remove_image(name)
    method remove_service_directory (line 265) | def remove_service_directory(name)
    method upgrade (line 278) | def upgrade(name)
    method with_accessory (line 295) | def with_accessory(name)
    method error_on_missing_accessory (line 304) | def error_on_missing_accessory(name)
    method accessory_hosts (line 312) | def accessory_hosts(accessory)
    method remove_accessory (line 316) | def remove_accessory(name)
    method prepare (line 323) | def prepare(name)

FILE: lib/kamal/cli/alias/command.rb
  class Kamal::Cli::Alias::Command (line 1) | class Kamal::Cli::Alias::Command < Thor::DynamicCommand
    method run (line 2) | def run(instance, args = [])

FILE: lib/kamal/cli/app.rb
  class Kamal::Cli::App (line 1) | class Kamal::Cli::App < Kamal::Cli::Base
    method boot (line 3) | def boot
    method start (line 44) | def start
    method stop (line 63) | def stop
    method details (line 84) | def details
    method exec (line 96) | def exec(*cmd)
    method containers (line 155) | def containers
    method stale_containers (line 162) | def stale_containers
    method images (line 185) | def images
    method logs (line 198) | def logs
    method remove (line 235) | def remove
    method live (line 245) | def live
    method maintenance (line 256) | def maintenance
    method remove_container (line 267) | def remove_container(version)
    method remove_containers (line 277) | def remove_containers
    method remove_images (line 287) | def remove_images
    method remove_app_directories (line 297) | def remove_app_directories
    method version (line 308) | def version
    method hosts_removing_all_roles (line 317) | def hosts_removing_all_roles
    method using_version (line 321) | def using_version(new_version)
    method current_running_version (line 335) | def current_running_version(host: KAMAL.primary_host)
    method version_or_latest (line 344) | def version_or_latest
    method with_lock_if_stopping (line 348) | def with_lock_if_stopping
    method host_boot_groups (line 356) | def host_boot_groups

FILE: lib/kamal/cli/app/assets.rb
  class Kamal::Cli::App::Assets (line 1) | class Kamal::Cli::App::Assets
    method initialize (line 6) | def initialize(host, role, sshkit)
    method run (line 12) | def run
    method app (line 21) | def app

FILE: lib/kamal/cli/app/boot.rb
  class Kamal::Cli::App::Boot (line 1) | class Kamal::Cli::App::Boot
    method initialize (line 6) | def initialize(host, role, sshkit, version, barrier)
    method run (line 14) | def run
    method old_version_renamed_if_clashing (line 35) | def old_version_renamed_if_clashing
    method start_new_version (line 46) | def start_new_version
    method stop_new_version (line 66) | def stop_new_version
    method stop_old_version (line 70) | def stop_old_version(version)
    method release_barrier (line 76) | def release_barrier
    method wait_at_barrier (line 82) | def wait_at_barrier
    method close_barrier (line 91) | def close_barrier
    method barrier_role? (line 103) | def barrier_role?
    method app (line 107) | def app
    method auditor (line 111) | def auditor
    method audit (line 115) | def audit(message)
    method gatekeeper? (line 119) | def gatekeeper?
    method queuer? (line 123) | def queuer?

FILE: lib/kamal/cli/app/error_pages.rb
  class Kamal::Cli::App::ErrorPages (line 1) | class Kamal::Cli::App::ErrorPages
    method initialize (line 7) | def initialize(host, sshkit)
    method run (line 12) | def run
    method with_error_pages_tmpdir (line 22) | def with_error_pages_tmpdir

FILE: lib/kamal/cli/app/ssl_certificates.rb
  class Kamal::Cli::App::SslCertificates (line 1) | class Kamal::Cli::App::SslCertificates
    method initialize (line 5) | def initialize(host, role, sshkit)
    method run (line 11) | def run
    method app (line 25) | def app

FILE: lib/kamal/cli/base.rb
  type Kamal::Cli (line 4) | module Kamal::Cli
    class Base (line 5) | class Base < Thor
      method exit_on_failure? (line 10) | def self.exit_on_failure?() true end
      method dynamic_command_class (line 11) | def self.dynamic_command_class() Kamal::Cli::Alias::Command end
      method initialize (line 27) | def initialize(args = [], local_options = {}, config = {})
      method options_with_subcommand_class_options (line 40) | def options_with_subcommand_class_options
      method initialize_commander (line 44) | def initialize_commander
      method print_runtime (line 66) | def print_runtime
      method with_lock (line 75) | def with_lock
      method confirming (line 96) | def confirming(question)
      method acquire_lock (line 106) | def acquire_lock
      method release_lock (line 117) | def release_lock
      method raise_if_locked (line 124) | def raise_if_locked
      method run_hook (line 136) | def run_hook(hook, **extra_details)
      method on (line 168) | def on(*args, &block)
      method pre_connect_if_required (line 174) | def pre_connect_if_required
      method command (line 181) | def command
      method subcommand (line 192) | def subcommand
      method first_invocation (line 199) | def first_invocation
      method reset_invocation (line 203) | def reset_invocation(cli_class)
      method ensure_run_directory (line 207) | def ensure_run_directory
      method with_env (line 213) | def with_env(env)
      method ensure_docker_installed (line 222) | def ensure_docker_installed

FILE: lib/kamal/cli/build.rb
  class Kamal::Cli::Build (line 1) | class Kamal::Cli::Build < Kamal::Cli::Base
    class BuildError (line 2) | class BuildError < StandardError; end
    method deliver (line 5) | def deliver
    method push (line 13) | def push
    method pull (line 70) | def pull
    method create (line 87) | def create
    method remove (line 108) | def remove
    method details (line 116) | def details
    method dev (line 126) | def dev
    method connect_to_remote_host (line 160) | def connect_to_remote_host(remote_host)
    method mirror_hosts (line 173) | def mirror_hosts
    method pull_on_hosts (line 188) | def pull_on_hosts(hosts)
    method login_to_registry_locally (line 197) | def login_to_registry_locally
    method login_to_registry_remotely (line 207) | def login_to_registry_remotely
    method forward_local_registry_port_for_remote_builder (line 213) | def forward_local_registry_port_for_remote_builder(&block)
    method forward_local_registry_port (line 222) | def forward_local_registry_port(hosts, **ssh_options, &block)
    method remote_builder_ssh_options (line 231) | def remote_builder_ssh_options(remote_uri)

FILE: lib/kamal/cli/build/clone.rb
  class Kamal::Cli::Build::Clone (line 1) | class Kamal::Cli::Build::Clone
    method initialize (line 5) | def initialize(sshkit)
    method prepare (line 9) | def prepare
    method clone_repo (line 30) | def clone_repo
    method reset (line 37) | def reset
    method validate! (line 45) | def validate!

FILE: lib/kamal/cli/build/port_forwarding.rb
  class Kamal::Cli::Build::PortForwarding (line 3) | class Kamal::Cli::Build::PortForwarding
    method initialize (line 6) | def initialize(hosts, port, **ssh_options)
    method forward (line 12) | def forward
    method stop (line 22) | def stop
    method forward_ports (line 27) | def forward_ports
    method error (line 63) | def error(message)

FILE: lib/kamal/cli/healthcheck/barrier.rb
  class Kamal::Cli::Healthcheck::Barrier (line 3) | class Kamal::Cli::Healthcheck::Barrier
    method initialize (line 4) | def initialize
    method close (line 8) | def close
    method open (line 12) | def open
    method wait (line 16) | def wait
    method opened? (line 23) | def opened?
    method set (line 27) | def set(value)

FILE: lib/kamal/cli/healthcheck/error.rb
  class Kamal::Cli::Healthcheck::Error (line 1) | class Kamal::Cli::Healthcheck::Error < StandardError

FILE: lib/kamal/cli/healthcheck/poller.rb
  type Kamal::Cli::Healthcheck::Poller (line 1) | module Kamal::Cli::Healthcheck::Poller
    function wait_for_healthy (line 4) | def wait_for_healthy(&block)
    function info (line 39) | def info(message)

FILE: lib/kamal/cli/lock.rb
  class Kamal::Cli::Lock (line 1) | class Kamal::Cli::Lock < Kamal::Cli::Base
    method status (line 3) | def status
    method acquire (line 13) | def acquire
    method release (line 26) | def release
    method handle_missing_lock (line 36) | def handle_missing_lock

FILE: lib/kamal/cli/main.rb
  class Kamal::Cli::Main (line 1) | class Kamal::Cli::Main < Kamal::Cli::Base
    method setup (line 5) | def setup
    method deploy (line 21) | def deploy(boot_accessories: false)
    method redeploy (line 57) | def redeploy
    method rollback (line 83) | def rollback(version)
    method details (line 107) | def details
    method audit (line 114) | def audit
    method config (line 122) | def config
    method docs (line 129) | def docs(section = nil)
    method init (line 142) | def init
    method remove (line 183) | def remove
    method upgrade (line 197) | def upgrade
    method version (line 226) | def version
    method container_available? (line 258) | def container_available?(version)
    method deploy_options (line 278) | def deploy_options

FILE: lib/kamal/cli/proxy.rb
  class Kamal::Cli::Proxy (line 1) | class Kamal::Cli::Proxy < Kamal::Cli::Base
    method boot (line 3) | def boot
    method boot_config (line 37) | def boot_config(subcommand)
    method reboot (line 109) | def reboot
    method upgrade (line 137) | def upgrade
    method start (line 176) | def start
    method stop (line 186) | def stop
    method restart (line 196) | def restart
    method details (line 204) | def details
    method logs (line 215) | def logs
    method remove (line 238) | def remove
    method remove_container (line 250) | def remove_container
    method remove_image (line 260) | def remove_image
    method remove_proxy_directory (line 270) | def remove_proxy_directory
    method removal_allowed? (line 279) | def removal_allowed?(force)

FILE: lib/kamal/cli/prune.rb
  class Kamal::Cli::Prune (line 1) | class Kamal::Cli::Prune < Kamal::Cli::Base
    method all (line 3) | def all
    method images (line 11) | def images
    method containers (line 23) | def containers

FILE: lib/kamal/cli/registry.rb
  class Kamal::Cli::Registry (line 1) | class Kamal::Cli::Registry < Kamal::Cli::Base
    method setup (line 5) | def setup
    method remove (line 19) | def remove
    method login (line 31) | def login
    method logout (line 42) | def logout

FILE: lib/kamal/cli/secrets.rb
  class Kamal::Cli::Secrets (line 1) | class Kamal::Cli::Secrets < Kamal::Cli::Base
    method fetch (line 7) | def fetch(*secrets)
    method extract (line 22) | def extract(name, secrets)
    method print (line 32) | def print
    method initialize_adapter (line 39) | def initialize_adapter(adapter)
    method return_or_puts (line 43) | def return_or_puts(value, inline: nil)

FILE: lib/kamal/cli/server.rb
  class Kamal::Cli::Server (line 1) | class Kamal::Cli::Server < Kamal::Cli::Base
    method exec (line 4) | def exec(*cmd)
    method bootstrap (line 29) | def bootstrap

FILE: lib/kamal/commander.rb
  class Kamal::Commander (line 5) | class Kamal::Commander
    method initialize (line 10) | def initialize
    method reset (line 14) | def reset
    method config (line 23) | def config
    method configure (line 30) | def configure(**kwargs)
    method configured? (line 34) | def configured?
    method specific_primary! (line 38) | def specific_primary!
    method specific_roles= (line 47) | def specific_roles=(role_names)
    method specific_hosts= (line 56) | def specific_hosts=(hosts)
    method with_specific_hosts (line 65) | def with_specific_hosts(hosts)
    method accessory_names (line 72) | def accessory_names
    method app (line 76) | def app(role: nil, host: nil)
    method accessory (line 80) | def accessory(name)
    method auditor (line 84) | def auditor(**details)
    method builder (line 88) | def builder
    method docker (line 92) | def docker
    method hook (line 96) | def hook
    method lock (line 100) | def lock
    method proxy (line 104) | def proxy(host)
    method prune (line 108) | def prune
    method registry (line 112) | def registry
    method server (line 116) | def server
    method alias (line 120) | def alias(name)
    method resolve_alias (line 124) | def resolve_alias(name)
    method with_verbosity (line 133) | def with_verbosity(level)
    method holding_lock? (line 145) | def holding_lock?
    method connected? (line 149) | def connected?
    method configure_sshkit_with (line 155) | def configure_sshkit_with(config)
    method specifics (line 166) | def specifics

FILE: lib/kamal/commander/specifics.rb
  class Kamal::Commander::Specifics (line 1) | class Kamal::Commander::Specifics
    method initialize (line 5) | def initialize(config, specific_hosts, specific_roles)
    method roles_on (line 17) | def roles_on(host)
    method app_hosts (line 21) | def app_hosts
    method proxy_hosts (line 25) | def proxy_hosts
    method accessory_hosts (line 29) | def accessory_hosts
    method primary_specific_role (line 36) | def primary_specific_role
    method primary_or_first_role (line 40) | def primary_or_first_role(roles)
    method specified_roles (line 44) | def specified_roles
    method specified_hosts (line 49) | def specified_hosts
    method sort_primary_role_hosts_first! (line 59) | def sort_primary_role_hosts_first!(hosts)

FILE: lib/kamal/commands.rb
  type Kamal::Commands (line 1) | module Kamal::Commands

FILE: lib/kamal/commands/accessory.rb
  class Kamal::Commands::Accessory (line 1) | class Kamal::Commands::Accessory < Kamal::Commands::Base
    method initialize (line 10) | def initialize(config, name:)
    method run (line 15) | def run(host: nil)
    method start (line 32) | def start
    method stop (line 36) | def stop
    method info (line 40) | def info(all: false, quiet: false)
    method logs (line 44) | def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_opt...
    method follow_logs (line 50) | def follow_logs(timestamps: true, grep: nil, grep_options: nil)
    method execute_in_existing_container (line 57) | def execute_in_existing_container(*command, interactive: false)
    method execute_in_new_container (line 64) | def execute_in_new_container(*command, interactive: false)
    method execute_in_existing_container_over_ssh (line 76) | def execute_in_existing_container_over_ssh(*command)
    method execute_in_new_container_over_ssh (line 80) | def execute_in_new_container_over_ssh(*command)
    method run_over_ssh (line 84) | def run_over_ssh(command)
    method ensure_local_file_present (line 88) | def ensure_local_file_present(local_file)
    method pull_image (line 94) | def pull_image
    method remove_service_directory (line 98) | def remove_service_directory
    method remove_container (line 102) | def remove_container
    method remove_image (line 106) | def remove_image
    method ensure_env_directory (line 110) | def ensure_env_directory
    method service_filter (line 115) | def service_filter

FILE: lib/kamal/commands/accessory/proxy.rb
  type Kamal::Commands::Accessory::Proxy (line 1) | module Kamal::Commands::Accessory::Proxy
    function deploy (line 4) | def deploy(target:)
    function remove (line 8) | def remove
    function proxy_exec (line 13) | def proxy_exec(*command)

FILE: lib/kamal/commands/app.rb
  class Kamal::Commands::App (line 1) | class Kamal::Commands::App < Kamal::Commands::Base
    method initialize (line 10) | def initialize(config, role: nil, host: nil)
    method run (line 16) | def run(hostname: nil)
    method start (line 37) | def start
    method status (line 41) | def status(version:)
    method stop (line 45) | def stop(version: nil)
    method info (line 51) | def info
    method current_running_container_id (line 56) | def current_running_container_id
    method container_id_for_version (line 60) | def container_id_for_version(version, only_running: false)
    method current_running_version (line 64) | def current_running_version
    method list_versions (line 70) | def list_versions(*docker_args, statuses: nil)
    method ensure_env_directory (line 76) | def ensure_env_directory
    method latest_image_id (line 81) | def latest_image_id
    method current_running_container (line 85) | def current_running_container(format:)
    method latest_image_container (line 91) | def latest_image_container(format:)
    method latest_container (line 95) | def latest_container(format:, filters: nil)
    method container_filter_args (line 99) | def container_filter_args(statuses: nil)
    method image_filter_args (line 103) | def image_filter_args
    method extract_version_from_name (line 107) | def extract_version_from_name
    method container_filters (line 112) | def container_filters(statuses: nil)
    method image_filters (line 122) | def image_filters

FILE: lib/kamal/commands/app/assets.rb
  type Kamal::Commands::App::Assets (line 1) | module Kamal::Commands::App::Assets
    function extract_assets (line 2) | def extract_assets
    function sync_asset_volumes (line 14) | def sync_asset_volumes(old_version: nil)
    function clean_up_assets (line 30) | def clean_up_assets
    function find_and_remove_older_siblings (line 37) | def find_and_remove_older_siblings(path)
    function copy_contents (line 48) | def copy_contents(source, destination, continue_on_error: false)

FILE: lib/kamal/commands/app/containers.rb
  type Kamal::Commands::App::Containers (line 1) | module Kamal::Commands::App::Containers
    function list_containers (line 4) | def list_containers
    function list_container_names (line 8) | def list_container_names
    function remove_container (line 12) | def remove_container(version:)
    function rename_container (line 18) | def rename_container(version:, new_version:)
    function remove_containers (line 22) | def remove_containers
    function container_health_log (line 26) | def container_health_log(version:)

FILE: lib/kamal/commands/app/error_pages.rb
  type Kamal::Commands::App::ErrorPages (line 1) | module Kamal::Commands::App::ErrorPages
    function create_error_pages_directory (line 2) | def create_error_pages_directory
    function clean_up_error_pages (line 6) | def clean_up_error_pages

FILE: lib/kamal/commands/app/execution.rb
  type Kamal::Commands::App::Execution (line 1) | module Kamal::Commands::App::Execution
    function execute_in_existing_container (line 2) | def execute_in_existing_container(*command, interactive: false, env:)
    function execute_in_new_container (line 10) | def execute_in_new_container(*command, interactive: false, detach: fal...
    function execute_in_existing_container_over_ssh (line 26) | def execute_in_existing_container_over_ssh(*command, env:)
    function execute_in_new_container_over_ssh (line 30) | def execute_in_new_container_over_ssh(*command, env:)
    function container_name_for_exec (line 35) | def container_name_for_exec

FILE: lib/kamal/commands/app/images.rb
  type Kamal::Commands::App::Images (line 1) | module Kamal::Commands::App::Images
    function list_images (line 2) | def list_images
    function remove_images (line 6) | def remove_images
    function tag_latest_image (line 10) | def tag_latest_image

FILE: lib/kamal/commands/app/logging.rb
  type Kamal::Commands::App::Logging (line 1) | module Kamal::Commands::App::Logging
    function logs (line 2) | def logs(container_id: nil, timestamps: true, since: nil, lines: nil, ...
    function follow_logs (line 9) | def follow_logs(host:, container_id: nil, timestamps: true, lines: nil...
    function container_id_command (line 21) | def container_id_command(container_id)

FILE: lib/kamal/commands/app/proxy.rb
  type Kamal::Commands::App::Proxy (line 1) | module Kamal::Commands::App::Proxy
    function deploy (line 4) | def deploy(target:)
    function remove (line 8) | def remove
    function live (line 12) | def live
    function maintenance (line 16) | def maintenance(**options)
    function remove_proxy_app_directory (line 20) | def remove_proxy_app_directory
    function create_ssl_directory (line 24) | def create_ssl_directory
    function proxy_exec (line 29) | def proxy_exec(*command)

FILE: lib/kamal/commands/auditor.rb
  class Kamal::Commands::Auditor (line 1) | class Kamal::Commands::Auditor < Kamal::Commands::Base
    method initialize (line 5) | def initialize(config, **details)
    method record (line 11) | def record(line, **details)
    method reveal (line 17) | def reveal
    method audit_log_file (line 22) | def audit_log_file
    method audit_tags (line 28) | def audit_tags(**details)
    method make_run_directory (line 32) | def make_run_directory
    method audit_line (line 36) | def audit_line(line, **details)

FILE: lib/kamal/commands/base.rb
  type Kamal::Commands (line 1) | module Kamal::Commands
    class Base (line 2) | class Base
      method initialize (line 9) | def initialize(config)
      method run_over_ssh (line 13) | def run_over_ssh(*command, host:)
      method container_id_for (line 17) | def container_id_for(container_name:, only_running: false)
      method make_directory_for (line 21) | def make_directory_for(remote_file)
      method make_directory (line 25) | def make_directory(path)
      method remove_directory (line 29) | def remove_directory(path)
      method remove_file (line 33) | def remove_file(path)
      method ensure_docker_installed (line 37) | def ensure_docker_installed
      method combine (line 44) | def combine(*commands, by: "&&")
      method chain (line 51) | def chain(*commands)
      method pipe (line 55) | def pipe(*commands)
      method append (line 59) | def append(*commands)
      method write (line 63) | def write(*commands)
      method any (line 67) | def any(*commands)
      method substitute (line 71) | def substitute(*commands)
      method xargs (line 75) | def xargs(command)
      method shell (line 79) | def shell(command)
      method docker (line 83) | def docker(*args)
      method pack (line 87) | def pack(*args)
      method git (line 91) | def git(*args, path: nil)
      method grep (line 95) | def grep(*args)
      method tags (line 99) | def tags(**details)
      method ssh_config_args (line 103) | def ssh_config_args
      method ssh_proxy_args (line 116) | def ssh_proxy_args
      method ssh_keys_args (line 125) | def ssh_keys_args
      method ssh_keys (line 129) | def ssh_keys
      method ensure_local_docker_installed (line 135) | def ensure_local_docker_installed
      method ensure_local_buildx_installed (line 139) | def ensure_local_buildx_installed
      method docker_interactive_args (line 143) | def docker_interactive_args

FILE: lib/kamal/commands/builder.rb
  class Kamal::Commands::Builder (line 3) | class Kamal::Commands::Builder < Kamal::Commands::Base
    method name (line 15) | def name
    method target (line 19) | def target
    method remote (line 35) | def remote
    method local (line 39) | def local
    method hybrid (line 43) | def hybrid
    method pack (line 47) | def pack
    method cloud (line 51) | def cloud

FILE: lib/kamal/commands/builder/base.rb
  class Kamal::Commands::Builder::Base (line 1) | class Kamal::Commands::Builder::Base < Kamal::Commands::Base
    class BuilderError (line 2) | class BuilderError < StandardError; end
    method clean (line 13) | def clean
    method push (line 17) | def push(export_action = "registry", tag_as_dirty: false, no_cache: fa...
    method pull (line 29) | def pull
    method info (line 33) | def info
    method inspect_builder (line 39) | def inspect_builder
    method build_options (line 43) | def build_options
    method build_context (line 47) | def build_context
    method validate_image (line 51) | def validate_image
    method first_mirror (line 60) | def first_mirror
    method login_to_registry_locally? (line 64) | def login_to_registry_locally?
    method push_env (line 68) | def push_env
    method build_tag_names (line 73) | def build_tag_names(tag_as_dirty: false)
    method build_tag_options (line 79) | def build_tag_options(tag_as_dirty: false)
    method build_cache (line 83) | def build_cache
    method build_labels (line 90) | def build_labels
    method build_args (line 94) | def build_args
    method build_secrets (line 98) | def build_secrets
    method build_dockerfile (line 102) | def build_dockerfile
    method build_target (line 110) | def build_target
    method build_ssh (line 114) | def build_ssh
    method builder_provenance (line 118) | def builder_provenance
    method builder_sbom (line 122) | def builder_sbom
    method builder_config (line 126) | def builder_config
    method registry_config (line 130) | def registry_config
    method driver_options (line 134) | def driver_options
    method platform_options (line 140) | def platform_options(arches)

FILE: lib/kamal/commands/builder/clone.rb
  type Kamal::Commands::Builder::Clone (line 1) | module Kamal::Commands::Builder::Clone
    function clone (line 2) | def clone
    function clone_reset_steps (line 6) | def clone_reset_steps
    function clone_status (line 16) | def clone_status
    function clone_revision (line 20) | def clone_revision
    function escaped_root (line 24) | def escaped_root
    function escaped_build_directory (line 28) | def escaped_build_directory

FILE: lib/kamal/commands/builder/cloud.rb
  class Kamal::Commands::Builder::Cloud (line 1) | class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base
    method create (line 4) | def create
    method remove (line 8) | def remove
    method builder_name (line 13) | def builder_name
    method inspect_buildx (line 17) | def inspect_buildx

FILE: lib/kamal/commands/builder/hybrid.rb
  class Kamal::Commands::Builder::Hybrid (line 1) | class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
    method create (line 2) | def create
    method builder_name (line 10) | def builder_name
    method create_local_buildx (line 14) | def create_local_buildx
    method append_remote_buildx (line 18) | def append_remote_buildx

FILE: lib/kamal/commands/builder/local.rb
  class Kamal::Commands::Builder::Local (line 1) | class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
    method create (line 2) | def create
    method remove (line 8) | def remove
    method builder_name (line 13) | def builder_name

FILE: lib/kamal/commands/builder/pack.rb
  class Kamal::Commands::Builder::Pack (line 1) | class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
    method push (line 2) | def push(export_action = "registry", tag_as_dirty: false, no_cache: fa...
    method remove (line 8) | def remove;end
    method info (line 10) | def info
    method build (line 16) | def build(tag_as_dirty: false, no_cache: false)
    method export (line 31) | def export(export_action)
    method platform (line 39) | def platform
    method buildpacks (line 43) | def buildpacks

FILE: lib/kamal/commands/builder/remote.rb
  class Kamal::Commands::Builder::Remote (line 1) | class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
    method create (line 2) | def create
    method remove (line 8) | def remove
    method info (line 14) | def info
    method inspect_builder (line 20) | def inspect_builder
    method login_to_registry_locally? (line 27) | def login_to_registry_locally?
    method push_env (line 31) | def push_env
    method builder_name (line 36) | def builder_name
    method remote_context_name (line 40) | def remote_context_name
    method remote_builder_name_suffix (line 44) | def remote_builder_name_suffix
    method inspect_buildx (line 48) | def inspect_buildx
    method inspect_remote_context (line 54) | def inspect_remote_context
    method create_remote_context (line 60) | def create_remote_context
    method remove_remote_context (line 64) | def remove_remote_context
    method create_buildx (line 68) | def create_buildx
    method remove_buildx (line 72) | def remove_buildx

FILE: lib/kamal/commands/docker.rb
  class Kamal::Commands::Docker (line 1) | class Kamal::Commands::Docker < Kamal::Commands::Base
    method install (line 3) | def install
    method installed? (line 8) | def installed?
    method running? (line 13) | def running?
    method superuser? (line 18) | def superuser?
    method root? (line 22) | def root?
    method in_docker_group? (line 26) | def in_docker_group?
    method add_to_docker_group (line 30) | def add_to_docker_group
    method refresh_session (line 34) | def refresh_session
    method create_network (line 38) | def create_network
    method get_docker (line 43) | def get_docker

FILE: lib/kamal/commands/hook.rb
  class Kamal::Commands::Hook (line 1) | class Kamal::Commands::Hook < Kamal::Commands::Base
    method run (line 2) | def run(hook)
    method env (line 6) | def env(secrets: false, **details)
    method hook_exists? (line 12) | def hook_exists?(hook)
    method hook_file (line 17) | def hook_file(hook)

FILE: lib/kamal/commands/lock.rb
  class Kamal::Commands::Lock (line 5) | class Kamal::Commands::Lock < Kamal::Commands::Base
    method acquire (line 6) | def acquire(message, version)
    method release (line 12) | def release
    method status (line 18) | def status
    method ensure_locks_directory (line 24) | def ensure_locks_directory
    method write_lock_details (line 29) | def write_lock_details(message, version)
    method read_lock_details (line 35) | def read_lock_details
    method stat_lock_dir (line 41) | def stat_lock_dir
    method lock_dir (line 47) | def lock_dir
    method lock_details_file (line 53) | def lock_details_file
    method lock_details (line 57) | def lock_details(message, version)
    method locked_by (line 65) | def locked_by

FILE: lib/kamal/commands/proxy.rb
  class Kamal::Commands::Proxy (line 1) | class Kamal::Commands::Proxy < Kamal::Commands::Base
    method initialize (line 5) | def initialize(config, host:)
    method run (line 10) | def run
    method start (line 27) | def start
    method stop (line 31) | def stop(name: container_name)
    method start_or_run (line 35) | def start_or_run
    method info (line 39) | def info
    method version (line 43) | def version
    method logs (line 49) | def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_opt...
    method follow_logs (line 55) | def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
    method remove_container (line 62) | def remove_container
    method remove_image (line 66) | def remove_image
    method cleanup_traefik (line 70) | def cleanup_traefik
    method ensure_proxy_directory (line 79) | def ensure_proxy_directory
    method remove_proxy_directory (line 83) | def remove_proxy_directory
    method ensure_apps_config_directory (line 87) | def ensure_apps_config_directory
    method boot_config (line 91) | def boot_config
    method read_boot_options (line 95) | def read_boot_options
    method read_image (line 99) | def read_image
    method read_image_version (line 103) | def read_image_version
    method read_run_command (line 107) | def read_run_command
    method reset_boot_options (line 111) | def reset_boot_options
    method reset_image (line 115) | def reset_image
    method reset_image_version (line 119) | def reset_image_version
    method reset_run_command (line 123) | def reset_run_command
    method container_name (line 128) | def container_name
    method read_file (line 132) | def read_file(file, default: nil)
    method docker_run (line 136) | def docker_run

FILE: lib/kamal/commands/prune.rb
  class Kamal::Commands::Prune (line 4) | class Kamal::Commands::Prune < Kamal::Commands::Base
    method dangling_images (line 5) | def dangling_images
    method tagged_images (line 9) | def tagged_images
    method app_containers (line 16) | def app_containers(retain:)
    method stopped_containers_filters (line 24) | def stopped_containers_filters
    method active_image_list (line 28) | def active_image_list
    method service_filter (line 35) | def service_filter

FILE: lib/kamal/commands/registry.rb
  class Kamal::Commands::Registry (line 1) | class Kamal::Commands::Registry < Kamal::Commands::Base
    method login (line 2) | def login(registry_config: nil)
    method logout (line 13) | def logout(registry_config: nil)
    method setup (line 19) | def setup(registry_config: nil)
    method remove (line 28) | def remove
    method local? (line 35) | def local?

FILE: lib/kamal/commands/server.rb
  class Kamal::Commands::Server (line 1) | class Kamal::Commands::Server < Kamal::Commands::Base
    method ensure_run_directory (line 2) | def ensure_run_directory
    method remove_app_directory (line 6) | def remove_app_directory
    method app_directory_count (line 10) | def app_directory_count

FILE: lib/kamal/configuration.rb
  class Kamal::Configuration (line 8) | class Kamal::Configuration
    method create_from (line 20) | def create_from(config_file:, destination: nil, version: nil)
    method load_raw_config (line 28) | def load_raw_config(config_file:, destination: nil)
    method load_config_files (line 33) | def load_config_files(*files)
    method load_config_file (line 37) | def load_config_file(file)
    method destination_config_file (line 49) | def destination_config_file(base_config_file, destination)
    method initialize (line 54) | def initialize(raw_config, destination: nil, version: nil, validate: t...
    method version= (line 92) | def version=(version)
    method version (line 96) | def version
    method abbreviated_version (line 100) | def abbreviated_version
    method minimum_version (line 111) | def minimum_version
    method service_and_destination (line 115) | def service_and_destination
    method roles (line 119) | def roles
    method role (line 123) | def role(name)
    method accessory (line 127) | def accessory(name)
    method all_hosts (line 131) | def all_hosts
    method host_roles (line 135) | def host_roles(host)
    method host_accessories (line 139) | def host_accessories(host)
    method app_hosts (line 143) | def app_hosts
    method primary_host (line 147) | def primary_host
    method primary_role_name (line 151) | def primary_role_name
    method primary_role (line 155) | def primary_role
    method allow_empty_roles? (line 159) | def allow_empty_roles?
    method proxy_roles (line 163) | def proxy_roles
    method proxy_role_names (line 167) | def proxy_role_names
    method proxy_accessories (line 171) | def proxy_accessories
    method proxy_hosts (line 175) | def proxy_hosts
    method image (line 179) | def image
    method proxy_run (line 186) | def proxy_run(host)
    method repository (line 191) | def repository
    method absolute_image (line 195) | def absolute_image
    method latest_image (line 199) | def latest_image
    method latest_tag (line 203) | def latest_tag
    method service_with_version (line 207) | def service_with_version
    method require_destination? (line 211) | def require_destination?
    method retain_containers (line 215) | def retain_containers
    method volume_args (line 219) | def volume_args
    method logging_args (line 227) | def logging_args
    method readiness_delay (line 231) | def readiness_delay
    method deploy_timeout (line 235) | def deploy_timeout
    method drain_timeout (line 239) | def drain_timeout
    method run_directory (line 243) | def run_directory
    method apps_directory (line 247) | def apps_directory
    method app_directory (line 251) | def app_directory
    method env_directory (line 255) | def env_directory
    method assets_directory (line 259) | def assets_directory
    method hooks_path (line 263) | def hooks_path
    method secrets_path (line 267) | def secrets_path
    method asset_path (line 271) | def asset_path
    method error_pages_path (line 275) | def error_pages_path
    method env_tags (line 279) | def env_tags
    method env_tag (line 287) | def env_tag(name)
    method hooks_output_for (line 291) | def hooks_output_for(hook)
    method to_h (line 300) | def to_h
    method ensure_destination_if_required (line 320) | def ensure_destination_if_required
    method ensure_required_keys_present (line 328) | def ensure_required_keys_present
    method ensure_valid_service_name (line 358) | def ensure_valid_service_name
    method ensure_valid_kamal_version (line 364) | def ensure_valid_kamal_version
    method ensure_retain_containers_valid (line 372) | def ensure_retain_containers_valid
    method ensure_no_traefik_reboot_hooks (line 378) | def ensure_no_traefik_reboot_hooks
    method ensure_one_host_for_ssl_roles (line 388) | def ensure_one_host_for_ssl_roles
    method ensure_unique_hosts_for_ssl_roles (line 394) | def ensure_unique_hosts_for_ssl_roles
    method ensure_local_registry_remote_builder_has_ssh_url (line 403) | def ensure_local_registry_remote_builder_has_ssh_url
    method ensure_no_conflicting_proxy_runs (line 413) | def ensure_no_conflicting_proxy_runs
    method proxy_runs (line 422) | def proxy_runs(host)
    method role_names (line 426) | def role_names
    method ensure_valid_hooks_output! (line 430) | def ensure_valid_hooks_output!
    method validate_hooks_output_level! (line 439) | def validate_hooks_output_level!(level, hook = nil)
    method git_version (line 445) | def git_version

FILE: lib/kamal/configuration/accessory.rb
  class Kamal::Configuration::Accessory (line 1) | class Kamal::Configuration::Accessory
    method initialize (line 10) | def initialize(name, config:)
    method service_name (line 26) | def service_name
    method image (line 30) | def image
    method hosts (line 34) | def hosts
    method port (line 38) | def port
    method network_args (line 44) | def network_args
    method publish_args (line 48) | def publish_args
    method labels (line 52) | def labels
    method label_args (line 56) | def label_args
    method env_args (line 60) | def env_args
    method env_directory (line 64) | def env_directory
    method secrets_io (line 68) | def secrets_io
    method secrets_path (line 72) | def secrets_path
    method files (line 76) | def files
    method directories (line 88) | def directories
    method volume_args (line 100) | def volume_args
    method option_args (line 104) | def option_args
    method cmd (line 112) | def cmd
    method running_proxy? (line 116) | def running_proxy?
    method initialize_env (line 123) | def initialize_env
    method initialize_proxy (line 130) | def initialize_proxy
    method initialize_registry (line 138) | def initialize_registry
    method default_labels (line 145) | def default_labels
    method expand_local_file (line 149) | def expand_local_file(local_file)
    method with_env_loaded (line 157) | def with_env_loaded
    method read_dynamic_file (line 164) | def read_dynamic_file(local_file)
    method expand_remote_file (line 168) | def expand_remote_file(remote_file)
    method specific_volumes (line 172) | def specific_volumes
    method path_volumes (line 176) | def path_volumes(paths)
    method parse_path_config (line 185) | def parse_path_config(config, default_mode:)
    method expand_host_path (line 211) | def expand_host_path(host_path)
    method expand_host_path_for_volume (line 215) | def expand_host_path_for_volume(host_path)
    method absolute_path? (line 219) | def absolute_path?(path)
    method service_data_directory (line 223) | def service_data_directory
    method hosts_from_host (line 227) | def hosts_from_host
    method hosts_from_hosts (line 231) | def hosts_from_hosts
    method hosts_from_roles (line 235) | def hosts_from_roles
    method hosts_from_tags (line 243) | def hosts_from_tags
    method extract_hosts_from_config_with_tag (line 251) | def extract_hosts_from_config_with_tag(tag)
    method network (line 261) | def network
    method ensure_valid_roles (line 265) | def ensure_valid_roles

FILE: lib/kamal/configuration/alias.rb
  class Kamal::Configuration::Alias (line 1) | class Kamal::Configuration::Alias
    method initialize (line 6) | def initialize(name, config:)

FILE: lib/kamal/configuration/boot.rb
  class Kamal::Configuration::Boot (line 1) | class Kamal::Configuration::Boot
    method initialize (line 6) | def initialize(config:)
    method limit (line 12) | def limit
    method wait (line 22) | def wait
    method parallel_roles (line 26) | def parallel_roles

FILE: lib/kamal/configuration/builder.rb
  class Kamal::Configuration::Builder (line 1) | class Kamal::Configuration::Builder
    method initialize (line 8) | def initialize(config:)
    method to_h (line 18) | def to_h
    method remote (line 22) | def remote
    method arches (line 26) | def arches
    method local_arches (line 30) | def local_arches
    method remote_arches (line 40) | def remote_arches
    method remote? (line 48) | def remote?
    method local? (line 52) | def local?
    method cloud? (line 56) | def cloud?
    method cached? (line 60) | def cached?
    method pack? (line 64) | def pack?
    method args (line 68) | def args
    method secrets (line 72) | def secrets
    method dockerfile (line 76) | def dockerfile
    method target (line 80) | def target
    method context (line 84) | def context
    method driver (line 88) | def driver
    method pack_builder (line 92) | def pack_builder
    method pack_buildpacks (line 96) | def pack_buildpacks
    method local_disabled? (line 100) | def local_disabled?
    method cache_from (line 104) | def cache_from
    method cache_to (line 115) | def cache_to
    method ssh (line 126) | def ssh
    method provenance (line 130) | def provenance
    method sbom (line 134) | def sbom
    method git_clone? (line 138) | def git_clone?
    method clone_directory (line 142) | def clone_directory
    method build_directory (line 146) | def build_directory
    method docker_driver? (line 155) | def docker_driver?
    method valid? (line 160) | def valid?
    method cache_image (line 172) | def cache_image
    method cache_image_ref (line 176) | def cache_image_ref
    method cache_options (line 180) | def cache_options
    method cache_from_config_for_gha (line 184) | def cache_from_config_for_gha
    method cache_from_config_for_registry (line 191) | def cache_from_config_for_registry
    method cache_to_config_for_gha (line 195) | def cache_to_config_for_gha
    method cache_to_config_for_registry (line 199) | def cache_to_config_for_registry
    method repo_basename (line 203) | def repo_basename
    method repo_relative_pwd (line 207) | def repo_relative_pwd
    method pwd_sha (line 211) | def pwd_sha
    method default_arch (line 215) | def default_arch

FILE: lib/kamal/configuration/env.rb
  class Kamal::Configuration::Env (line 1) | class Kamal::Configuration::Env
    method initialize (line 7) | def initialize(config:, secrets:, context: "env")
    method clear_args (line 15) | def clear_args
    method secrets_io (line 19) | def secrets_io
    method merge (line 23) | def merge(other)
    method to_h (line 29) | def to_h
    method aliased_secrets (line 34) | def aliased_secrets
    method extract_alias (line 38) | def extract_alias(key)

FILE: lib/kamal/configuration/env/tag.rb
  class Kamal::Configuration::Env::Tag (line 1) | class Kamal::Configuration::Env::Tag
    method initialize (line 4) | def initialize(name, config:, secrets:)
    method env (line 10) | def env

FILE: lib/kamal/configuration/logging.rb
  class Kamal::Configuration::Logging (line 1) | class Kamal::Configuration::Logging
    method initialize (line 8) | def initialize(logging_config:, context: "logging")
    method driver (line 13) | def driver
    method options (line 17) | def options
    method merge (line 21) | def merge(other)
    method args (line 25) | def args

FILE: lib/kamal/configuration/proxy.rb
  class Kamal::Configuration::Proxy (line 1) | class Kamal::Configuration::Proxy
    method initialize (line 10) | def initialize(config:, proxy_config:, role_name: nil, secrets:, conte...
    method app_port (line 20) | def app_port
    method ssl? (line 24) | def ssl?
    method hosts (line 28) | def hosts
    method custom_ssl_certificate? (line 32) | def custom_ssl_certificate?
    method certificate_pem_content (line 38) | def certificate_pem_content
    method private_key_pem_content (line 44) | def private_key_pem_content
    method host_tls_cert (line 50) | def host_tls_cert
    method host_tls_key (line 54) | def host_tls_key
    method container_tls_cert (line 58) | def container_tls_cert
    method container_tls_key (line 62) | def container_tls_key
    method path_prefixes (line 66) | def path_prefixes
    method deploy_options (line 70) | def deploy_options
    method deploy_command_args (line 97) | def deploy_command_args(target:)
    method stop_options (line 101) | def stop_options(drain_timeout: nil, message: nil)
    method stop_command_args (line 108) | def stop_command_args(**options)
    method merge (line 112) | def merge(other)
    method tls_path (line 117) | def tls_path(directory, filename)
    method seconds_duration (line 121) | def seconds_duration(value)
    method error_pages (line 125) | def error_pages

FILE: lib/kamal/configuration/proxy/boot.rb
  class Kamal::Configuration::Proxy::Boot (line 1) | class Kamal::Configuration::Proxy::Boot
    method initialize (line 5) | def initialize(config:)
    method publish_args (line 9) | def publish_args(http_port, https_port, bind_ips = nil)
    method logging_args (line 21) | def logging_args(max_size)
    method default_boot_options (line 25) | def default_boot_options
    method repository_name (line 32) | def repository_name
    method image_name (line 36) | def image_name
    method image_default (line 40) | def image_default
    method container_name (line 44) | def container_name
    method host_directory (line 48) | def host_directory
    method options_file (line 52) | def options_file
    method image_file (line 56) | def image_file
    method image_version_file (line 60) | def image_version_file
    method run_command_file (line 64) | def run_command_file
    method apps_directory (line 68) | def apps_directory
    method apps_container_directory (line 72) | def apps_container_directory
    method apps_volume (line 76) | def apps_volume
    method app_directory (line 82) | def app_directory
    method app_container_directory (line 86) | def app_container_directory
    method error_pages_directory (line 90) | def error_pages_directory
    method error_pages_container_directory (line 94) | def error_pages_container_directory
    method tls_directory (line 98) | def tls_directory
    method tls_container_directory (line 102) | def tls_container_directory
    method ensure_valid_bind_ips (line 107) | def ensure_valid_bind_ips(bind_ips)
    method format_bind_ip (line 116) | def format_bind_ip(ip)

FILE: lib/kamal/configuration/proxy/run.rb
  class Kamal::Configuration::Proxy::Run (line 1) | class Kamal::Configuration::Proxy::Run
    method initialize (line 10) | def initialize(config, run_config:, context: "proxy/run")
    method debug? (line 16) | def debug?
    method publish? (line 20) | def publish?
    method http_port (line 24) | def http_port
    method https_port (line 28) | def https_port
    method bind_ips (line 32) | def bind_ips
    method publish_args (line 36) | def publish_args
    method log_max_size (line 48) | def log_max_size
    method logging_args (line 52) | def logging_args
    method version (line 56) | def version
    method registry (line 60) | def registry
    method repository (line 64) | def repository
    method image (line 68) | def image
    method container_name (line 72) | def container_name
    method options_args (line 76) | def options_args
    method run_command (line 82) | def run_command
    method metrics_port (line 86) | def metrics_port
    method run_command_options (line 90) | def run_command_options
    method docker_options_args (line 94) | def docker_options_args
    method host_directory (line 104) | def host_directory
    method apps_directory (line 108) | def apps_directory
    method apps_container_directory (line 112) | def apps_container_directory
    method apps_volume (line 116) | def apps_volume
    method apps_volume_args (line 122) | def apps_volume_args
    method app_directory (line 126) | def app_directory
    method app_container_directory (line 130) | def app_container_directory
    method format_bind_ip (line 135) | def format_bind_ip(ip)

FILE: lib/kamal/configuration/registry.rb
  class Kamal::Configuration::Registry (line 1) | class Kamal::Configuration::Registry
    method initialize (line 4) | def initialize(config:, secrets:, context: "registry")
    method server (line 10) | def server
    method username (line 14) | def username
    method password (line 18) | def password
    method local? (line 22) | def local?
    method local_port (line 26) | def local_port
    method lookup (line 33) | def lookup(key)

FILE: lib/kamal/configuration/role.rb
  class Kamal::Configuration::Role (line 1) | class Kamal::Configuration::Role
    method initialize (line 10) | def initialize(name, config:)
    method primary_host (line 30) | def primary_host
    method hosts (line 34) | def hosts
    method env_tags (line 38) | def env_tags(host)
    method cmd (line 42) | def cmd
    method option_args (line 46) | def option_args
    method labels (line 54) | def labels
    method label_args (line 58) | def label_args
    method logging_args (line 62) | def logging_args
    method logging (line 66) | def logging
    method proxy (line 70) | def proxy
    method running_proxy? (line 74) | def running_proxy?
    method ssl? (line 78) | def ssl?
    method stop_args (line 82) | def stop_args
    method env (line 89) | def env(host)
    method env_args (line 94) | def env_args(host)
    method env_directory (line 98) | def env_directory
    method secrets_io (line 102) | def secrets_io(host)
    method secrets_path (line 106) | def secrets_path
    method asset_volume_args (line 110) | def asset_volume_args
    method primary? (line 115) | def primary?
    method container_name (line 120) | def container_name(version = nil)
    method container_prefix (line 124) | def container_prefix
    method asset_path (line 129) | def asset_path
    method assets? (line 133) | def assets?
    method asset_volume (line 137) | def asset_volume(version = config.version)
    method asset_path_options (line 144) | def asset_path_options
    method asset_extracted_directory (line 148) | def asset_extracted_directory(version = config.version)
    method asset_volume_directory (line 152) | def asset_volume_directory(version = config.version)
    method ensure_one_host_for_ssl (line 156) | def ensure_one_host_for_ssl
    method initialize_specialized_proxy (line 163) | def initialize_specialized_proxy
    method tagged_hosts (line 186) | def tagged_hosts
    method extract_hosts_from_config (line 199) | def extract_hosts_from_config
    method default_labels (line 208) | def default_labels
    method specializations (line 212) | def specializations
    method role_config (line 216) | def role_config
    method custom_labels (line 220) | def custom_labels
    method asset_path_config (line 227) | def asset_path_config

FILE: lib/kamal/configuration/servers.rb
  class Kamal::Configuration::Servers (line 1) | class Kamal::Configuration::Servers
    method initialize (line 6) | def initialize(config:)
    method role_names (line 15) | def role_names

FILE: lib/kamal/configuration/ssh.rb
  class Kamal::Configuration::Ssh (line 1) | class Kamal::Configuration::Ssh
    method initialize (line 8) | def initialize(config:)
    method user (line 14) | def user
    method port (line 18) | def port
    method proxy (line 22) | def proxy
    method keys_only (line 30) | def keys_only
    method keys (line 34) | def keys
    method key_data (line 38) | def key_data
    method config (line 52) | def config
    method options (line 56) | def options
    method to_h (line 60) | def to_h
    method logger (line 65) | def logger
    method log_level (line 69) | def log_level

FILE: lib/kamal/configuration/sshkit.rb
  class Kamal::Configuration::Sshkit (line 1) | class Kamal::Configuration::Sshkit
    method initialize (line 6) | def initialize(config:)
    method max_concurrent_starts (line 11) | def max_concurrent_starts
    method pool_idle_timeout (line 15) | def pool_idle_timeout
    method dns_retries (line 19) | def dns_retries
    method to_h (line 23) | def to_h

FILE: lib/kamal/configuration/validation.rb
  type Kamal::Configuration::Validation (line 4) | module Kamal::Configuration::Validation
    function validation_doc (line 8) | def validation_doc
    function validation_config_key (line 12) | def validation_config_key
    function validate! (line 17) | def validate!(config, example: nil, context: nil, with: Kamal::Configu...
    function validation_yml (line 24) | def validation_yml

FILE: lib/kamal/configuration/validator.rb
  class Kamal::Configuration::Validator (line 1) | class Kamal::Configuration::Validator
    method initialize (line 4) | def initialize(config, example:, context:)
    method validate! (line 10) | def validate!
    method validate_against_example! (line 15) | def validate_against_example!(validation_config, example)
    method valid_type? (line 64) | def valid_type?(value, type)
    method type_description (line 70) | def type_description(type)
    method boolean? (line 80) | def boolean?(type)
    method stringish? (line 84) | def stringish?(value)
    method validate_array_of_or_type! (line 88) | def validate_array_of_or_type!(value, type)
    method validate_array_of! (line 98) | def validate_array_of!(array, type)
    method validate_hash_of! (line 108) | def validate_hash_of!(hash, type)
    method validate_servers! (line 118) | def validate_servers!(servers)
    method validate_ssh_config! (line 138) | def validate_ssh_config!(config)
    method validate_paths! (line 148) | def validate_paths!(paths)
    method validate_hooks_output! (line 166) | def validate_hooks_output!(value)
    method validate_type! (line 179) | def validate_type!(value, *types)
    method error (line 183) | def error(message)
    method type_error (line 187) | def type_error(*expected_types)
    method unknown_keys_error (line 192) | def unknown_keys_error(unknown_keys)
    method error_context (line 196) | def error_context
    method with_context (line 200) | def with_context(context)
    method allow_extensions? (line 208) | def allow_extensions?
    method extension? (line 212) | def extension?(key)
    method check_unknown_keys! (line 216) | def check_unknown_keys!(config, example)
    method validate_labels! (line 222) | def validate_labels!(labels)
    method validate_docker_options! (line 234) | def validate_docker_options!(options)

FILE: lib/kamal/configuration/validator/accessory.rb
  class Kamal::Configuration::Validator::Accessory (line 1) | class Kamal::Configuration::Validator::Accessory < Kamal::Configuration:...
    method validate! (line 2) | def validate!

FILE: lib/kamal/configuration/validator/alias.rb
  class Kamal::Configuration::Validator::Alias (line 1) | class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Val...
    method validate! (line 2) | def validate!

FILE: lib/kamal/configuration/validator/builder.rb
  class Kamal::Configuration::Validator::Builder (line 1) | class Kamal::Configuration::Validator::Builder < Kamal::Configuration::V...
    method validate! (line 2) | def validate!

FILE: lib/kamal/configuration/validator/configuration.rb
  class Kamal::Configuration::Validator::Configuration (line 1) | class Kamal::Configuration::Validator::Configuration < Kamal::Configurat...
    method allow_extensions? (line 3) | def allow_extensions?

FILE: lib/kamal/configuration/validator/env.rb
  class Kamal::Configuration::Validator::Env (line 1) | class Kamal::Configuration::Validator::Env < Kamal::Configuration::Valid...
    method validate! (line 4) | def validate!
    method validate_simple_env! (line 13) | def validate_simple_env!
    method validate_complex_env! (line 17) | def validate_complex_env!
    method known_keys (line 25) | def known_keys
    method unknown_keys (line 29) | def unknown_keys
    method validate_tags! (line 33) | def validate_tags!

FILE: lib/kamal/configuration/validator/proxy.rb
  class Kamal::Configuration::Validator::Proxy (line 1) | class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Val...
    method validate! (line 2) | def validate!
    method ensure_valid_bind_ips (line 39) | def ensure_valid_bind_ips(bind_ips)

FILE: lib/kamal/configuration/validator/registry.rb
  class Kamal::Configuration::Validator::Registry (line 1) | class Kamal::Configuration::Validator::Registry < Kamal::Configuration::...
    method validate! (line 4) | def validate!
    method validate_string_or_one_item_array! (line 14) | def validate_string_or_one_item_array!(key)

FILE: lib/kamal/configuration/validator/role.rb
  class Kamal::Configuration::Validator::Role (line 1) | class Kamal::Configuration::Validator::Role < Kamal::Configuration::Vali...
    method validate! (line 2) | def validate!

FILE: lib/kamal/configuration/validator/servers.rb
  class Kamal::Configuration::Validator::Servers (line 1) | class Kamal::Configuration::Validator::Servers < Kamal::Configuration::V...
    method validate! (line 2) | def validate!

FILE: lib/kamal/configuration/volume.rb
  class Kamal::Configuration::Volume (line 1) | class Kamal::Configuration::Volume
    method initialize (line 5) | def initialize(host_path:, container_path:, options: nil)
    method docker_args (line 11) | def docker_args
    method docker_args_string (line 15) | def docker_args_string
    method host_path_for_docker_volume (line 22) | def host_path_for_docker_volume

FILE: lib/kamal/docker.rb
  type Kamal::Docker (line 4) | module Kamal::Docker
    function included_files (line 8) | def included_files

FILE: lib/kamal/env_file.rb
  class Kamal::EnvFile (line 2) | class Kamal::EnvFile
    method initialize (line 3) | def initialize(env)
    method to_s (line 7) | def to_s
    method to_io (line 18) | def to_io
    method docker_env_file_line (line 25) | def docker_env_file_line(key, value)
    method escape_docker_env_file_value (line 30) | def escape_docker_env_file_value(value)
    method escape_docker_env_file_ascii_value (line 37) | def escape_docker_env_file_ascii_value(value)

FILE: lib/kamal/git.rb
  type Kamal::Git (line 1) | module Kamal::Git
    function used? (line 4) | def used?
    function user_name (line 8) | def user_name
    function email (line 12) | def email
    function revision (line 16) | def revision
    function uncommitted_changes (line 20) | def uncommitted_changes
    function root (line 24) | def root
    function uncommitted_files (line 29) | def uncommitted_files
    function untracked_files (line 34) | def untracked_files

FILE: lib/kamal/secrets.rb
  class Kamal::Secrets (line 3) | class Kamal::Secrets
    method initialize (line 6) | def initialize(destination: nil, secrets_path:)
    method [] (line 12) | def [](key)
    method to_h (line 22) | def to_h
    method secrets_files (line 26) | def secrets_files
    method key? (line 30) | def key?(key)
    method secrets (line 37) | def secrets
    method secrets_filenames (line 43) | def secrets_filenames
    method synchronized_fetch (line 47) | def synchronized_fetch(key)

FILE: lib/kamal/secrets/adapters.rb
  type Kamal::Secrets::Adapters (line 2) | module Kamal::Secrets::Adapters
    function lookup (line 3) | def self.lookup(name)
    function adapter_class (line 11) | def self.adapter_class(name)

FILE: lib/kamal/secrets/adapters/aws_secrets_manager.rb
  class Kamal::Secrets::Adapters::AwsSecretsManager (line 1) | class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adap...
    method requires_account? (line 2) | def requires_account?
    method login (line 7) | def login(_account)
    method fetch_secrets (line 11) | def fetch_secrets(secrets, from:, account: nil, session:)
    method get_from_secrets_manager (line 26) | def get_from_secrets_manager(secrets, account: nil)
    method check_dependencies! (line 43) | def check_dependencies!
    method cli_installed? (line 47) | def cli_installed?

FILE: lib/kamal/secrets/adapters/base.rb
  class Kamal::Secrets::Adapters::Base (line 1) | class Kamal::Secrets::Adapters::Base
    method fetch (line 4) | def fetch(secrets, account: nil, from: nil)
    method requires_account? (line 13) | def requires_account?
    method login (line 18) | def login(...)
    method fetch_secrets (line 22) | def fetch_secrets(...)
    method check_dependencies! (line 26) | def check_dependencies!
    method prefixed_secrets (line 30) | def prefixed_secrets(secrets, from:)

FILE: lib/kamal/secrets/adapters/bitwarden.rb
  class Kamal::Secrets::Adapters::Bitwarden (line 1) | class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
    method login (line 3) | def login(account)
    method fetch_secrets (line 24) | def fetch_secrets(secrets, from:, account:, session:)
    method fetch_secrets_from_fields (line 44) | def fetch_secrets_from_fields(fields, item, item_json)
    method items_fields (line 53) | def items_fields(secrets)
    method signedin? (line 63) | def signedin?(account)
    method run_command (line 67) | def run_command(command, session: nil, raw: false)
    method check_dependencies! (line 73) | def check_dependencies!
    method cli_installed? (line 77) | def cli_installed?

FILE: lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
  class Kamal::Secrets::Adapters::BitwardenSecretsManager (line 1) | class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets...
    method requires_account? (line 2) | def requires_account?
    method fetch_secrets (line 12) | def fetch_secrets(secrets, from:, account:, session:)
    method extract_command_and_project (line 37) | def extract_command_and_project(secrets)
    method run_command (line 48) | def run_command(command, session: nil)
    method login (line 53) | def login(account)
    method check_dependencies! (line 58) | def check_dependencies!
    method cli_installed? (line 62) | def cli_installed?

FILE: lib/kamal/secrets/adapters/doppler.rb
  class Kamal::Secrets::Adapters::Doppler (line 1) | class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
    method requires_account? (line 2) | def requires_account?
    method login (line 7) | def login(*)
    method loggedin? (line 14) | def loggedin?
    method fetch_secrets (line 19) | def fetch_secrets(secrets, from:, **)
    method secrets_get_flags (line 33) | def secrets_get_flags(secrets)
    method service_token_set? (line 45) | def service_token_set?
    method check_dependencies! (line 49) | def check_dependencies!
    method cli_installed? (line 53) | def cli_installed?

FILE: lib/kamal/secrets/adapters/enpass.rb
  class Kamal::Secrets::Adapters::Enpass (line 11) | class Kamal::Secrets::Adapters::Enpass < Kamal::Secrets::Adapters::Base
    method requires_account? (line 12) | def requires_account?
    method fetch_secrets (line 17) | def fetch_secrets(secrets, from:, account:, session:)
    method check_dependencies! (line 25) | def check_dependencies!
    method cli_installed? (line 29) | def cli_installed?
    method login (line 34) | def login(account)
    method fetch_secret_titles (line 38) | def fetch_secret_titles(secrets)
    method parse_result_and_take_secrets (line 51) | def parse_result_and_take_secrets(unparsed_result, secrets)

FILE: lib/kamal/secrets/adapters/gcp_secret_manager.rb
  class Kamal::Secrets::Adapters::GcpSecretManager (line 1) | class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapt...
    method login (line 3) | def login(account)
    method fetch_secrets (line 29) | def fetch_secrets(secrets, from:, account:, session:)
    method fetch_secret (line 41) | def fetch_secret(project, secret_name, secret_version, user, service_a...
    method secrets_with_metadata (line 66) | def secrets_with_metadata(secrets)
    method run_command (line 80) | def run_command(command, project: "default", user: "default", service_...
    method check_dependencies! (line 92) | def check_dependencies!
    method cli_installed? (line 96) | def cli_installed?
    method logged_in? (line 101) | def logged_in?
    method parse_account (line 105) | def parse_account(account)
    method is_user? (line 109) | def is_user?(candidate)

FILE: lib/kamal/secrets/adapters/last_pass.rb
  class Kamal::Secrets::Adapters::LastPass (line 1) | class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
    method login (line 3) | def login(account)
    method loggedin? (line 10) | def loggedin?(account)
    method fetch_secrets (line 14) | def fetch_secrets(secrets, from:, account:, session:)
    method check_dependencies! (line 32) | def check_dependencies!
    method cli_installed? (line 36) | def cli_installed?

FILE: lib/kamal/secrets/adapters/one_password.rb
  class Kamal::Secrets::Adapters::OnePassword (line 1) | class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::...
    method login (line 5) | def login(account)
    method loggedin? (line 13) | def loggedin?(account)
    method fetch_secrets (line 18) | def fetch_secrets(secrets, from:, account:, session:)
    method fetch_specified_secrets (line 26) | def fetch_specified_secrets(secrets, from:, account:, session:)
    method fetch_all_secrets (line 39) | def fetch_all_secrets(from:, account:, session:)
    method to_options (line 51) | def to_options(**options)
    method vaults_items_fields (line 55) | def vaults_items_fields(secrets)
    method vault_items (line 69) | def vault_items(from)
    method fields_map (line 75) | def fields_map(fields_json)
    method op_item_get (line 83) | def op_item_get(vault, item, fields: nil, account:, session:)
    method check_dependencies! (line 96) | def check_dependencies!
    method cli_installed? (line 100) | def cli_installed?

FILE: lib/kamal/secrets/adapters/passbolt.rb
  class Kamal::Secrets::Adapters::Passbolt (line 1) | class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
    method requires_account? (line 2) | def requires_account?
    method login (line 8) | def login(*)
    method fetch_secrets (line 13) | def fetch_secrets(secrets, from:, **)
    method secrets_get_folders (line 60) | def secrets_get_folders(secrets)
    method get_folder_path (line 111) | def get_folder_path(folder, all_folders, path = [])
    method check_dependencies! (line 121) | def check_dependencies!
    method cli_installed? (line 125) | def cli_installed?

FILE: lib/kamal/secrets/adapters/test.rb
  class Kamal::Secrets::Adapters::Test (line 1) | class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
    method login (line 3) | def login(account)
    method fetch_secrets (line 7) | def fetch_secrets(secrets, from:, account:, session:)
    method check_dependencies! (line 13) | def check_dependencies!

FILE: lib/kamal/secrets/dotenv/inline_command_substitution.rb
  class Kamal::Secrets::Dotenv::InlineCommandSubstitution (line 1) | class Kamal::Secrets::Dotenv::InlineCommandSubstitution
    method install! (line 17) | def install!
    method call (line 21) | def call(value, env, overwrite: false)
    method inline_secrets_command (line 43) | def inline_secrets_command(command)

FILE: lib/kamal/sshkit_with_ext.rb
  class SSHKit::Backend::Abstract (line 9) | class SSHKit::Backend::Abstract
    method capture_with_info (line 10) | def capture_with_info(*args, **kwargs)
    method capture_with_debug (line 14) | def capture_with_debug(*args, **kwargs)
    method capture_with_pretty_json (line 18) | def capture_with_pretty_json(*args, **kwargs)
    method puts_by_host (line 22) | def puts_by_host(host, output, type: "App", quiet: false)
    type CommandEnvMerge (line 35) | module CommandEnvMerge
      function command (line 40) | def command(args, options)
      function build_command (line 48) | def build_command(args, env: nil, **options)
      function default_command_options (line 53) | def default_command_options
      function env_for (line 57) | def env_for(env)
  class SSHKit::Backend::Netssh::Configuration (line 64) | class SSHKit::Backend::Netssh::Configuration
  class SSHKit::Backend::Netssh (line 68) | class SSHKit::Backend::Netssh
    type DnsRetriable (line 69) | module DnsRetriable
      function with_dns_retry (line 75) | def with_dns_retry(hostname, retries: config.dns_retries, base: DNS_...
      function retryable_dns_error? (line 91) | def retryable_dns_error?(error)
      function dns_retry_sleep (line 102) | def dns_retry_sleep(attempt, base:, jitter:, max_sleep:)
    type LimitConcurrentStartsClass (line 109) | module LimitConcurrentStartsClass
      function configure (line 112) | def configure(&block)
    type ConnectSsh (line 126) | module ConnectSsh
      function connect_ssh (line 128) | def connect_ssh(...)
    type DnsRetriableConnection (line 134) | module DnsRetriableConnection
      function connect_ssh (line 136) | def connect_ssh(...)
    type LimitConcurrentStartsInstance (line 142) | module LimitConcurrentStartsInstance
      function with_ssh (line 144) | def with_ssh(&block)
      function connect_ssh (line 155) | def connect_ssh(...)
      function with_concurrency_limit (line 159) | def with_concurrency_limit(&block)
  class SSHKit::Runner::Parallel (line 170) | class SSHKit::Runner::Parallel
    type CompleteAll (line 176) | module CompleteAll
      function execute (line 177) | def execute
  type NetSshForwardingNoPuts (line 207) | module NetSshForwardingNoPuts
    function puts (line 208) | def puts(*)
  type SSHKitDslRoles (line 214) | module SSHKitDslRoles
    function on_roles (line 230) | def on_roles(roles, hosts:, parallel: true, &block)

FILE: lib/kamal/tags.rb
  class Kamal::Tags (line 3) | class Kamal::Tags
    method from_config (line 7) | def from_config(config, **extra)
    method default_tags (line 11) | def default_tags(config)
    method service_version (line 20) | def service_version(config)
    method initialize (line 25) | def initialize(**tags)
    method env (line 29) | def env
    method to_s (line 33) | def to_s
    method except (line 37) | def except(*tags)

FILE: lib/kamal/utils.rb
  type Kamal::Utils (line 3) | module Kamal::Utils
    function argumentize (line 9) | def argumentize(argument, attributes, sensitive: false)
    function optionize (line 24) | def optionize(args, with: nil, escape: true)
    function flatten_args (line 35) | def flatten_args(args)
    function sensitive (line 42) | def sensitive(...)
    function redacted (line 46) | def redacted(value)
    function escape_shell_value (line 60) | def escape_shell_value(value)
    function escape_ascii_shell_value (line 66) | def escape_ascii_shell_value(value)
    function filter_specific_items (line 73) | def filter_specific_items(filters, items)
    function stable_sort! (line 87) | def stable_sort!(elements, &block)
    function join_commands (line 91) | def join_commands(commands)
    function docker_arch (line 95) | def docker_arch
    function older_version? (line 107) | def older_version?(version, other_version)

FILE: lib/kamal/utils/sensitive.rb
  class Kamal::Utils::Sensitive (line 4) | class Kamal::Utils::Sensitive
    method initialize (line 12) | def initialize(value, redaction: "[REDACTED]")
    method encode_with (line 17) | def encode_with(coder)

FILE: lib/kamal/version.rb
  type Kamal (line 1) | module Kamal

FILE: test/cli/accessory_test.rb
  class CliAccessoryTest (line 3) | class CliAccessoryTest < CliTestCase
    method run_command (line 277) | def run_command(*command)

FILE: test/cli/app_test.rb
  class CliAppTest (line 3) | class CliAppTest < CliTestCase
    method run_command (line 583) | def run_command(*command, config: :with_accessories, host: "1.1.1.1", ...
    method stub_running (line 591) | def stub_running

FILE: test/cli/build_test.rb
  class CliBuildTest (line 3) | class CliBuildTest < CliTestCase
    method run_command (line 454) | def run_command(*command, fixture: :with_accessories)
    method stub_dependency_checks (line 458) | def stub_dependency_checks

FILE: test/cli/cli_test_case.rb
  class CliTestCase (line 3) | class CliTestCase < ActiveSupport::TestCase
    method fail_hook (line 19) | def fail_hook(hook)
    method stub_setup (line 30) | def stub_setup
    method assert_hook_ran (line 43) | def assert_hook_ran(hook, output, count: 1)
    method with_argv (line 48) | def with_argv(*argv)
    method with_build_directory (line 56) | def with_build_directory
    method pwd_sha (line 65) | def pwd_sha

FILE: test/cli/lock_test.rb
  class CliLockTest (line 3) | class CliLockTest < CliTestCase
    method run_command (line 17) | def run_command(*command)

FILE: test/cli/main_test.rb
  class CliMainTest (line 3) | class CliMainTest < CliTestCase
    method run_command (line 728) | def run_command(*command, config_file: "deploy_simple")
    method run_command_with_config_path (line 734) | def run_command_with_config_path(*command, config_path:, destination: ...
    method in_dummy_git_repo (line 744) | def in_dummy_git_repo
    method with_config_files (line 753) | def with_config_files
    method assert_file (line 767) | def assert_file(file, content)
    method with_kamal_lock_env (line 771) | def with_kamal_lock_env

FILE: test/cli/proxy_test.rb
  class CliProxyTest (line 3) | class CliProxyTest < CliTestCase
    method run_command (line 408) | def run_command(*command, fixture: :with_proxy)

FILE: test/cli/prune_test.rb
  class CliPruneTest (line 3) | class CliPruneTest < CliTestCase
    method run_command (line 33) | def run_command(*command)

FILE: test/cli/registry_test.rb
  class CliRegistryTest (line 3) | class CliRegistryTest < CliTestCase
    method run_command (line 136) | def run_command(*command, fixture: :with_accessories)

FILE: test/cli/secrets_test.rb
  class CliSecretsTest (line 3) | class CliSecretsTest < CliTestCase
    method run_command (line 31) | def run_command(*command)

FILE: test/cli/server_test.rb
  class CliServerTest (line 3) | class CliServerTest < CliTestCase
    method run_command (line 92) | def run_command(*command)

FILE: test/commander_test.rb
  class CommanderTest (line 3) | class CommanderTest < ActiveSupport::TestCase
    method configure_with (line 196) | def configure_with(variant)

FILE: test/commands/accessory_test.rb
  class CommandsAccessoryTest (line 3) | class CommandsAccessoryTest < ActiveSupport::TestCase
    method new_command (line 202) | def new_command(accessory)

FILE: test/commands/app_test.rb
  class CommandsAppTest (line 3) | class CommandsAppTest < ActiveSupport::TestCase
    method new_command (line 575) | def new_command(role: "web", host: "1.1.1.1", **additional_config)

FILE: test/commands/auditor_test.rb
  class CommandsAuditorTest (line 4) | class CommandsAuditorTest < ActiveSupport::TestCase
    method new_command (line 61) | def new_command(destination: nil, **details)

FILE: test/commands/builder_test.rb
  class CommandsBuilderTest (line 3) | class CommandsBuilderTest < ActiveSupport::TestCase
    method new_builder_command (line 275) | def new_builder_command(additional_config = {})
    method local_arch (line 283) | def local_arch
    method remote_arch (line 287) | def remote_arch

FILE: test/commands/docker_test.rb
  class CommandsDockerTest (line 3) | class CommandsDockerTest < ActiveSupport::TestCase

FILE: test/commands/hook_test.rb
  class CommandsHookTest (line 3) | class CommandsHookTest < ActiveSupport::TestCase
    method new_command (line 51) | def new_command(**extra_config)

FILE: test/commands/lock_test.rb
  class CommandsLockTest (line 3) | class CommandsLockTest < ActiveSupport::TestCase
    method new_command (line 30) | def new_command

FILE: test/commands/proxy_test.rb
  class CommandsProxyTest (line 3) | class CommandsProxyTest < ActiveSupport::TestCase
    method new_command (line 225) | def new_command

FILE: test/commands/prune_test.rb
  class CommandsPruneTest (line 3) | class CommandsPruneTest < ActiveSupport::TestCase
    method new_command (line 34) | def new_command

FILE: test/commands/registry_test.rb
  class CommandsRegistryTest (line 3) | class CommandsRegistryTest < ActiveSupport::TestCase
    method registry (line 98) | def registry
    method main_config (line 102) | def main_config
    method accessory_registry_config (line 106) | def accessory_registry_config

FILE: test/commands/server_test.rb
  class CommandsServerTest (line 3) | class CommandsServerTest < ActiveSupport::TestCase
    method new_command (line 16) | def new_command(extra_config = {})

FILE: test/configuration/accessory_test.rb
  class ConfigurationAccessoryTest (line 3) | class ConfigurationAccessoryTest < ActiveSupport::TestCase

FILE: test/configuration/boot_test.rb
  class ConfigurationBootTest (line 3) | class ConfigurationBootTest < ActiveSupport::TestCase
    method config_with_boot (line 40) | def config_with_boot(boot)

FILE: test/configuration/builder_test.rb
  class ConfigurationBuilderTest (line 3) | class ConfigurationBuilderTest < ActiveSupport::TestCase
    method config (line 192) | def config

FILE: test/configuration/env/tags_test.rb
  class ConfigurationEnvTagsTest (line 3) | class ConfigurationEnvTagsTest < ActiveSupport::TestCase

FILE: test/configuration/env_test.rb
  class ConfigurationEnvTest (line 3) | class ConfigurationEnvTest < ActiveSupport::TestCase
    method assert_config (line 71) | def assert_config(config:, clear: {}, secrets: {})

FILE: test/configuration/proxy/boot_test.rb
  class ConfigurationProxyBootTest (line 3) | class ConfigurationProxyBootTest < ActiveSupport::TestCase

FILE: test/configuration/proxy_test.rb
  class ConfigurationProxyTest (line 3) | class ConfigurationProxyTest < ActiveSupport::TestCase
    method config (line 109) | def config

FILE: test/configuration/role_test.rb
  class ConfigurationRoleTest (line 3) | class ConfigurationRoleTest < ActiveSupport::TestCase
    method config (line 298) | def config
    method config_with_roles (line 302) | def config_with_roles

FILE: test/configuration/ssh_test.rb
  class ConfigurationSshTest (line 3) | class ConfigurationSshTest < ActiveSupport::TestCase

FILE: test/configuration/sshkit_test.rb
  class ConfigurationSshkitTest (line 3) | class ConfigurationSshkitTest < ActiveSupport::TestCase

FILE: test/configuration/validation_test.rb
  class ConfigurationValidationTest (line 2) | class ConfigurationValidationTest < ActiveSupport::TestCase
    method assert_error (line 124) | def assert_error(message, **invalid_config)

FILE: test/configuration/volume_test.rb
  class ConfigurationVolumeTest (line 3) | class ConfigurationVolumeTest < ActiveSupport::TestCase

FILE: test/configuration_test.rb
  class ConfigurationTest (line 3) | class ConfigurationTest < ActiveSupport::TestCase

FILE: test/env_file_test.rb
  class EnvFileTest (line 3) | class EnvFileTest < ActiveSupport::TestCase

FILE: test/git_test.rb
  class GitTest (line 3) | class GitTest < ActiveSupport::TestCase

FILE: test/integration/accessory_test.rb
  class AccessoryTest (line 3) | class AccessoryTest < IntegrationTest
    method assert_accessory_running (line 57) | def assert_accessory_running(name)
    method assert_accessory_not_running (line 61) | def assert_accessory_not_running(name)
    method assert_accessory_volume_mount_options (line 65) | def assert_accessory_volume_mount_options(name)
    method assert_accessory_file_mode_and_owner (line 70) | def assert_accessory_file_mode_and_owner(name)
    method assert_accessory_directory_mode_and_owner (line 75) | def assert_accessory_directory_mode_and_owner(name)
    method accessory_details (line 80) | def accessory_details(name)
    method assert_netcat_is_up (line 84) | def assert_netcat_is_up
    method assert_netcat_not_found (line 90) | def assert_netcat_not_found
    method netcat_response (line 96) | def netcat_response

FILE: test/integration/app_test.rb
  class AppTest (line 3) | class AppTest < IntegrationTest

FILE: test/integration/broken_deploy_test.rb
  class BrokenDeployTest (line 3) | class BrokenDeployTest < IntegrationTest
    method assert_failed_deploy (line 25) | def assert_failed_deploy(output)

FILE: test/integration/integration_test.rb
  class IntegrationTest (line 4) | class IntegrationTest < ActiveSupport::TestCase
    method docker_compose (line 25) | def docker_compose(*commands, capture: false, raise_on_error: true)
    method deployer_exec (line 38) | def deployer_exec(*commands, workdir: nil, **options)
    method kamal (line 43) | def kamal(*commands, **options)
    method assert_app_is_down (line 47) | def assert_app_is_down
    method assert_app_in_maintenance (line 51) | def assert_app_in_maintenance(message: nil)
    method assert_app_not_found (line 55) | def assert_app_not_found
    method assert_app_error_code (line 59) | def assert_app_error_code(code, message: nil)
    method assert_app_is_up (line 66) | def assert_app_is_up(version: nil, app: @app, cert: nil)
    method wait_for_app_to_be_up (line 73) | def wait_for_app_to_be_up(timeout: 20, up_count: 3)
    method app_response (line 85) | def app_response(app: @app, cert: nil)
    method update_app_rev (line 95) | def update_app_rev
    method break_app (line 100) | def break_app
    method latest_app_version (line 105) | def latest_app_version
    method assert_app_version (line 109) | def assert_app_version(version, response)
    method assert_hooks_ran (line 113) | def assert_hooks_ran(*hooks)
    method assert_200 (line 120) | def assert_200(response)
    method wait_for_healthy (line 132) | def wait_for_healthy(timeout: 30)
    method setup_deployer (line 147) | def setup_deployer
    method debug_response_code (line 152) | def debug_response_code(app_response, expected_code)
    method assert_container_running (line 163) | def assert_container_running(host:, name:)
    method assert_container_not_running (line 167) | def assert_container_not_running(host:, name:)
    method container_running? (line 171) | def container_running?(host:, name:)
    method assert_app_directory_removed (line 175) | def assert_app_directory_removed
    method assert_directory_removed (line 179) | def assert_directory_removed(directory)
    method assert_proxy_running (line 183) | def assert_proxy_running
    method assert_proxy_not_running (line 187) | def assert_proxy_not_running
    method app_host (line 191) | def app_host(app = @app)
    method https_response_with_cert (line 200) | def https_response_with_cert(uri, cert)

FILE: test/integration/lock_test.rb
  class LockTest (line 3) | class LockTest < IntegrationTest

FILE: test/integration/main_test.rb
  class MainTest (line 3) | class MainTest < IntegrationTest
    method assert_envs (line 177) | def assert_envs(version:)
    method assert_env (line 192) | def assert_env(key, value, vm:, version:)
    method assert_no_env (line 196) | def assert_no_env(key, vm:, version:)
    method assert_accumulated_assets (line 202) | def assert_accumulated_assets(*versions)
    method assert_asset_volume_read_only (line 210) | def assert_asset_volume_read_only(version)
    method image_ids (line 215) | def image_ids(vm:)
    method container_ids (line 219) | def container_ids(vm:)
    method assert_no_images_or_containers (line 223) | def assert_no_images_or_containers
    method assert_images_and_containers (line 230) | def assert_images_and_containers
    method assert_hook_env_variables (line 237) | def assert_hook_env_variables(output, version:)
    method assert_hook_output (line 248) | def assert_hook_output(output)

FILE: test/integration/proxy_test.rb
  class ProxyTest (line 3) | class ProxyTest < IntegrationTest
    method assert_docker_options_in_file (line 54) | def assert_docker_options_in_file
    method assert_docker_options_in_container (line 59) | def assert_docker_options_in_container

FILE: test/secrets/aws_secrets_manager_adapter_test.rb
  class AwsSecretsManagerAdapterTest (line 3) | class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
    method run_command (line 191) | def run_command(*command, account: "default")

FILE: test/secrets/bitwarden_adapter_test.rb
  class BitwardenAdapterTest (line 3) | class BitwardenAdapterTest < SecretAdapterTestCase
    method run_command (line 194) | def run_command(*command)
    method stub_unlocked (line 204) | def stub_unlocked
    method stub_mypassword (line 212) | def stub_mypassword(session: nil)
    method stub_noteitem (line 235) | def stub_noteitem(session: nil)
    method stub_noteitem_with_fields (line 259) | def stub_noteitem_with_fields(session: nil)
    method stub_myitem (line 289) | def stub_myitem

FILE: test/secrets/bitwarden_secrets_manager_adapter_test.rb
  class BitwardenSecretsManagerAdapterTest (line 3) | class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
    method stub_login (line 182) | def stub_login
    method run_command (line 186) | def run_command(*command)

FILE: test/secrets/doppler_adapter_test.rb
  class DopplerAdapterTest (line 3) | class DopplerAdapterTest < SecretAdapterTestCase
    method run_command (line 166) | def run_command(*command)
    method single_item_json (line 175) | def single_item_json

FILE: test/secrets/dotenv_inline_command_substitution_test.rb
  class SecretsInlineCommandSubstitution (line 3) | class SecretsInlineCommandSubstitution < SecretAdapterTestCase

FILE: test/secrets/enpass_adapter_test.rb
  class EnpassAdapterTest (line 3) | class EnpassAdapterTest < SecretAdapterTestCase
    method run_command (line 72) | def run_command(*command)

FILE: test/secrets/gcp_secret_manager_adapter_test.rb
  class GcpSecretManagerAdapterTest (line 3) | class GcpSecretManagerAdapterTest < SecretAdapterTestCase
    method run_command (line 141) | def run_command(*command, account: "default")
    method stub_gcloud_version (line 151) | def stub_gcloud_version(succeed: true)
    method stub_authenticated (line 155) | def stub_authenticated
    method stub_unauthenticated (line 168) | def stub_unauthenticated
    method stub_mypassword (line 183) | def stub_mypassword
    method stub_items (line 197) | def stub_items(n, project: nil, account: nil, version: "latest", imper...

FILE: test/secrets/last_pass_adapter_test.rb
  class LastPassAdapterTest (line 3) | class LastPassAdapterTest < SecretAdapterTestCase
    method run_command (line 137) | def run_command(*command)
    method single_item_json (line 147) | def single_item_json

FILE: test/secrets/one_password_adapter_test.rb
  class SecretsOnePasswordAdapterTest (line 3) | class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
    method run_command (line 224) | def run_command(*command)
    method single_item_json (line 234) | def single_item_json

FILE: test/secrets/passbolt_adapter_test.rb
  class PassboltAdapterTest (line 3) | class PassboltAdapterTest < SecretAdapterTestCase
    method run_command (line 481) | def run_command(*command)

FILE: test/secrets_test.rb
  class SecretsTest (line 3) | class SecretsTest < ActiveSupport::TestCase

FILE: test/sshkit_dns_retry_test.rb
  class SshkitDnsRetryTest (line 3) | class SshkitDnsRetryTest < ActiveSupport::TestCase

FILE: test/test_helper.rb
  class SSHKit::Backend::Printer (line 22) | class SSHKit::Backend::Printer
    method upload! (line 23) | def upload!(local, location, **kwargs)
  type SSHKit (line 31) | module SSHKit
    type DSL (line 32) | module DSL
      function run_locally (line 33) | def run_locally(&block)
  class ActiveSupport::TestCase (line 39) | class ActiveSupport::TestCase
    method stdouted (line 44) | def stdouted
    method stderred (line 48) | def stderred
    method stub_stdin_tty (line 52) | def stub_stdin_tty
    method stub_stdin_file (line 58) | def stub_stdin_file
    method stub_stdin (line 64) | def stub_stdin(io)
    method with_test_secrets (line 73) | def with_test_secrets(**files)
    method setup_test_secrets (line 80) | def setup_test_secrets(**files)
    method teardown_test_secrets (line 94) | def teardown_test_secrets
    method with_error_pages (line 99) | def with_error_pages(directory:)
    method copy_fixtures (line 117) | def copy_fixtures(to_dir)
  class SecretAdapterTestCase (line 124) | class SecretAdapterTestCase < ActiveSupport::TestCase
    method stub_ticks (line 130) | def stub_ticks
    method stub_ticks_with (line 134) | def stub_ticks_with(command, succeed: true)

FILE: test/utils_test.rb
  class UtilsTest (line 3) | class UtilsTest < ActiveSupport::TestCase
Condensed preview — 313 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (824K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2308,
    "preview": "name: CI\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njob"
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "chars": 1602,
    "preview": "name: Docker\n\non:\n  workflow_dispatch:\n    inputs:\n      tagInput:\n        description: 'Tag'\n        required: true\n   "
  },
  {
    "path": ".gitignore",
    "chars": 59,
    "preview": ".byebug_history\n*.gem\ncoverage/*\n.DS_Store\ngemfiles/*.lock\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 50,
    "preview": "inherit_gem:\n  rubocop-rails-omakase: rubocop.yml\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 2852,
    "preview": "# Contributor Code of Conduct\n\nAs contributors and maintainers of the Kamal project, we pledge to create a welcoming and"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2844,
    "preview": "# Contributing to Kamal development\n\nThank you for considering contributing to Kamal! This document outlines some guidel"
  },
  {
    "path": "Dockerfile",
    "chars": 1208,
    "preview": "FROM ruby:3.4-alpine\n\n# Install docker/buildx-bin\nCOPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/"
  },
  {
    "path": "Gemfile",
    "chars": 172,
    "preview": "source \"https://rubygems.org\"\ngit_source(:github) { |repo| \"https://github.com/#{repo}.git\" }\n\ngemspec\n\ngroup :rubocop d"
  },
  {
    "path": "MIT-LICENSE",
    "chars": 1068,
    "preview": "Copyright (c) 2023 David Heinemeier Hansson\n\nPermission is hereby granted, free of charge, to any person obtaining\na cop"
  },
  {
    "path": "README.md",
    "chars": 925,
    "preview": "# Kamal: Deploy web apps anywhere\n\nFrom bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses"
  },
  {
    "path": "bin/docs",
    "chars": 3468,
    "preview": "#!/usr/bin/env ruby\nrequire \"stringio\"\n\ndef usage\n  puts \"Usage: #{$0} <kamal_site_repo>\"\n  exit 1\nend\n\nusage if ARGV.si"
  },
  {
    "path": "bin/kamal",
    "chars": 428,
    "preview": "#!/usr/bin/env ruby\n\n# Prevent failures from being reported twice.\nThread.report_on_exception = false\n\nrequire \"kamal\"\n\n"
  },
  {
    "path": "bin/release",
    "chars": 353,
    "preview": "#!/usr/bin/env bash\n\nVERSION=$1\n\nprintf \"module Kamal\\n  VERSION = \\\"$VERSION\\\"\\nend\\n\" > ./lib/kamal/version.rb\nbundle\n"
  },
  {
    "path": "bin/test",
    "chars": 116,
    "preview": "#!/usr/bin/env ruby\n$: << File.expand_path(\"../test\", __dir__)\n\nrequire \"bundler/setup\"\nrequire \"rails/plugin/test\"\n"
  },
  {
    "path": "gemfiles/rails_edge.gemfile",
    "chars": 203,
    "preview": "source 'https://rubygems.org'\ngit_source(:github) { |repo| \"https://github.com/#{repo}.git\" }\n\ngit \"https://github.com/r"
  },
  {
    "path": "kamal.gemspec",
    "chars": 1143,
    "preview": "require_relative \"lib/kamal/version\"\n\nGem::Specification.new do |spec|\n  spec.name        = \"kamal\"\n  spec.version     ="
  },
  {
    "path": "lib/kamal/cli/accessory.rb",
    "chars": 12170,
    "preview": "require \"active_support/core_ext/array/conversions\"\nrequire \"concurrent/array\"\n\nclass Kamal::Cli::Accessory < Kamal::Cli"
  },
  {
    "path": "lib/kamal/cli/alias/command.rb",
    "chars": 259,
    "preview": "class Kamal::Cli::Alias::Command < Thor::DynamicCommand\n  def run(instance, args = [])\n    if (command = KAMAL.resolve_a"
  },
  {
    "path": "lib/kamal/cli/app/assets.rb",
    "chars": 583,
    "preview": "class Kamal::Cli::App::Assets\n  attr_reader :host, :role, :sshkit\n  delegate :execute, :capture_with_info, :info, to: :s"
  },
  {
    "path": "lib/kamal/cli/app/boot.rb",
    "chars": 3920,
    "preview": "class Kamal::Cli::App::Boot\n  attr_reader :host, :role, :version, :barrier, :sshkit\n  delegate :execute, :capture_with_i"
  },
  {
    "path": "lib/kamal/cli/app/error_pages.rb",
    "chars": 933,
    "preview": "class Kamal::Cli::App::ErrorPages\n  ERROR_PAGES_GLOB = \"{4??.html,5??.html}\"\n\n  attr_reader :host, :sshkit\n  delegate :u"
  },
  {
    "path": "lib/kamal/cli/app/ssl_certificates.rb",
    "chars": 807,
    "preview": "class Kamal::Cli::App::SslCertificates\n  attr_reader :host, :role, :sshkit\n  delegate :execute, :info, :upload!, to: :ss"
  },
  {
    "path": "lib/kamal/cli/app.rb",
    "chars": 15063,
    "preview": "class Kamal::Cli::App < Kamal::Cli::Base\n  desc \"boot\", \"Boot app on servers (or reboot app if already running)\"\n  def b"
  },
  {
    "path": "lib/kamal/cli/base.rb",
    "chars": 7284,
    "preview": "require \"thor\"\nrequire \"kamal/sshkit_with_ext\"\n\nmodule Kamal::Cli\n  class Base < Thor\n    include SSHKit::DSL\n\n    VERBO"
  },
  {
    "path": "lib/kamal/cli/build/clone.rb",
    "chars": 1898,
    "preview": "class Kamal::Cli::Build::Clone\n  attr_reader :sshkit\n  delegate :info, :error, :execute, :capture_with_info, to: :sshkit"
  },
  {
    "path": "lib/kamal/cli/build/port_forwarding.rb",
    "chars": 1610,
    "preview": "require \"concurrent/atomic/count_down_latch\"\n\nclass Kamal::Cli::Build::PortForwarding\n  attr_reader :hosts, :port, :ssh_"
  },
  {
    "path": "lib/kamal/cli/build.rb",
    "chars": 7891,
    "preview": "class Kamal::Cli::Build < Kamal::Cli::Base\n  class BuildError < StandardError; end\n\n  desc \"deliver\", \"Build app and pus"
  },
  {
    "path": "lib/kamal/cli/healthcheck/barrier.rb",
    "chars": 479,
    "preview": "require \"concurrent/ivar\"\n\nclass Kamal::Cli::Healthcheck::Barrier\n  def initialize\n    @ivar = Concurrent::IVar.new\n  en"
  },
  {
    "path": "lib/kamal/cli/healthcheck/error.rb",
    "chars": 57,
    "preview": "class Kamal::Cli::Healthcheck::Error < StandardError\nend\n"
  },
  {
    "path": "lib/kamal/cli/healthcheck/poller.rb",
    "chars": 1101,
    "preview": "module Kamal::Cli::Healthcheck::Poller\n  extend self\n\n  def wait_for_healthy(&block)\n    attempt = 1\n    timeout_at = Ti"
  },
  {
    "path": "lib/kamal/cli/lock.rb",
    "chars": 1100,
    "preview": "class Kamal::Cli::Lock < Kamal::Cli::Base\n  desc \"status\", \"Report lock status\"\n  def status\n    handle_missing_lock do\n"
  },
  {
    "path": "lib/kamal/cli/main.rb",
    "chars": 10078,
    "preview": "class Kamal::Cli::Main < Kamal::Cli::Base\n  desc \"setup\", \"Setup all accessories, push the env, and deploy app to server"
  },
  {
    "path": "lib/kamal/cli/proxy.rb",
    "chars": 12274,
    "preview": "class Kamal::Cli::Proxy < Kamal::Cli::Base\n  desc \"boot\", \"Boot proxy on servers\"\n  def boot\n    with_lock do\n      on(K"
  },
  {
    "path": "lib/kamal/cli/prune.rb",
    "chars": 971,
    "preview": "class Kamal::Cli::Prune < Kamal::Cli::Base\n  desc \"all\", \"Prune unused images and stopped containers\"\n  def all\n    with"
  },
  {
    "path": "lib/kamal/cli/registry.rb",
    "chars": 2180,
    "preview": "class Kamal::Cli::Registry < Kamal::Cli::Base\n  desc \"setup\", \"Setup local registry or log in to remote registry locally"
  },
  {
    "path": "lib/kamal/cli/secrets.rb",
    "chars": 1699,
    "preview": "class Kamal::Cli::Secrets < Kamal::Cli::Base\n  desc \"fetch [SECRETS...]\", \"Fetch secrets from a vault\"\n  option :adapter"
  },
  {
    "path": "lib/kamal/cli/server.rb",
    "chars": 2100,
    "preview": "class Kamal::Cli::Server < Kamal::Cli::Base\n  desc \"exec\", \"Run a custom command on the server (use --help to show optio"
  },
  {
    "path": "lib/kamal/cli/templates/deploy.yml",
    "chars": 2796,
    "preview": "# Name of your application. Used to uniquely configure containers.\nservice: my-app\n\n# Name of the container image.\nimage"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/docker-setup.sample",
    "chars": 51,
    "preview": "#!/bin/sh\n\necho \"Docker set up on $KAMAL_HOSTS...\"\n"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/post-app-boot.sample",
    "chars": 71,
    "preview": "#!/bin/sh\n\necho \"Booted app version $KAMAL_VERSION on $KAMAL_HOSTS...\"\n"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/post-deploy.sample",
    "chars": 319,
    "preview": "#!/bin/sh\n\n# A sample post-deploy hook\n#\n# These environment variables are available:\n# KAMAL_RECORDED_AT\n# KAMAL_PERFOR"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample",
    "chars": 55,
    "preview": "#!/bin/sh\n\necho \"Rebooted kamal-proxy on $KAMAL_HOSTS\"\n"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample",
    "chars": 72,
    "preview": "#!/bin/sh\n\necho \"Booting app version $KAMAL_VERSION on $KAMAL_HOSTS...\"\n"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/pre-build.sample",
    "chars": 1103,
    "preview": "#!/bin/sh\n\n# A sample pre-build hook\n#\n# Checks:\n# 1. We have a clean checkout\n# 2. A remote is configured\n# 3. The bran"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/pre-connect.sample",
    "chars": 1039,
    "preview": "#!/usr/bin/env ruby\n\n# A sample pre-connect check\n#\n# Warms DNS before connecting to hosts in parallel\n#\n# These environ"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/pre-deploy.sample",
    "chars": 2833,
    "preview": "#!/usr/bin/env ruby\n\n# A sample pre-deploy hook\n#\n# Checks the Github status of the build, waiting for a pending build t"
  },
  {
    "path": "lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample",
    "chars": 59,
    "preview": "#!/bin/sh\n\necho \"Rebooting kamal-proxy on $KAMAL_HOSTS...\"\n"
  },
  {
    "path": "lib/kamal/cli/templates/secrets",
    "chars": 1051,
    "preview": "# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,\n# and accessori"
  },
  {
    "path": "lib/kamal/cli.rb",
    "chars": 284,
    "preview": "module Kamal::Cli\n  class BootError < StandardError; end\n  class HookError < StandardError; end\n  class LockError < Stan"
  },
  {
    "path": "lib/kamal/commander/specifics.rb",
    "chars": 1873,
    "preview": "class Kamal::Commander::Specifics\n  attr_reader :primary_host, :primary_role, :hosts, :roles\n  delegate :stable_sort!, t"
  },
  {
    "path": "lib/kamal/commander.rb",
    "chars": 4252,
    "preview": "require \"active_support/core_ext/enumerable\"\nrequire \"active_support/core_ext/module/delegation\"\nrequire \"active_support"
  },
  {
    "path": "lib/kamal/commands/accessory/proxy.rb",
    "chars": 395,
    "preview": "module Kamal::Commands::Accessory::Proxy\n  delegate :container_name, to: :\"config.proxy_boot\", prefix: :proxy\n\n  def dep"
  },
  {
    "path": "lib/kamal/commands/accessory.rb",
    "chars": 3071,
    "preview": "class Kamal::Commands::Accessory < Kamal::Commands::Base\n  include Proxy\n\n  attr_reader :accessory_config\n  delegate :se"
  },
  {
    "path": "lib/kamal/commands/app/assets.rb",
    "chars": 1838,
    "preview": "module Kamal::Commands::App::Assets\n  def extract_assets\n    asset_container = \"#{role.container_prefix}-assets\"\n\n    co"
  },
  {
    "path": "lib/kamal/commands/app/containers.rb",
    "chars": 845,
    "preview": "module Kamal::Commands::App::Containers\n  DOCKER_HEALTH_LOG_FORMAT    = \"'{{json .State.Health}}'\"\n\n  def list_container"
  },
  {
    "path": "lib/kamal/commands/app/error_pages.rb",
    "chars": 336,
    "preview": "module Kamal::Commands::App::ErrorPages\n  def create_error_pages_directory\n    make_directory(config.proxy_boot.error_pa"
  },
  {
    "path": "lib/kamal/commands/app/execution.rb",
    "chars": 1195,
    "preview": "module Kamal::Commands::App::Execution\n  def execute_in_existing_container(*command, interactive: false, env:)\n    docke"
  },
  {
    "path": "lib/kamal/commands/app/images.rb",
    "chars": 289,
    "preview": "module Kamal::Commands::App::Images\n  def list_images\n    docker :image, :ls, config.repository\n  end\n\n  def remove_imag"
  },
  {
    "path": "lib/kamal/commands/app/logging.rb",
    "chars": 999,
    "preview": "module Kamal::Commands::App::Logging\n  def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, "
  },
  {
    "path": "lib/kamal/commands/app/proxy.rb",
    "chars": 799,
    "preview": "module Kamal::Commands::App::Proxy\n  delegate :container_name, to: :\"config.proxy_boot\", prefix: :proxy\n\n  def deploy(ta"
  },
  {
    "path": "lib/kamal/commands/app.rb",
    "chars": 3540,
    "preview": "class Kamal::Commands::App < Kamal::Commands::Base\n  include Assets, Containers, ErrorPages, Execution, Images, Logging,"
  },
  {
    "path": "lib/kamal/commands/auditor.rb",
    "chars": 918,
    "preview": "class Kamal::Commands::Auditor < Kamal::Commands::Base\n  attr_reader :details\n  delegate :escape_shell_value, to: Kamal:"
  },
  {
    "path": "lib/kamal/commands/base.rb",
    "chars": 3504,
    "preview": "module Kamal::Commands\n  class Base\n    delegate :sensitive, :argumentize, to: Kamal::Utils\n\n    DOCKER_HEALTH_STATUS_FO"
  },
  {
    "path": "lib/kamal/commands/builder/base.rb",
    "chars": 3543,
    "preview": "class Kamal::Commands::Builder::Base < Kamal::Commands::Base\n  class BuilderError < StandardError; end\n\n  ENDPOINT_DOCKE"
  },
  {
    "path": "lib/kamal/commands/builder/clone.rb",
    "chars": 879,
    "preview": "module Kamal::Commands::Builder::Clone\n  def clone\n    git :clone, escaped_root, \"--recurse-submodules\", path: config.bu"
  },
  {
    "path": "lib/kamal/commands/builder/cloud.rb",
    "chars": 485,
    "preview": "class Kamal::Commands::Builder::Cloud < Kamal::Commands::Builder::Base\n  # Expects `driver` to be of format \"cloud docke"
  },
  {
    "path": "lib/kamal/commands/builder/hybrid.rb",
    "chars": 634,
    "preview": "class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote\n  def create\n    combine \\\n      create_local_"
  },
  {
    "path": "lib/kamal/commands/builder/local.rb",
    "chars": 461,
    "preview": "class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base\n  def create\n    return if docker_driver?\n\n    do"
  },
  {
    "path": "lib/kamal/commands/builder/pack.rb",
    "chars": 1296,
    "preview": "class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base\n  def push(export_action = \"registry\", tag_as_dirt"
  },
  {
    "path": "lib/kamal/commands/builder/remote.rb",
    "chars": 1677,
    "preview": "class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base\n  def create\n    chain \\\n      create_remote_con"
  },
  {
    "path": "lib/kamal/commands/builder.rb",
    "chars": 1061,
    "preview": "require \"active_support/core_ext/string/filters\"\n\nclass Kamal::Commands::Builder < Kamal::Commands::Base\n  delegate \\\n  "
  },
  {
    "path": "lib/kamal/commands/docker.rb",
    "chars": 1156,
    "preview": "class Kamal::Commands::Docker < Kamal::Commands::Base\n  # Install Docker using the https://github.com/docker/docker-inst"
  },
  {
    "path": "lib/kamal/commands/hook.rb",
    "chars": 397,
    "preview": "class Kamal::Commands::Hook < Kamal::Commands::Base\n  def run(hook)\n    [ hook_file(hook) ]\n  end\n\n  def env(secrets: fa"
  },
  {
    "path": "lib/kamal/commands/lock.rb",
    "chars": 1402,
    "preview": "require \"active_support/duration\"\nrequire \"time\"\nrequire \"base64\"\n\nclass Kamal::Commands::Lock < Kamal::Commands::Base\n "
  },
  {
    "path": "lib/kamal/commands/proxy.rb",
    "chars": 4080,
    "preview": "class Kamal::Commands::Proxy < Kamal::Commands::Base\n  delegate :argumentize, :optionize, to: Kamal::Utils\n  attr_reader"
  },
  {
    "path": "lib/kamal/commands/prune.rb",
    "chars": 1389,
    "preview": "require \"active_support/duration\"\nrequire \"active_support/core_ext/numeric/time\"\n\nclass Kamal::Commands::Prune < Kamal::"
  },
  {
    "path": "lib/kamal/commands/registry.rb",
    "chars": 996,
    "preview": "class Kamal::Commands::Registry < Kamal::Commands::Base\n  def login(registry_config: nil)\n    registry_config ||= config"
  },
  {
    "path": "lib/kamal/commands/server.rb",
    "chars": 309,
    "preview": "class Kamal::Commands::Server < Kamal::Commands::Base\n  def ensure_run_directory\n    make_directory config.run_directory"
  },
  {
    "path": "lib/kamal/commands.rb",
    "chars": 27,
    "preview": "module Kamal::Commands\nend\n"
  },
  {
    "path": "lib/kamal/configuration/accessory.rb",
    "chars": 7048,
    "preview": "class Kamal::Configuration::Accessory\n  include Kamal::Configuration::Validation\n\n  DEFAULT_NETWORK = \"kamal\"\n\n  delegat"
  },
  {
    "path": "lib/kamal/configuration/alias.rb",
    "chars": 390,
    "preview": "class Kamal::Configuration::Alias\n  include Kamal::Configuration::Validation\n\n  attr_reader :name, :command\n\n  def initi"
  },
  {
    "path": "lib/kamal/configuration/boot.rb",
    "chars": 535,
    "preview": "class Kamal::Configuration::Boot\n  include Kamal::Configuration::Validation\n\n  attr_reader :boot_config, :host_count\n\n  "
  },
  {
    "path": "lib/kamal/configuration/builder.rb",
    "chars": 4678,
    "preview": "class Kamal::Configuration::Builder\n  include Kamal::Configuration::Validation\n\n  attr_reader :config, :builder_config\n "
  },
  {
    "path": "lib/kamal/configuration/docs/accessory.yml",
    "chars": 4569,
    "preview": "# Accessories\n#\n# Accessories can be booted on a single host, a list of hosts, or on specific roles.\n# The hosts do not "
  },
  {
    "path": "lib/kamal/configuration/docs/alias.yml",
    "chars": 687,
    "preview": "# Aliases\n#\n# Aliases are shortcuts for Kamal commands.\n#\n# For example, for a Rails app, you might open a console with:"
  },
  {
    "path": "lib/kamal/configuration/docs/boot.yml",
    "chars": 711,
    "preview": "# Booting\n#\n# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at t"
  },
  {
    "path": "lib/kamal/configuration/docs/builder.yml",
    "chars": 3603,
    "preview": "# Builder\n#\n# The builder configuration controls how the application is built with `docker build`.\n#\n# See https://kamal"
  },
  {
    "path": "lib/kamal/configuration/docs/configuration.yml",
    "chars": 4782,
    "preview": "# Kamal Configuration\n#\n# Configuration is read from the `config/deploy.yml`.\n\n# Destinations\n#\n# When running commands,"
  },
  {
    "path": "lib/kamal/configuration/docs/env.yml",
    "chars": 3225,
    "preview": "# Environment variables\n#\n# Environment variables can be set directly in the Kamal configuration or\n# read from `.kamal/"
  },
  {
    "path": "lib/kamal/configuration/docs/logging.yml",
    "chars": 486,
    "preview": "# Custom logging configuration\n#\n# Set these to control the Docker logging driver and options.\n\n# Logging settings\n#\n# T"
  },
  {
    "path": "lib/kamal/configuration/docs/proxy.yml",
    "chars": 6856,
    "preview": "# Proxy\n#\n# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide\n# gapless deployments. It runs "
  },
  {
    "path": "lib/kamal/configuration/docs/registry.yml",
    "chars": 2390,
    "preview": "# Registry\n#\n# The default registry is Docker Hub, but you can change it using `registry/server`.\n\n# Using a local conta"
  },
  {
    "path": "lib/kamal/configuration/docs/role.yml",
    "chars": 1472,
    "preview": "# Roles\n#\n# Roles are used to configure different types of servers in the deployment.\n# The most common use for this is "
  },
  {
    "path": "lib/kamal/configuration/docs/servers.yml",
    "chars": 715,
    "preview": "# Servers\n#\n# Servers are split into different roles, with each role having its own configuration.\n#\n# For simpler deplo"
  },
  {
    "path": "lib/kamal/configuration/docs/ssh.yml",
    "chars": 1987,
    "preview": "# SSH configuration\n#\n# Kamal uses SSH to connect and run commands on your hosts.\n# By default, it will attempt to conne"
  },
  {
    "path": "lib/kamal/configuration/docs/sshkit.yml",
    "chars": 1081,
    "preview": "# SSHKit\n#\n# [SSHKit](https://github.com/capistrano/sshkit) is the SSH toolkit used by Kamal.\n#\n# The default, settings "
  },
  {
    "path": "lib/kamal/configuration/env/tag.rb",
    "chars": 275,
    "preview": "class Kamal::Configuration::Env::Tag\n  attr_reader :name, :config, :secrets\n\n  def initialize(name, config:, secrets:)\n "
  },
  {
    "path": "lib/kamal/configuration/env.rb",
    "chars": 1136,
    "preview": "class Kamal::Configuration::Env\n  include Kamal::Configuration::Validation\n\n  attr_reader :context, :clear, :secrets, :s"
  },
  {
    "path": "lib/kamal/configuration/logging.rb",
    "chars": 770,
    "preview": "class Kamal::Configuration::Logging\n  delegate :optionize, :argumentize, to: Kamal::Utils\n\n  include Kamal::Configuratio"
  },
  {
    "path": "lib/kamal/configuration/proxy/boot.rb",
    "chars": 2872,
    "preview": "class Kamal::Configuration::Proxy::Boot\n  attr_reader :config\n  delegate :argumentize, :optionize, to: Kamal::Utils\n\n  d"
  },
  {
    "path": "lib/kamal/configuration/proxy/run.rb",
    "chars": 3025,
    "preview": "class Kamal::Configuration::Proxy::Run\n  MINIMUM_VERSION = \"v0.9.2\"\n  DEFAULT_HTTP_PORT = 80\n  DEFAULT_HTTPS_PORT = 443\n"
  },
  {
    "path": "lib/kamal/configuration/proxy.rb",
    "chars": 4398,
    "preview": "class Kamal::Configuration::Proxy\n  include Kamal::Configuration::Validation\n\n  DEFAULT_LOG_REQUEST_HEADERS = [ \"Cache-C"
  },
  {
    "path": "lib/kamal/configuration/registry.rb",
    "chars": 816,
    "preview": "class Kamal::Configuration::Registry\n  include Kamal::Configuration::Validation\n\n  def initialize(config:, secrets:, con"
  },
  {
    "path": "lib/kamal/configuration/role.rb",
    "chars": 5812,
    "preview": "class Kamal::Configuration::Role\n  include Kamal::Configuration::Validation\n\n  delegate :argumentize, :optionize, to: Ka"
  },
  {
    "path": "lib/kamal/configuration/servers.rb",
    "chars": 596,
    "preview": "class Kamal::Configuration::Servers\n  include Kamal::Configuration::Validation\n\n  attr_reader :config, :servers_config, "
  },
  {
    "path": "lib/kamal/configuration/ssh.rb",
    "chars": 1529,
    "preview": "class Kamal::Configuration::Ssh\n  LOGGER = ::Logger.new(STDERR)\n\n  include Kamal::Configuration::Validation\n\n  attr_read"
  },
  {
    "path": "lib/kamal/configuration/sshkit.rb",
    "chars": 506,
    "preview": "class Kamal::Configuration::Sshkit\n  include Kamal::Configuration::Validation\n\n  attr_reader :sshkit_config\n\n  def initi"
  },
  {
    "path": "lib/kamal/configuration/validation.rb",
    "chars": 763,
    "preview": "require \"yaml\"\nrequire \"active_support/inflector\"\n\nmodule Kamal::Configuration::Validation\n  extend ActiveSupport::Conce"
  },
  {
    "path": "lib/kamal/configuration/validator/accessory.rb",
    "chars": 381,
    "preview": "class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validator\n  def validate!\n    super\n\n    if (co"
  },
  {
    "path": "lib/kamal/configuration/validator/alias.rb",
    "chars": 446,
    "preview": "class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator\n  def validate!\n    super\n\n    name = con"
  },
  {
    "path": "lib/kamal/configuration/validator/builder.rb",
    "chars": 614,
    "preview": "class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator\n  def validate!\n    super\n\n    if confi"
  },
  {
    "path": "lib/kamal/configuration/validator/configuration.rb",
    "chars": 146,
    "preview": "class Kamal::Configuration::Validator::Configuration < Kamal::Configuration::Validator\n  private\n    def allow_extension"
  },
  {
    "path": "lib/kamal/configuration/validator/env.rb",
    "chars": 1402,
    "preview": "class Kamal::Configuration::Validator::Env < Kamal::Configuration::Validator\n  SPECIAL_KEYS = [ \"clear\", \"secret\", \"tags"
  },
  {
    "path": "lib/kamal/configuration/validator/proxy.rb",
    "chars": 1529,
    "preview": "class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator\n  def validate!\n    unless config.nil?\n  "
  },
  {
    "path": "lib/kamal/configuration/validator/registry.rb",
    "chars": 875,
    "preview": "class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validator\n  STRING_OR_ONE_ITEM_ARRAY_KEYS = [ \"u"
  },
  {
    "path": "lib/kamal/configuration/validator/role.rb",
    "chars": 323,
    "preview": "class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator\n  def validate!\n    validate_type! config,"
  },
  {
    "path": "lib/kamal/configuration/validator/servers.rb",
    "chars": 209,
    "preview": "class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator\n  def validate!\n    validate_type! conf"
  },
  {
    "path": "lib/kamal/configuration/validator.rb",
    "chars": 6701,
    "preview": "class Kamal::Configuration::Validator\n  attr_reader :config, :example, :context\n\n  def initialize(config, example:, cont"
  },
  {
    "path": "lib/kamal/configuration/volume.rb",
    "chars": 700,
    "preview": "class Kamal::Configuration::Volume\n  attr_reader :host_path, :container_path, :options\n  delegate :argumentize, to: Kama"
  },
  {
    "path": "lib/kamal/configuration.rb",
    "chars": 12258,
    "preview": "require \"active_support/ordered_options\"\nrequire \"active_support/core_ext/string/inquiry\"\nrequire \"active_support/core_e"
  },
  {
    "path": "lib/kamal/docker.rb",
    "chars": 708,
    "preview": "require \"tempfile\"\nrequire \"open3\"\n\nmodule Kamal::Docker\n  extend self\n  BUILD_CHECK_TAG = \"kamal-local-build-check\"\n\n  "
  },
  {
    "path": "lib/kamal/env_file.rb",
    "chars": 1233,
    "preview": "# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.\nclass Kamal:"
  },
  {
    "path": "lib/kamal/git.rb",
    "chars": 698,
    "preview": "module Kamal::Git\n  extend self\n\n  def used?\n    system(\"git rev-parse\")\n  end\n\n  def user_name\n    `git config user.nam"
  },
  {
    "path": "lib/kamal/secrets/adapters/aws_secrets_manager.rb",
    "chars": 1590,
    "preview": "class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base\n  def requires_account?\n    false\n  e"
  },
  {
    "path": "lib/kamal/secrets/adapters/base.rb",
    "chars": 728,
    "preview": "class Kamal::Secrets::Adapters::Base\n  delegate :optionize, to: Kamal::Utils\n\n  def fetch(secrets, account: nil, from: n"
  },
  {
    "path": "lib/kamal/secrets/adapters/bitwarden.rb",
    "chars": 2808,
    "preview": "class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base\n  private\n    def login(account)\n      status"
  },
  {
    "path": "lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb",
    "chars": 2223,
    "preview": "class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base\n  def requires_account?\n    fal"
  },
  {
    "path": "lib/kamal/secrets/adapters/doppler.rb",
    "chars": 1538,
    "preview": "class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base\n  def requires_account?\n    false\n  end\n\n  priv"
  },
  {
    "path": "lib/kamal/secrets/adapters/enpass.rb",
    "chars": 2401,
    "preview": "##\n# Enpass is different from most password managers, in a way that it's offline and doesn't need an account.\n#\n# Usage\n"
  },
  {
    "path": "lib/kamal/secrets/adapters/gcp_secret_manager.rb",
    "chars": 4420,
    "preview": "class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base\n  private\n    def login(account)\n     "
  },
  {
    "path": "lib/kamal/secrets/adapters/last_pass.rb",
    "chars": 1199,
    "preview": "class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base\n  private\n    def login(account)\n      unless "
  },
  {
    "path": "lib/kamal/secrets/adapters/one_password.rb",
    "chars": 3297,
    "preview": "class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base\n  delegate :optionize, to: Kamal::Utils\n\n  "
  },
  {
    "path": "lib/kamal/secrets/adapters/passbolt.rb",
    "chars": 4879,
    "preview": "class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base\n  def requires_account?\n    false\n  end\n\n  pri"
  },
  {
    "path": "lib/kamal/secrets/adapters/test.rb",
    "chars": 390,
    "preview": "class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base\n  private\n    def login(account)\n      true\n    en"
  },
  {
    "path": "lib/kamal/secrets/adapters.rb",
    "chars": 579,
    "preview": "require \"active_support/core_ext/string/inflections\"\nmodule Kamal::Secrets::Adapters\n  def self.lookup(name)\n    name = "
  },
  {
    "path": "lib/kamal/secrets/dotenv/inline_command_substitution.rb",
    "chars": 1694,
    "preview": "class Kamal::Secrets::Dotenv::InlineCommandSubstitution\n  # Unlike dotenv, this regex does not match escaped\n  # parenth"
  },
  {
    "path": "lib/kamal/secrets.rb",
    "chars": 1314,
    "preview": "require \"dotenv\"\n\nclass Kamal::Secrets\n  Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!\n\n  def initialize(de"
  },
  {
    "path": "lib/kamal/sshkit_with_ext.rb",
    "chars": 7785,
    "preview": "require \"sshkit\"\nrequire \"sshkit/dsl\"\nrequire \"net/scp\"\nrequire \"active_support/core_ext/hash/deep_merge\"\nrequire \"json\""
  },
  {
    "path": "lib/kamal/tags.rb",
    "chars": 875,
    "preview": "require \"time\"\n\nclass Kamal::Tags\n  attr_reader :config, :tags\n\n  class << self\n    def from_config(config, **extra)\n   "
  },
  {
    "path": "lib/kamal/utils/sensitive.rb",
    "chars": 512,
    "preview": "require \"active_support/core_ext/module/delegation\"\nrequire \"sshkit\"\n\nclass Kamal::Utils::Sensitive\n  # So SSHKit knows "
  },
  {
    "path": "lib/kamal/utils.rb",
    "chars": 3346,
    "preview": "require \"active_support/core_ext/object/try\"\n\nmodule Kamal::Utils\n  extend self\n\n  DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_R"
  },
  {
    "path": "lib/kamal/version.rb",
    "chars": 38,
    "preview": "module Kamal\n  VERSION = \"2.11.0\"\nend\n"
  },
  {
    "path": "lib/kamal.rb",
    "chars": 359,
    "preview": "module Kamal\n  class ConfigurationError < StandardError; end\nend\n\nrequire \"active_support\"\nrequire \"zeitwerk\"\nrequire \"y"
  },
  {
    "path": "test/cli/accessory_test.rb",
    "chars": 15620,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliAccessoryTest < CliTestCase\n  setup do\n    setup_test_secrets(\"secrets\" => \"M"
  },
  {
    "path": "test/cli/app_test.rb",
    "chars": 40127,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliAppTest < CliTestCase\n  test \"boot\" do\n    stub_running\n    run_command(\"boot"
  },
  {
    "path": "test/cli/build_test.rb",
    "chars": 22674,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliBuildTest < CliTestCase\n  test \"deliver\" do\n    Kamal::Cli::Build.any_instanc"
  },
  {
    "path": "test/cli/cli_test_case.rb",
    "chars": 2305,
    "preview": "require \"test_helper\"\n\nclass CliTestCase < ActiveSupport::TestCase\n  setup do\n    ENV[\"VERSION\"]             = \"999\"\n   "
  },
  {
    "path": "test/cli/lock_test.rb",
    "chars": 576,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliLockTest < CliTestCase\n  test \"status\" do\n    run_command(\"status\").tap do |o"
  },
  {
    "path": "test/cli/main_test.rb",
    "chars": 34659,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliMainTest < CliTestCase\n  setup { @original_env = ENV.to_h.dup }\n  teardown { "
  },
  {
    "path": "test/cli/proxy_test.rb",
    "chars": 24585,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliProxyTest < CliTestCase\n  test \"boot\" do\n    run_command(\"boot\").tap do |outp"
  },
  {
    "path": "test/cli/prune_test.rb",
    "chars": 1597,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliPruneTest < CliTestCase\n  test \"all\" do\n    Kamal::Cli::Prune.any_instance.ex"
  },
  {
    "path": "test/cli/registry_test.rb",
    "chars": 4921,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliRegistryTest < CliTestCase\n  test \"setup\" do\n    run_command(\"setup\").tap do "
  },
  {
    "path": "test/cli/secrets_test.rb",
    "chars": 1080,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliSecretsTest < CliTestCase\n  test \"fetch\" do\n    assert_equal \\\n      '{\"foo\":"
  },
  {
    "path": "test/cli/server_test.rb",
    "chars": 5568,
    "preview": "require_relative \"cli_test_case\"\n\nclass CliServerTest < CliTestCase\n  test \"running a command with exec\" do\n    SSHKit::"
  },
  {
    "path": "test/commander_test.rb",
    "chars": 6453,
    "preview": "require \"test_helper\"\n\nclass CommanderTest < ActiveSupport::TestCase\n  setup do\n    configure_with(:deploy_with_roles)\n "
  },
  {
    "path": "test/commands/accessory_test.rb",
    "chars": 7929,
    "preview": "require \"test_helper\"\n\nclass CommandsAccessoryTest < ActiveSupport::TestCase\n  setup do\n    setup_test_secrets(\"secrets\""
  },
  {
    "path": "test/commands/app_test.rb",
    "chars": 35894,
    "preview": "require \"test_helper\"\n\nclass CommandsAppTest < ActiveSupport::TestCase\n  setup do\n    setup_test_secrets(\"secrets\" => \"R"
  },
  {
    "path": "test/commands/auditor_test.rb",
    "chars": 1933,
    "preview": "require \"test_helper\"\nrequire \"active_support/testing/time_helpers\"\n\nclass CommandsAuditorTest < ActiveSupport::TestCase"
  },
  {
    "path": "test/commands/builder_test.rb",
    "chars": 15634,
    "preview": "require \"test_helper\"\n\nclass CommandsBuilderTest < ActiveSupport::TestCase\n  setup do\n    @config = { service: \"app\", im"
  },
  {
    "path": "test/commands/docker_test.rb",
    "chars": 1328,
    "preview": "require \"test_helper\"\n\nclass CommandsDockerTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"app"
  },
  {
    "path": "test/commands/hook_test.rb",
    "chars": 1515,
    "preview": "require \"test_helper\"\n\nclass CommandsHookTest < ActiveSupport::TestCase\n  include ActiveSupport::Testing::TimeHelpers\n\n "
  },
  {
    "path": "test/commands/lock_test.rb",
    "chars": 971,
    "preview": "require \"test_helper\"\n\nclass CommandsLockTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"app\","
  },
  {
    "path": "test/commands/proxy_test.rb",
    "chars": 9695,
    "preview": "require \"test_helper\"\n\nclass CommandsProxyTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"app\""
  },
  {
    "path": "test/commands/prune_test.rb",
    "chars": 1506,
    "preview": "require \"test_helper\"\n\nclass CommandsPruneTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"app\""
  },
  {
    "path": "test/commands/registry_test.rb",
    "chars": 3241,
    "preview": "require \"test_helper\"\n\nclass CommandsRegistryTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"a"
  },
  {
    "path": "test/commands/server_test.rb",
    "chars": 543,
    "preview": "require \"test_helper\"\n\nclass CommandsServerTest < ActiveSupport::TestCase\n  setup do\n    @config = {\n      service: \"app"
  },
  {
    "path": "test/configuration/accessory_test.rb",
    "chars": 13508,
    "preview": "require \"test_helper\"\n\nclass ConfigurationAccessoryTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      servi"
  },
  {
    "path": "test/configuration/boot_test.rb",
    "chars": 1345,
    "preview": "require \"test_helper\"\n\nclass ConfigurationBootTest < ActiveSupport::TestCase\n  test \"no boot config\" do\n    config = con"
  },
  {
    "path": "test/configuration/builder_test.rb",
    "chars": 5530,
    "preview": "require \"test_helper\"\n\nclass ConfigurationBuilderTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service"
  },
  {
    "path": "test/configuration/env/tags_test.rb",
    "chars": 4310,
    "preview": "require \"test_helper\"\n\nclass ConfigurationEnvTagsTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service"
  },
  {
    "path": "test/configuration/env_test.rb",
    "chars": 2172,
    "preview": "require \"test_helper\"\n\nclass ConfigurationEnvTest < ActiveSupport::TestCase\n  require \"test_helper\"\n\n  test \"simple\" do\n"
  },
  {
    "path": "test/configuration/proxy/boot_test.rb",
    "chars": 1363,
    "preview": "require \"test_helper\"\n\nclass ConfigurationProxyBootTest < ActiveSupport::TestCase\n  setup do\n    ENV[\"RAILS_MASTER_KEY\"]"
  },
  {
    "path": "test/configuration/proxy_test.rb",
    "chars": 3528,
    "preview": "require \"test_helper\"\n\nclass ConfigurationProxyTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service: "
  },
  {
    "path": "test/configuration/role_test.rb",
    "chars": 11024,
    "preview": "require \"test_helper\"\n\nclass ConfigurationRoleTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service: \""
  },
  {
    "path": "test/configuration/ssh_test.rb",
    "chars": 3197,
    "preview": "require \"test_helper\"\n\nclass ConfigurationSshTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service: \"a"
  },
  {
    "path": "test/configuration/sshkit_test.rb",
    "chars": 1023,
    "preview": "require \"test_helper\"\n\nclass ConfigurationSshkitTest < ActiveSupport::TestCase\n  setup do\n    @deploy = {\n      service:"
  },
  {
    "path": "test/configuration/validation_test.rb",
    "chars": 7803,
    "preview": "require \"test_helper\"\nclass ConfigurationValidationTest < ActiveSupport::TestCase\n  test \"unknown root key\" do\n    asser"
  },
  {
    "path": "test/configuration/volume_test.rb",
    "chars": 1458,
    "preview": "require \"test_helper\"\n\nclass ConfigurationVolumeTest < ActiveSupport::TestCase\n  test \"docker args absolute\" do\n    volu"
  },
  {
    "path": "test/configuration_test.rb",
    "chars": 16613,
    "preview": "require \"test_helper\"\n\nclass ConfigurationTest < ActiveSupport::TestCase\n  setup do\n    ENV[\"RAILS_MASTER_KEY\"] = \"456\"\n"
  },
  {
    "path": "test/env_file_test.rb",
    "chars": 1958,
    "preview": "require \"test_helper\"\n\nclass EnvFileTest < ActiveSupport::TestCase\n  test \"to_s\" do\n    env = {\n      \"foo\" => \"bar\",\n  "
  },
  {
    "path": "test/fixtures/deploy.elsewhere.yml",
    "chars": 186,
    "preview": "service: app3\nimage: dhh/app3\nservers:\n  - \"1.1.1.3\"\n  - \"1.1.1.4\"\nregistry:\n  username: user\n  password: pw\nbuilder:\n  "
  },
  {
    "path": "test/fixtures/deploy.erb.yml",
    "chars": 222,
    "preview": "service: app\nimage: dhh/app\nservers:\n  - 1.1.1.1\n  - 1.1.1.2\nenv:\n  REDIS_URL: redis://x/y\nregistry:\n  server: registry."
  },
  {
    "path": "test/fixtures/deploy.yml",
    "chars": 232,
    "preview": "service: app\nimage: dhh/app\nservers:\n  - \"1.1.1.1\"\n  - \"1.1.1.2\"\nregistry:\n  username: user\n  password: pw\nbuilder:\n  ar"
  },
  {
    "path": "test/fixtures/deploy2.yml",
    "chars": 188,
    "preview": "service: app2\nimage: dhh/app2\nservers:\n  - \"1.1.1.1\"\n  - \"1.1.1.2\"\nregistry:\n  username: user2\n  password: pw2\nbuilder:\n"
  },
  {
    "path": "test/fixtures/deploy_for_dest.mars.yml",
    "chars": 63,
    "preview": "servers:\n  - 1.1.1.3\n  - 1.1.1.4\nenv:\n  REDIS_URL: redis://a/b\n"
  },
  {
    "path": "test/fixtures/deploy_for_dest.world.yml",
    "chars": 63,
    "preview": "servers:\n  - 1.1.1.1\n  - 1.1.1.2\nenv:\n  REDIS_URL: redis://x/y\n"
  },
  {
    "path": "test/fixtures/deploy_for_dest.yml",
    "chars": 159,
    "preview": "service: app\nimage: dhh/app\nregistry:\n  server: registry.digitalocean.com\n  username: <%= \"my-user\" %>\n  password: <%= \""
  },
  {
    "path": "test/fixtures/deploy_for_required_dest.world.yml",
    "chars": 63,
    "preview": "servers:\n  - 1.1.1.1\n  - 1.1.1.2\nenv:\n  REDIS_URL: redis://x/y\n"
  },
  {
    "path": "test/fixtures/deploy_for_required_dest.yml",
    "chars": 226,
    "preview": "service: app\nimage: dhh/app\nregistry:\n  server: registry.digitalocean.com\n  username: <%= \"my-user\" %>\n  password: <%= \""
  },
  {
    "path": "test/fixtures/deploy_primary_web_role_override.yml",
    "chars": 334,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web_chicago:\n    proxy: {}\n    hosts:\n      - 1.1.1.1\n      - 1.1.1.2\n  web_tokyo"
  },
  {
    "path": "test/fixtures/deploy_simple.yml",
    "chars": 130,
    "preview": "service: app\nimage: dhh/app\nservers:\n  - \"1.1.1.1\"\n  - \"1.1.1.2\"\nregistry:\n  username: user\n  password: pw\nbuilder:\n  ar"
  },
  {
    "path": "test/fixtures/deploy_with_accessories.yml",
    "chars": 592,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - \"1.1.1.1\"\n    - \"1.1.1.2\"\n  workers:\n    - \"1.1.1.3\"\n    - \"1.1.1.4\"\nr"
  },
  {
    "path": "test/fixtures/deploy_with_accessories_on_independent_server.yml",
    "chars": 592,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - \"1.1.1.1\"\n    - \"1.1.1.2\"\n  workers:\n    - \"1.1.1.3\"\n    - \"1.1.1.4\"\nr"
  },
  {
    "path": "test/fixtures/deploy_with_accessories_with_different_registries.yml",
    "chars": 810,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - \"1.1.1.1\"\n    - \"1.1.1.2\"\n  workers:\n    - \"1.1.1.3\"\n    - \"1.1.1.4\"\nr"
  },
  {
    "path": "test/fixtures/deploy_with_aliases.yml",
    "chars": 466,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - 1.1.1.1\n    - 1.1.1.2\n  workers:\n    hosts:\n      - 1.1.1.3\n      - 1."
  },
  {
    "path": "test/fixtures/deploy_with_assets.yml",
    "chars": 157,
    "preview": "service: app\nimage: dhh/app\nservers:\n  - \"1.1.1.1\"\n  - \"1.1.1.2\"\nregistry:\n  username: user\n  password: pw\nbuilder:\n  ar"
  },
  {
    "path": "test/fixtures/deploy_with_boot_strategy.yml",
    "chars": 213,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - \"1.1.1.1\"\n    - \"1.1.1.2\"\n  workers:\n    - \"1.1.1.3\"\n    - \"1.1.1.4\"\nb"
  },
  {
    "path": "test/fixtures/deploy_with_cloud_builder.yml",
    "chars": 692,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - \"1.1.1.1\"\n    - \"1.1.1.2\"\n  workers:\n    - \"1.1.1.3\"\n    - \"1.1.1.4\"\nr"
  },
  {
    "path": "test/fixtures/deploy_with_env_tags.yml",
    "chars": 493,
    "preview": "service: app\nimage: dhh/app\nservers:\n  web:\n    - 1.1.1.1: site1\n    - 1.1.1.2: [ site1 experimental ]\n    - 1.2.1.1: si"
  },
  {
    "path": "test/fixtures/deploy_with_error_pages.yml",
    "chars": 155,
    "preview": "service: app\nimage: dhh/app\nservers:\n  - \"1.1.1.1\"\n  - \"1.1.1.2\"\nregistry:\n  username: user\n  password: pw\nbuilder:\n  ar"
  }
]

// ... and 113 more files (download for full content)

About this extraction

This page contains the full source code of the basecamp/kamal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 313 files (739.5 KB), approximately 218.9k tokens, and a symbol index with 1275 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!