Files
Charon/docs/plans/current_spec.md
GitHub Actions ba900e20c5 chore(ci): add Docker Hub as secondary container registry
Publish Docker images to both Docker Hub (docker.io/wikid82/charon) and
GitHub Container Registry (ghcr.io/wikid82/charon) for maximum reach.

Add Docker Hub login with secret existence check for graceful fallback
Update docker/metadata-action to generate tags for both registries
Add Cosign keyless signing for both GHCR and Docker Hub images
Attach SBOM to Docker Hub via cosign attach sbom
Add Docker Hub signature verification to supply-chain-verify workflow
Update README with Docker Hub badges and dual registry examples
Update getting-started.md with both registry options
Supply chain security maintained: identical tags, signatures, and SBOMs
on both registries. PR images remain GHCR-only.
2026-01-25 16:04:42 +00:00

594 lines
19 KiB
Markdown

# Docker Hub + GHCR Dual Registry Publishing Plan
**Plan ID**: DOCKER-2026-001
**Status**: 📋 PLANNED
**Priority**: High
**Created**: 2026-01-25
**Branch**: feature/beta-release
**Scope**: Publish Docker images to both Docker Hub and GitHub Container Registry (GHCR)
---
## Executive Summary
This plan details the implementation of dual-registry publishing for the Charon Docker image. Currently, images are published exclusively to GHCR (`ghcr.io/wikid82/charon`). This plan adds Docker Hub (`docker.io/wikid82/charon`) as an additional registry while maintaining full parity in tags, platforms, and supply chain security.
---
## 1. Current State Analysis
### 1.1 Existing Registry Setup (GHCR Only)
| Workflow | Purpose | Tags Generated | Platforms |
|----------|---------|----------------|-----------|
| `docker-build.yml` | Main builds on push/PR | `latest`, `dev`, `sha-*`, `pr-*`, `feature-*` | `linux/amd64`, `linux/arm64` |
| `nightly-build.yml` | Nightly builds from `nightly` branch | `nightly`, `nightly-YYYY-MM-DD`, `nightly-sha-*` | `linux/amd64`, `linux/arm64` |
| `release-goreleaser.yml` | Release builds on tag push | `vX.Y.Z` | N/A (binary releases, not Docker) |
### 1.2 Current Environment Variables
```yaml
# docker-build.yml (Line 25-28)
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/charon
```
### 1.3 Supply Chain Security Features
| Feature | Status | Implementation |
|---------|--------|----------------|
| **SBOM Generation** | ✅ Active | `anchore/sbom-action` → CycloneDX JSON |
| **SBOM Attestation** | ✅ Active | `actions/attest-sbom` → Push to registry |
| **Trivy Scanning** | ✅ Active | SARIF upload to GitHub Security |
| **Cosign Signing** | 🔶 Partial | Verification exists, signing not in docker-build.yml |
| **SLSA Provenance** | ⚠️ Not Implemented | `provenance: true` in Buildx but not verified |
### 1.4 Current Permissions
```yaml
# docker-build.yml (Lines 31-36)
permissions:
contents: read
packages: write
security-events: write
id-token: write # OIDC for signing
attestations: write # SBOM attestation
```
---
## 2. Docker Hub Setup
### 2.1 Required GitHub Secrets
| Secret Name | Description | Where to Get |
|-------------|-------------|--------------|
| `DOCKERHUB_USERNAME` | Docker Hub username | hub.docker.com → Account Settings |
| `DOCKERHUB_TOKEN` | Docker Hub Access Token | hub.docker.com → Account Settings → Security → New Access Token |
**Access Token Requirements:**
- Scope: `Read, Write, Delete` for automated pushes
- Name: e.g., `github-actions-charon`
### 2.2 Repository Naming
| Registry | Repository | Full Image Reference |
|----------|------------|---------------------|
| Docker Hub | `wikid82/charon` | `docker.io/wikid82/charon:latest` |
| GHCR | `wikid82/charon` | `ghcr.io/wikid82/charon:latest` |
**Note**: Docker Hub uses lowercase repository names. The GitHub repository owner is `Wikid82` (capital W), so we normalize to `wikid82`.
### 2.3 Docker Hub Repository Setup
1. Go to [hub.docker.com](https://hub.docker.com)
2. Click "Create Repository"
3. Name: `charon`
4. Visibility: Public
5. Description: "Web UI for managing Caddy reverse proxy configurations"
---
## 3. Workflow Modifications
### 3.1 Files to Modify
| File | Changes |
|------|---------|
| `.github/workflows/docker-build.yml` | Add Docker Hub login, multi-registry push |
| `.github/workflows/nightly-build.yml` | Add Docker Hub login, multi-registry push |
| `.github/workflows/supply-chain-verify.yml` | Verify Docker Hub signatures |
### 3.2 docker-build.yml Changes
#### 3.2.1 Update Environment Variables
**Location**: Lines 25-28
**Before**:
```yaml
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/charon
SYFT_VERSION: v1.17.0
GRYPE_VERSION: v0.85.0
```
**After**:
```yaml
env:
# Primary registry (GHCR)
GHCR_REGISTRY: ghcr.io
# Secondary registry (Docker Hub)
DOCKERHUB_REGISTRY: docker.io
# Image name (lowercase for Docker Hub compatibility)
IMAGE_NAME: wikid82/charon
SYFT_VERSION: v1.17.0
GRYPE_VERSION: v0.85.0
```
#### 3.2.2 Add Docker Hub Login Step
**Location**: After "Log in to Container Registry" step (around line 70)
**Add**:
```yaml
- name: Log in to Docker Hub
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
```
#### 3.2.3 Update Metadata Action for Multi-Registry
**Location**: Extract metadata step (around line 78)
**Before**:
```yaml
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
# ... rest of tags
```
**After**:
```yaml
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
type=ref,event=branch,enable=${{ startsWith(github.ref, 'refs/heads/feature/') }}
type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }}
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
flavor: |
latest=false
```
#### 3.2.4 Add Cosign Signing for Docker Hub
**Location**: After SBOM attestation step (around line 190)
**Add**:
```yaml
# Sign Docker Hub image with Cosign (keyless, OIDC-based)
- name: Install Cosign
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
- name: Sign GHCR Image with Cosign
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
COSIGN_EXPERIMENTAL: "true"
run: |
echo "Signing GHCR image with Cosign..."
cosign sign --yes ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}
- name: Sign Docker Hub Image with Cosign
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
COSIGN_EXPERIMENTAL: "true"
run: |
echo "Signing Docker Hub image with Cosign..."
cosign sign --yes ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}
```
#### 3.2.5 Attach SBOM to Docker Hub
**Location**: After existing SBOM attestation (around line 200)
**Add**:
```yaml
# Attach SBOM to Docker Hub image
- name: Attach SBOM to Docker Hub
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
run: |
echo "Attaching SBOM to Docker Hub image..."
cosign attach sbom --sbom sbom.cyclonedx.json \
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
```
### 3.3 nightly-build.yml Changes
Apply similar changes:
1. Add `DOCKERHUB_REGISTRY` environment variable
2. Add Docker Hub login step
3. Update metadata action with multiple images
4. Add Cosign signing for both registries
5. Attach SBOM to Docker Hub image
### 3.4 Complete docker-build.yml Diff Summary
```diff
env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository_owner }}/charon
+ GHCR_REGISTRY: ghcr.io
+ DOCKERHUB_REGISTRY: docker.io
+ IMAGE_NAME: wikid82/charon
SYFT_VERSION: v1.17.0
GRYPE_VERSION: v0.85.0
# ... in steps ...
- name: Log in to Container Registry
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Log in to Docker Hub
+ if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+ with:
+ registry: docker.io
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: |
+ ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
+ ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# ... tags unchanged ...
```
---
## 4. Supply Chain Security
### 4.1 Parity Matrix
| Feature | GHCR | Docker Hub |
|---------|------|------------|
| Multi-platform | ✅ `linux/amd64`, `linux/arm64` | ✅ Same |
| SBOM | ✅ Attestation | ✅ Attached via Cosign |
| Cosign Signature | ✅ Keyless OIDC | ✅ Keyless OIDC |
| Trivy Scan | ✅ SARIF to GitHub | ✅ Same SARIF |
| SLSA Provenance | 🔶 Buildx `provenance: true` | 🔶 Same |
### 4.2 Cosign Signing Strategy
Both registries will use **keyless signing** via OIDC (OpenID Connect):
- No private keys to manage
- Signatures tied to GitHub Actions identity
- Transparent logging to Sigstore Rekor
### 4.3 SBOM Attachment Strategy
**GHCR**: Uses `actions/attest-sbom` which creates an attestation linked to the image manifest.
**Docker Hub**: Uses `cosign attach sbom` to attach the SBOM as an OCI artifact.
### 4.4 Verification Commands
```bash
# Verify GHCR signature
cosign verify ghcr.io/wikid82/charon:latest \
--certificate-identity-regexp="https://github.com/Wikid82/Charon" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"
# Verify Docker Hub signature
cosign verify docker.io/wikid82/charon:latest \
--certificate-identity-regexp="https://github.com/Wikid82/Charon" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com"
# Download SBOM from Docker Hub
cosign download sbom docker.io/wikid82/charon:latest > sbom.json
```
---
## 5. Tag Strategy
### 5.1 Tag Parity Matrix
| Trigger | GHCR Tag | Docker Hub Tag |
|---------|----------|----------------|
| Push to `main` | `ghcr.io/wikid82/charon:latest` | `docker.io/wikid82/charon:latest` |
| Push to `development` | `ghcr.io/wikid82/charon:dev` | `docker.io/wikid82/charon:dev` |
| Push to `feature/*` | `ghcr.io/wikid82/charon:feature-*` | `docker.io/wikid82/charon:feature-*` |
| PR | `ghcr.io/wikid82/charon:pr-N` | ❌ Not pushed to Docker Hub |
| Release tag `vX.Y.Z` | `ghcr.io/wikid82/charon:X.Y.Z` | `docker.io/wikid82/charon:X.Y.Z` |
| SHA | `ghcr.io/wikid82/charon:sha-abc1234` | `docker.io/wikid82/charon:sha-abc1234` |
| Nightly | `ghcr.io/wikid82/charon:nightly` | `docker.io/wikid82/charon:nightly` |
### 5.2 PR Images
PR images (`pr-N`) are **not pushed to Docker Hub** to:
- Reduce Docker Hub storage/bandwidth usage
- Keep Docker Hub clean for production images
- PRs are internal development artifacts
---
## 6. Documentation Updates
### 6.1 README.md Changes
**Location**: Badge section (around line 13-22)
**Add Docker Hub badge**:
```markdown
<p align="center">
<!-- Existing badges -->
<a href="https://www.repostatus.org/#active"><img src="https://www.repostatus.org/badges/latest/active.svg" alt="Project Status: Active" /></a>
<a href="https://www.bestpractices.dev/projects/11648"><img src="https://www.bestpractices.dev/projects/11648/badge"></a>
<br>
<!-- Add Docker Hub badge -->
<a href="https://hub.docker.com/r/wikid82/charon"><img src="https://img.shields.io/docker/pulls/wikid82/charon.svg" alt="Docker Pulls"></a>
<a href="https://hub.docker.com/r/wikid82/charon"><img src="https://img.shields.io/docker/v/wikid82/charon?sort=semver" alt="Docker Version"></a>
<!-- Existing badges continue -->
<a href="https://codecov.io/gh/Wikid82/Charon" ><img src="https://codecov.io/gh/Wikid82/Charon/branch/main/graph/badge.svg?token=RXSINLQTGE" alt="Code Coverage"/></a>
<!-- ... -->
</p>
```
**Add to Installation section**:
```markdown
## Quick Start
### Docker Hub (Recommended)
\`\`\`bash
docker pull wikid82/charon:latest
docker run -d -p 80:80 -p 443:443 -p 8080:8080 \
-v charon-data:/app/data \
wikid82/charon:latest
\`\`\`
### GitHub Container Registry
\`\`\`bash
docker pull ghcr.io/wikid82/charon:latest
docker run -d -p 80:80 -p 443:443 -p 8080:8080 \
-v charon-data:/app/data \
ghcr.io/wikid82/charon:latest
\`\`\`
```
### 6.2 getting-started.md Changes
**Location**: Step 1 Install section
**Update docker-compose.yml example**:
```yaml
services:
charon:
# Docker Hub (recommended for most users)
image: wikid82/charon:latest
# Alternative: GitHub Container Registry
# image: ghcr.io/wikid82/charon:latest
container_name: charon
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- ./charon-data:/app/data
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CHARON_ENV=production
```
### 6.3 Docker Hub README Sync
Create a workflow or use Docker Hub's "Build Settings" to sync the README:
**Option A**: Manual sync via Docker Hub API (in workflow)
```yaml
- name: Sync README to Docker Hub
if: github.ref == 'refs/heads/main'
uses: peter-evans/dockerhub-description@0e6a7b2f56b498411d884fc55f14e1e2caf38d24 # v4.0.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: wikid82/charon
readme-filepath: ./README.md
short-description: "Web UI for managing Caddy reverse proxy configurations"
```
**Option B**: Create a dedicated Docker Hub README at `docs/docker-hub-readme.md`
---
## 7. File Change Review
### 7.1 .gitignore
**No changes required.** Current `.gitignore` is comprehensive.
### 7.2 codecov.yml
**No changes required.** Docker image publishing doesn't affect code coverage.
### 7.3 .dockerignore
**No changes required.** Current `.dockerignore` is comprehensive and well-organized.
### 7.4 Dockerfile
**No changes required.** The Dockerfile is registry-agnostic. Labels are already configured:
```dockerfile
LABEL org.opencontainers.image.source="https://github.com/Wikid82/charon" \
org.opencontainers.image.url="https://github.com/Wikid82/charon" \
org.opencontainers.image.vendor="charon" \
```
---
## 8. Implementation Checklist
### Phase 1: Docker Hub Setup (Manual)
- [ ] **1.1** Create Docker Hub account (if not exists)
- [ ] **1.2** Create `wikid82/charon` repository on Docker Hub
- [ ] **1.3** Generate Docker Hub Access Token
- [ ] **1.4** Add `DOCKERHUB_USERNAME` secret to GitHub repository
- [ ] **1.5** Add `DOCKERHUB_TOKEN` secret to GitHub repository
### Phase 2: Workflow Updates
- [ ] **2.1** Update `docker-build.yml` environment variables
- [ ] **2.2** Add Docker Hub login step to `docker-build.yml`
- [ ] **2.3** Update metadata action for multi-registry in `docker-build.yml`
- [ ] **2.4** Add Cosign signing steps to `docker-build.yml`
- [ ] **2.5** Add SBOM attachment step to `docker-build.yml`
- [ ] **2.6** Apply same changes to `nightly-build.yml`
- [ ] **2.7** Add README sync step (optional)
### Phase 3: Documentation
- [ ] **3.1** Add Docker Hub badge to `README.md`
- [ ] **3.2** Update Quick Start section in `README.md`
- [ ] **3.3** Update `docs/getting-started.md` with Docker Hub examples
- [ ] **3.4** Create Docker Hub-specific README (optional)
### Phase 4: Verification
- [ ] **4.1** Push to `development` branch and verify both registries receive image
- [ ] **4.2** Verify tags are identical on both registries
- [ ] **4.3** Verify Cosign signatures on both registries
- [ ] **4.4** Verify SBOM attachment on Docker Hub
- [ ] **4.5** Pull image from Docker Hub and run basic smoke test
- [ ] **4.6** Create test release tag and verify version tags
### Phase 5: Monitoring
- [ ] **5.1** Set up Docker Hub vulnerability scanning (Settings → Vulnerability Scanning)
- [ ] **5.2** Monitor Docker Hub download metrics
---
## 9. Rollback Plan
If issues occur with Docker Hub publishing:
1. **Immediate**: Remove Docker Hub login step from workflow
2. **Revert**: Use `git revert` on the workflow changes
3. **Secrets**: Secrets can remain (they're not exposed)
4. **Docker Hub Repo**: Can remain (no harm in empty repo)
---
## 10. Security Considerations
### 10.1 Secret Management
| Secret | Rotation Policy | Access Level |
|--------|-----------------|--------------|
| `DOCKERHUB_TOKEN` | Every 90 days | Read/Write/Delete |
| `GITHUB_TOKEN` | Auto-rotated | Built-in |
### 10.2 Supply Chain Risks
| Risk | Mitigation |
|------|------------|
| Compromised Docker Hub credentials | Use access tokens (not password), enable 2FA |
| Image tampering | Cosign signatures verify integrity |
| Dependency confusion | SBOM provides transparency |
| Malicious base image | Pin base images by digest in Dockerfile |
---
## 11. Cost Analysis
### Docker Hub Free Tier Limits
| Resource | Limit | Expected Usage |
|----------|-------|----------------|
| Private repos | 1 | 0 (public repo) |
| Pulls | Unlimited for public | N/A |
| Builds | Disabled (we use GitHub Actions) | 0 |
| Teams | 1 | 1 |
**Conclusion**: No cost impact expected for public repository.
---
## 12. References
- [docker/login-action](https://github.com/docker/login-action)
- [docker/metadata-action - Multiple registries](https://github.com/docker/metadata-action#extracting-to-multiple-registries)
- [Cosign keyless signing](https://docs.sigstore.dev/cosign/keyless/)
- [Cosign attach sbom](https://docs.sigstore.dev/cosign/signing/other_types/#sbom)
- [peter-evans/dockerhub-description](https://github.com/peter-evans/dockerhub-description)
- [Docker Hub Access Tokens](https://docs.docker.com/docker-hub/access-tokens/)
---
## 13. Appendix: Full Workflow YAML
### 13.1 Updated docker-build.yml (Complete)
See the detailed diff in Section 3.4. The full updated workflow should be generated during implementation.
### 13.2 Example Multi-Registry Push Output
```
#12 pushing ghcr.io/wikid82/charon:latest with docker
#12 pushing layer sha256:abc123... 0.2s
#12 pushing manifest sha256:xyz789... done
#12 pushing ghcr.io/wikid82/charon:sha-abc1234 with docker
#12 done
#13 pushing docker.io/wikid82/charon:latest with docker
#13 pushing layer sha256:abc123... 0.2s
#13 pushing manifest sha256:xyz789... done
#13 pushing docker.io/wikid82/charon:sha-abc1234 with docker
#13 done
```
---
**End of Plan**