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

19 KiB

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

# 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

# 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
  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:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository_owner }}/charon
  SYFT_VERSION: v1.17.0
  GRYPE_VERSION: v0.85.0

After:

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:

      - 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:

      - 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:

      - 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:

      # 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:

      # 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

 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

# 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:

<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:

## 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:

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)

      - 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:

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


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