From 424647cd939d53004ad67263e487aeea9abc72ab Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 24 Jan 2026 19:29:01 -0800 Subject: [PATCH] Use Tailscale sidecar for container registry push Docker Desktop's VM can't resolve tailnet hostnames. Work around this by: 1. Starting a Tailscale container that joins the tailnet 2. Building the image with docker build 3. Saving to tarball with docker save 4. Pushing via skopeo inside the Tailscale container Uses TS_CI_GATEWAY_AUTHKEY repository secret for authentication. Co-Authored-By: Claude Opus 4.5 --- .forgejo/actions/build-push-image/action.yaml | 96 +++++++++++++++++-- .forgejo/workflows/build-container.yaml | 1 + 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/.forgejo/actions/build-push-image/action.yaml b/.forgejo/actions/build-push-image/action.yaml index ac6f711..b96ab17 100644 --- a/.forgejo/actions/build-push-image/action.yaml +++ b/.forgejo/actions/build-push-image/action.yaml @@ -1,8 +1,15 @@ name: 'Build and Push Image' -description: 'Build a container image with Docker and push to zot registry' +description: 'Build a container image with Docker and push to zot registry via Tailscale sidecar' -# TODO: Investigate zot tag immutability to prevent overwriting released versions -# See: https://zotregistry.dev/v2.1.1/articles/immutable-tags/ +# Uses a Tailscale sidecar container to push images to the registry. +# This works around Docker Desktop's VM not having access to the tailnet. +# +# Flow: +# 1. Start Tailscale sidecar container (joins tailnet) +# 2. Build image with docker build +# 3. Save image to OCI tarball +# 4. Push tarball to registry using skopeo (via Tailscale network) +# 5. Cleanup sidecar inputs: context: @@ -22,26 +29,97 @@ inputs: description: 'Registry URL' required: false default: 'registry.tail8d86e.ts.net' + tailscale_authkey: + description: 'Tailscale OAuth client secret for ci-gateway' + required: true runs: using: 'composite' steps: + - name: Start Tailscale sidecar + shell: bash + run: | + echo "Starting Tailscale sidecar..." + + # Clean up any existing sidecar + docker rm -f ts-ci-gateway 2>/dev/null || true + + # Start Tailscale container + docker run -d \ + --name ts-ci-gateway \ + --hostname ci-gateway \ + -e TS_AUTHKEY="${{ inputs.tailscale_authkey }}" \ + -e TS_EXTRA_ARGS="--advertise-tags=tag:ci-gateway" \ + -e TS_ACCEPT_DNS=true \ + -e TS_USERSPACE=true \ + tailscale/tailscale:latest + + # Wait for Tailscale to connect + echo "Waiting for Tailscale to connect..." + for i in {1..30}; do + if docker exec ts-ci-gateway tailscale status >/dev/null 2>&1; then + echo "Tailscale connected!" + docker exec ts-ci-gateway tailscale status + break + fi + echo " Attempt $i/30..." + sleep 2 + done + + # Verify DNS resolution + echo "Testing DNS resolution..." + docker exec ts-ci-gateway nslookup ${{ inputs.registry }} || echo "DNS test failed, continuing anyway..." + - name: Build image with Docker shell: bash run: | echo "Building ${{ inputs.image_name }}:${{ inputs.version }}" docker build \ - --tag ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }} \ - --tag ${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }} \ + --tag ${{ inputs.image_name }}:${{ inputs.version }} \ + --tag ${{ inputs.image_name }}:${{ github.sha }} \ --file ${{ inputs.context }}/${{ inputs.dockerfile }} \ ${{ inputs.context }} - - name: Push to registry + - name: Save image to tarball shell: bash run: | - echo "Pushing ${{ inputs.image_name }}:${{ inputs.version }}" - docker push ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }} - docker push ${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }} + echo "Saving image to tarball..." + mkdir -p /tmp/ci-images + docker save ${{ inputs.image_name }}:${{ inputs.version }} -o /tmp/ci-images/image.tar + ls -lh /tmp/ci-images/image.tar + + - name: Push to registry via Tailscale + shell: bash + run: | + echo "Pushing ${{ inputs.image_name }}:${{ inputs.version }} via Tailscale sidecar..." + + # Copy tarball into the Tailscale container + docker cp /tmp/ci-images/image.tar ts-ci-gateway:/tmp/image.tar + + # Install skopeo in the Tailscale container and push + docker exec ts-ci-gateway sh -c ' + apk add --no-cache skopeo >/dev/null 2>&1 + + echo "Pushing version tag..." + skopeo copy \ + --dest-tls-verify=false \ + docker-archive:/tmp/image.tar \ + docker://${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }} + + echo "Pushing SHA tag..." + skopeo copy \ + --dest-tls-verify=false \ + docker-archive:/tmp/image.tar \ + docker://${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }} + ' + + - name: Cleanup + shell: bash + if: always() + run: | + echo "Cleaning up..." + docker rm -f ts-ci-gateway 2>/dev/null || true + rm -rf /tmp/ci-images - name: Summary shell: bash diff --git a/.forgejo/workflows/build-container.yaml b/.forgejo/workflows/build-container.yaml index 60771fe..6ed1be8 100644 --- a/.forgejo/workflows/build-container.yaml +++ b/.forgejo/workflows/build-container.yaml @@ -74,3 +74,4 @@ jobs: context: ${{ steps.check.outputs.context }} image_name: blumeops/${{ steps.parse.outputs.container }} version: ${{ steps.parse.outputs.version }} + tailscale_authkey: ${{ secrets.TS_CI_GATEWAY_AUTHKEY }}