Distribution and Deployment
Distribution and Deployment
The following files were used as context for generating this wiki page:
- .dockerignore
- .github/workflows/docker-build.yml
- .github/workflows/ghcr-publish.yml
- README.md
- docker-compose.yml
Purpose and Scope
This document describes how WSHawk is packaged, distributed, and deployed across different environments. It covers the automated CI/CD pipeline, distribution channels (PyPI and container registries), Docker image architecture, and deployment patterns for local development, CI/CD integration, and production Kubernetes environments.
For information about configuring WSHawk after deployment, see Configuration System. For details about the Web Management Dashboard deployment, see Dashboard Overview and Launch.
Distribution Architecture Overview
WSHawk employs a multi-channel distribution strategy with automated CI/CD pipelines that build and publish artifacts to three primary distribution channels.
graph TB
subgraph SourceControl["Source Control"]
GitHubRepo["GitHub Repository<br/>regaan/wshawk"]
end
subgraph CI["CI/CD Pipeline"]
GHActions["GitHub Actions<br/>docker-build.yml<br/>ghcr-publish.yml"]
BuildTriggers["Triggers:<br/>push to main<br/>tags v*<br/>pull_request"]
QEMUSetup["QEMU Emulation<br/>docker/setup-qemu-action@v3"]
BuildxSetup["Docker Buildx<br/>docker/setup-buildx-action@v3"]
MetadataExtract["Metadata Extraction<br/>docker/metadata-action@v5"]
MultiArchBuild["Multi-Arch Build<br/>linux/amd64<br/>linux/arm64"]
end
subgraph Registries["Distribution Channels"]
PyPI["PyPI<br/>pip install wshawk<br/>Python Package Index"]
DockerHub["Docker Hub<br/>rothackers/wshawk<br/>Public Registry"]
GHCR["GitHub Container Registry<br/>ghcr.io/regaan/wshawk<br/>GITHUB_TOKEN Auth"]
end
subgraph TagStrategy["Tag Strategy"]
LatestTag["latest<br/>is_default_branch"]
SemverFull["3.0.0<br/>{{version}}"]
SemverMinor["3.0<br/>{{major}}.{{minor}}"]
SemverMajor["3<br/>{{major}}"]
BranchTag["main<br/>ref/branch"]
end
subgraph Deployment["Deployment Targets"]
LocalDev["Local Development<br/>docker-compose.yml<br/>pip install -e ."]
CICDIntegration["CI/CD Integration<br/>GitHub Actions Jobs<br/>GitLab CI Pipelines"]
K8sJobs["Kubernetes<br/>Jobs/CronJobs<br/>Scheduled Scanning"]
ProdDocker["Production Docker<br/>Volume Mounts<br/>Environment Variables"]
end
GitHubRepo --> BuildTriggers
BuildTriggers --> GHActions
GHActions --> QEMUSetup
GHActions --> BuildxSetup
GHActions --> MetadataExtract
QEMUSetup --> MultiArchBuild
BuildxSetup --> MultiArchBuild
MetadataExtract --> MultiArchBuild
MultiArchBuild --> PyPI
MultiArchBuild --> DockerHub
MultiArchBuild --> GHCR
MetadataExtract --> LatestTag
MetadataExtract --> SemverFull
MetadataExtract --> SemverMinor
MetadataExtract --> SemverMajor
MetadataExtract --> BranchTag
LatestTag --> DockerHub
LatestTag --> GHCR
SemverFull --> DockerHub
SemverFull --> GHCR
SemverMinor --> DockerHub
SemverMinor --> GHCR
PyPI --> LocalDev
DockerHub --> LocalDev
DockerHub --> CICDIntegration
DockerHub --> K8sJobs
DockerHub --> ProdDocker
GHCR --> CICDIntegration
GHCR --> K8sJobs
Sources: .github/workflows/docker-build.yml:1-89, README.md:53-81
Package Distribution Channels
WSHawk is distributed through three official channels, each serving different deployment scenarios.
Distribution Channel Matrix
| Channel | URL | Authentication | Use Case | Installation Command |
|---------|-----|----------------|----------|---------------------|
| PyPI | pypi.org/project/wshawk | Public | Python environments, pip-based workflows | pip install wshawk |
| Docker Hub | rothackers/wshawk | Public | Production containers, quick deployment | docker pull rothackers/wshawk |
| GitHub Container Registry | ghcr.io/regaan/wshawk | GITHUB_TOKEN | CI/CD integration, private workflows | docker pull ghcr.io/regaan/wshawk |
PyPI Distribution
WSHawk is published to the Python Package Index for installation via pip. This is the recommended method for development environments and when integrating WSHawk into Python-based security toolchains.
# Standard installation
pip install wshawk
# With optional Playwright support for XSS verification
pip install wshawk
playwright install chromium
The PyPI distribution includes all Python packages, entry points defined in pyproject.toml, and payload files specified in MANIFEST.in. Package metadata such as version, dependencies, and entry points are defined in setup.py and pyproject.toml.
Docker Hub Distribution
The primary public container registry is Docker Hub under the rothackers/wshawk repository. Images are automatically pushed on every commit to main and on version tags matching v* pattern.
# Pull latest version
docker pull rothackers/wshawk:latest
# Pull specific version
docker pull rothackers/wshawk:3.0.0
Environment variable DOCKERHUB_REPO is set to rothackers/wshawk .github/workflows/docker-build.yml:15.
GitHub Container Registry Distribution
GHCR provides GitHub-integrated distribution, useful for CI/CD workflows that already use GitHub Actions. Authentication uses GITHUB_TOKEN, eliminating the need for separate credentials.
# Login to GHCR
echo $GITHUB_TOKEN | docker login ghcr.io -u ${{ github.actor }} --password-stdin
# Pull image
docker pull ghcr.io/regaan/wshawk:latest
The registry URL is defined as ghcr.io/${{ github.repository }} .github/workflows/docker-build.yml:16.
Sources: README.md:53-81, .github/workflows/docker-build.yml:15-16
CI/CD Pipeline Architecture
WSHawk's distribution is fully automated via GitHub Actions workflows. The pipeline builds multi-architecture Docker images and publishes them to both Docker Hub and GHCR.
graph TB
subgraph Triggers["Pipeline Triggers"]
PushMain["push to main branch"]
PushTag["push tags v*"]
PullRequest["pull_request to main"]
ManualDispatch["workflow_dispatch"]
end
subgraph Setup["Build Environment Setup"]
CheckoutRepo["actions/checkout@v4<br/>Clone Repository"]
SetupQEMU["docker/setup-qemu-action@v3<br/>Enable ARM64 Emulation"]
SetupBuildx["docker/setup-buildx-action@v3<br/>Multi-Platform Builder"]
end
subgraph Authentication["Registry Authentication"]
VerifySecrets["Verify DOCKER_USERNAME<br/>Verify DOCKER_PASSWORD<br/>Skip on pull_request"]
LoginDockerHub["docker/login-action@v3<br/>username: DOCKER_USERNAME<br/>password: DOCKER_PASSWORD"]
LoginGHCR["docker/login-action@v3<br/>registry: ghcr.io<br/>username: github.actor<br/>password: GITHUB_TOKEN"]
end
subgraph MetadataGen["Metadata Generation"]
ExtractMeta["docker/metadata-action@v5"]
TagRules["Tag Rules:<br/>type=ref,event=branch<br/>type=ref,event=pr<br/>type=semver,pattern={{version}}<br/>type=semver,pattern={{major}}.{{minor}}<br/>type=semver,pattern={{major}}<br/>type=raw,value=latest"]
ImageTargets["Image Targets:<br/>rothackers/wshawk<br/>ghcr.io/regaan/wshawk"]
end
subgraph BuildPush["Build and Push"]
BuildAction["docker/build-push-action@v5"]
BuildContext["context: .<br/>Dockerfile"]
Platforms["platforms:<br/>linux/amd64<br/>linux/arm64"]
CacheStrategy["cache-from: type=gha<br/>cache-to: type=gha,mode=max"]
PushCondition["push: github.event_name != 'pull_request'"]
end
subgraph Testing["Image Testing"]
PullTest["docker pull ghcr.io/...wshawk:latest"]
HelpTest["docker run --rm ... --help"]
DefensiveTest["docker run --rm<br/>--entrypoint wshawk-defensive<br/>... --help"]
end
PushMain --> CheckoutRepo
PushTag --> CheckoutRepo
PullRequest --> CheckoutRepo
ManualDispatch --> CheckoutRepo
CheckoutRepo --> SetupQEMU
SetupQEMU --> SetupBuildx
SetupBuildx --> VerifySecrets
VerifySecrets --> LoginDockerHub
VerifySecrets --> LoginGHCR
LoginDockerHub --> ExtractMeta
LoginGHCR --> ExtractMeta
ExtractMeta --> TagRules
ExtractMeta --> ImageTargets
TagRules --> BuildAction
ImageTargets --> BuildAction
BuildAction --> BuildContext
BuildAction --> Platforms
BuildAction --> CacheStrategy
BuildAction --> PushCondition
PushCondition --> PullTest
PullTest --> HelpTest
HelpTest --> DefensiveTest
Build Trigger Configuration
The pipeline is triggered by four event types defined in .github/workflows/docker-build.yml:3-12:
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
pushtomain: Builds and pusheslatesttagpushtagsv*: Builds and pushes semantic version tags (e.g.,v3.0.0→3.0.0,3.0,3,latest)pull_request: Builds only (no push) for validationworkflow_dispatch: Manual trigger for ad-hoc builds
Multi-Architecture Build Support
WSHawk supports both linux/amd64 and linux/arm64 architectures via QEMU emulation. This is configured in .github/workflows/docker-build.yml:29-33:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
The build action specifies both platforms .github/workflows/docker-build.yml:80:
platforms: linux/amd64,linux/arm64
This ensures WSHawk can run on x86_64 servers, ARM-based cloud instances (AWS Graviton), and Apple Silicon Macs.
Semantic Versioning Tag Strategy
The metadata extraction step generates multiple tags from a single version release using .github/workflows/docker-build.yml:56-69:
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_REPO }}
${{ env.GHCR_REPO }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
Tag generation example for v3.0.0:
| Input Tag | Generated Tags | Description |
|-----------|---------------|-------------|
| v3.0.0 | 3.0.0 | Full semantic version |
| v3.0.0 | 3.0 | Major.minor (allows patch updates) |
| v3.0.0 | 3 | Major version only |
| v3.0.0 | latest | Always points to most recent release |
Secret Management
Docker Hub authentication requires two secrets to be configured in the GitHub repository:
DOCKER_USERNAME: Docker Hub usernameDOCKER_PASSWORD: Docker Hub access token or password
Secret verification occurs in .github/workflows/docker-build.yml:35-39:
- name: Verify Secrets Presence
if: github.event_name != 'pull_request'
run: |
if [ -z "${{ secrets.DOCKER_USERNAME }}" ]; then
echo "::error::DOCKER_USERNAME secret is MISSING"; exit 1
fi
GHCR authentication uses the automatic GITHUB_TOKEN provided by GitHub Actions .github/workflows/docker-build.yml:48-54.
Build Caching Strategy
The pipeline uses GitHub Actions cache to accelerate builds .github/workflows/docker-build.yml:78-79:
cache-from: type=gha
cache-to: type=gha,mode=max
This caches Docker layers between builds, significantly reducing build time for incremental changes.
Automated Testing
After successful push, the pipeline validates the image .github/workflows/docker-build.yml:82-88:
- name: Test Docker image
if: github.event_name != 'pull_request'
run: |
docker pull ${{ env.GHCR_REPO }}:latest
docker run --rm ${{ env.GHCR_REPO }}:latest --help
docker run --rm --entrypoint wshawk-defensive ${{ env.GHCR_REPO }}:latest --help
This verifies:
- Image is pullable from GHCR
- Default entrypoint (
wshawk) executes successfully - Alternative entrypoint (
wshawk-defensive) is accessible
Sources: .github/workflows/docker-build.yml:1-89, .github/workflows/ghcr-publish.yml:1-50
Docker Image Build Configuration
Dockerfile Multi-Stage Build
The WSHawk Docker image is built using a multi-stage Dockerfile pattern (referenced but not shown in provided files). The build process installs system dependencies, Python packages, and Playwright browsers.
Build Context Exclusions
Files excluded from the Docker build context are defined in .dockerignore:1-72. Key exclusions include:
| Category | Patterns | Rationale |
|----------|----------|-----------|
| Python Artifacts | __pycache__/, *.pyc, dist/, build/ | Build-time generated files |
| Virtual Environments | venv/, env/, wshawk_venv/ | Environment-specific dependencies |
| Test Artifacts | .pytest_cache/, .coverage, .hypothesis/ | Testing infrastructure not needed in production |
| Reports | reports/, *.html, *.log | Runtime-generated output |
| Documentation | docs/, *.md (except README.md) | Not needed in container runtime |
| CI/CD | .github/, .gitlab-ci.yml | Build configuration only |
This reduces the Docker build context size and prevents sensitive local files from being included in the image.
Sources: .dockerignore:1-72
Docker Compose for Local Development
The project includes a docker-compose.yml configuration for local development and testing scenarios.
graph LR
subgraph ComposeServices["docker-compose.yml Services"]
WSHawkService["wshawk service<br/>build: .<br/>network_mode: host"]
VulnServer["vulnerable-server service<br/>python:3.11-slim<br/>port 8765"]
end
subgraph Volumes["Volume Mounts"]
ReportsVol["./reports:/app/reports<br/>Persist scan reports"]
ExamplesVol["./examples:/app<br/>Test server content"]
end
subgraph Network["Networking"]
HostNetwork["host network<br/>Direct WebSocket access"]
BridgeNetwork["wshawk-network<br/>bridge driver"]
end
subgraph Entrypoint["Interactive Usage"]
BashShell["entrypoint: /bin/bash<br/>stdin_open: true<br/>tty: true"]
end
WSHawkService --> ReportsVol
WSHawkService --> HostNetwork
WSHawkService --> BashShell
VulnServer --> ExamplesVol
VulnServer --> BridgeNetwork
Service Configuration
The wshawk service is configured for interactive development docker-compose.yml:4-18:
wshawk:
build: .
container_name: wshawk
network_mode: host
volumes:
- ./reports:/app/reports
entrypoint: /bin/bash
stdin_open: true
tty: true
Key features:
network_mode: host: Provides direct access tolocalhostWebSocket servers without Docker networking overheadentrypoint: /bin/bash: Drops into interactive shell for manual testingstdin_open: true+tty: true: Enables interactive terminal
Vulnerable Test Server
A companion service provides a basic test server docker-compose.yml:20-31:
vulnerable-server:
image: python:3.11-slim
ports:
- "8765:8765"
volumes:
- ./examples:/app
command: python3 -m http.server 8765
This allows testing WSHawk against a local target without external dependencies.
Usage Pattern
# Start services
docker-compose up -d
# Enter WSHawk container
docker-compose exec wshawk bash
# Inside container, run scans
wshawk ws://localhost:8765
wshawk-defensive wss://production-server.com
# View reports on host
ls -lh reports/
# Cleanup
docker-compose down
Sources: docker-compose.yml:1-36
Docker Deployment Patterns
Basic Docker Run Commands
# Quick scan with default settings
docker run --rm rothackers/wshawk ws://target.com
# Advanced scan with all features
docker run --rm rothackers/wshawk wshawk-advanced \
ws://target.com \
--smart-payloads \
--playwright \
--full
# Defensive validation
docker run --rm rothackers/wshawk wshawk-defensive wss://your-server.com
Volume Mounts for Report Persistence
Scan reports and traffic logs are generated inside the container at /app/reports. To persist these to the host:
docker run --rm \
-v $(pwd)/reports:/app/reports \
rothackers/wshawk ws://target.com
This mounts the host's ./reports directory to /app/reports inside the container, making all generated files accessible after the container exits.
Environment Variable Configuration
WSHawk supports configuration via environment variables. Common examples:
# Web dashboard with authentication
docker run -d \
-p 5000:5000 \
-e WSHAWK_WEB_PASSWORD='secure-password' \
-e WSHAWK_API_KEY='api-key-for-automation' \
rothackers/wshawk --web --host 0.0.0.0 --port 5000
# Integration credentials
docker run --rm \
-e JIRA_API_TOKEN='token-value' \
-e DEFECTDOJO_API_KEY='dd-key' \
-v $(pwd)/wshawk.yaml:/app/wshawk.yaml \
rothackers/wshawk ws://target.com
See Configuration System for complete environment variable reference.
Security Considerations
For production deployments, apply container security best practices:
# Run as non-root user (if supported by image)
docker run --rm \
--user 1000:1000 \
--read-only \
--tmpfs /tmp \
-v $(pwd)/reports:/app/reports:rw \
rothackers/wshawk ws://target.com
# Resource limits
docker run --rm \
--memory="512m" \
--cpus="1.0" \
rothackers/wshawk ws://target.com
Sources: README.md:64-81
CI/CD Integration Scenarios
GitHub Actions Integration
WSHawk can be integrated into GitHub Actions workflows for automated security scanning on every pull request or scheduled scans.
name: WebSocket Security Scan
on:
pull_request:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
wshawk-scan:
runs-on: ubuntu-latest
steps:
- name: Run WSHawk Scan
run: |
docker pull ghcr.io/regaan/wshawk:latest
docker run --rm \
-v ${{ github.workspace }}/reports:/app/reports \
ghcr.io/regaan/wshawk:latest \
wshawk-advanced ws://staging.example.com --full
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: reports/wshawk.sarif
This workflow:
- Pulls the latest GHCR image (authentication via
GITHUB_TOKEN) - Runs a full scan against the staging environment
- Uploads SARIF results to GitHub Security tab
GitLab CI Integration
websocket_security_scan:
stage: security
image: docker:latest
services:
- docker:dind
script:
- docker pull rothackers/wshawk:latest
- docker run --rm
-v $(pwd)/reports:/app/reports
rothackers/wshawk
wshawk ws://${STAGING_WS_URL}
artifacts:
paths:
- reports/
reports:
junit: reports/wshawk-junit.xml
only:
- merge_requests
- schedules
SARIF Export for Security Dashboards
WSHawk can export findings in SARIF (Static Analysis Results Interchange Format) for integration with security dashboards. See Report Formats and Export for SARIF generation details.
Sources: README.md:64-81, .github/workflows/docker-build.yml:1-89
Kubernetes Deployment
One-Time Security Scan Job
For ad-hoc security assessments:
apiVersion: batch/v1
kind: Job
metadata:
name: wshawk-scan-prod
namespace: security
spec:
template:
spec:
containers:
- name: wshawk
image: rothackers/wshawk:3.0.0
command: ["wshawk-advanced"]
args:
- "wss://production-api.example.com/ws"
- "--full"
- "--smart-payloads"
env:
- name: JIRA_API_TOKEN
valueFrom:
secretKeyRef:
name: wshawk-secrets
key: jira-token
volumeMounts:
- name: reports
mountPath: /app/reports
- name: config
mountPath: /app/wshawk.yaml
subPath: wshawk.yaml
volumes:
- name: reports
persistentVolumeClaim:
claimName: wshawk-reports-pvc
- name: config
configMap:
name: wshawk-config
restartPolicy: Never
Scheduled CronJob for Regular Scans
For recurring security assessments:
apiVersion: batch/v1
kind: CronJob
metadata:
name: wshawk-weekly-scan
namespace: security
spec:
schedule: "0 3 * * 0" # Every Sunday at 3 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: wshawk
image: ghcr.io/regaan/wshawk:latest
command: ["wshawk-advanced"]
args:
- "wss://api.example.com/ws"
- "--playwright"
- "--rate=5"
env:
- name: DEFECTDOJO_API_KEY
valueFrom:
secretKeyRef:
name: defectdojo-creds
key: api-key
- name: WSHAWK_WEB_PASSWORD
valueFrom:
secretKeyRef:
name: wshawk-web-creds
key: password
volumeMounts:
- name: reports
mountPath: /app/reports
volumes:
- name: reports
persistentVolumeClaim:
claimName: wshawk-reports-pvc
restartPolicy: OnFailure
Multi-Service Scanning Deployment
For organizations with multiple WebSocket services:
apiVersion: batch/v1
kind: Job
metadata:
name: wshawk-multi-service-scan
spec:
parallelism: 3
completions: 3
template:
spec:
containers:
- name: wshawk
image: rothackers/wshawk:latest
command: ["bash", "-c"]
args:
- |
TARGETS=(
"wss://service1.example.com/ws"
"wss://service2.example.com/ws"
"wss://service3.example.com/ws"
)
wshawk-advanced ${TARGETS[$JOB_COMPLETION_INDEX]} --full
env:
- name: JOB_COMPLETION_INDEX
valueFrom:
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
restartPolicy: Never
Centralized DefectDojo Integration
For pushing findings to a centralized DefectDojo instance:
apiVersion: v1
kind: ConfigMap
metadata:
name: wshawk-config
data:
wshawk.yaml: |
integrations:
defectdojo:
url: "https://defectdojo.security.internal"
api_key: "env:DEFECTDOJO_API_KEY"
engagement_id: 42
auto_create_findings: true
minimum_severity: "MEDIUM"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: wshawk-to-defectdojo
spec:
schedule: "0 */6 * * *" # Every 6 hours
jobTemplate:
spec:
template:
spec:
containers:
- name: wshawk
image: rothackers/wshawk:3.0
args:
- "wss://api.prod.example.com/ws"
- "--full"
volumeMounts:
- name: config
mountPath: /app/wshawk.yaml
subPath: wshawk.yaml
volumes:
- name: config
configMap:
name: wshawk-config
restartPolicy: OnFailure
Sources: README.md:23-37
Build Metadata and Packaging
Entry Points Definition
WSHawk defines four CLI entry points in pyproject.toml:
[project.scripts]
wshawk = "wshawk.__main__:main"
wshawk-interactive = "wshawk.interactive:main"
wshawk-advanced = "wshawk.scanner_v2:cli_main"
wshawk-defensive = "wshawk.defensive_validation:main"
These entry points are automatically registered during pip install and are available as commands in the container's PATH.
Package Metadata
Version information, dependencies, and project metadata are centralized in setup.py and pyproject.toml. The package includes:
- Core dependencies:
websockets,aiohttp,requests,pyyaml,colorama - Optional dependencies:
playwright(for XSS verification) - Payload files: Included via
MANIFEST.inspecifications - Entry scripts: CLI commands for different operational modes
Image Entrypoint
The default Docker entrypoint is set to wshawk, allowing users to run:
docker run rothackers/wshawk ws://target.com
Alternative entrypoints can be specified:
docker run --rm --entrypoint wshawk-defensive rothackers/wshawk ws://target.com
docker run --rm --entrypoint wshawk-interactive rothackers/wshawk
Sources: README.md:84-110, .github/workflows/docker-build.yml:87
Distribution Security and Verification
Official Distribution Channels
As stated in README.md:3-14, only the following sources are official:
- GitHub Repository:
https://github.com/regaan/wshawk - PyPI:
pip install wshawk - Docker Hub:
docker pull rothackers/wshawk - GHCR:
docker pull ghcr.io/regaan/wshawk
Verification Methods
To verify Docker image authenticity:
# Verify image digest
docker pull rothackers/wshawk:3.0.0
docker inspect rothackers/wshawk:3.0.0 | grep -A5 RepoDigests
# Verify GHCR image matches source repository
docker pull ghcr.io/regaan/wshawk:3.0.0
docker history ghcr.io/regaan/wshawk:3.0.0
To verify PyPI package integrity:
# Check package hash
pip download wshawk --no-deps
sha256sum wshawk-*.whl
# Verify package metadata
pip show wshawk
Registry Access Patterns
| Registry | Pull Authentication | Push Authentication | Rate Limits |
|----------|-------------------|-------------------|-------------|
| Docker Hub | Not required (public) | DOCKER_USERNAME + DOCKER_PASSWORD | 100 pulls/6hr (anonymous), 200 pulls/6hr (authenticated) |
| GHCR | Not required (public repos) | GITHUB_TOKEN with packages:write | 5000 requests/hour |
| PyPI | Not required | PyPI API token | 10 requests/second (downloads unlimited) |
Sources: README.md:3-14, .github/workflows/docker-build.yml:41-54