From 3029e931757c61b6508f88bc3afd7628ed519683 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 7 Apr 2026 11:52:33 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20ci:=20optimize=20CI/CD=20with=20?= =?UTF-8?q?Docker=20cache=20and=20remove=20Buildx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci-cd.yaml | 57 +++------------ Dockerfile.prod | 35 +++++++++ adr/0020-docker-build-strategy.md | 113 +++++++++++++++++++++++++++++- 3 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 Dockerfile.prod diff --git a/.gitea/workflows/ci-cd.yaml b/.gitea/workflows/ci-cd.yaml index 180db6c..817ae24 100644 --- a/.gitea/workflows/ci-cd.yaml +++ b/.gitea/workflows/ci-cd.yaml @@ -143,9 +143,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Gitea Container Registry uses: docker/login-action@v3 with: @@ -162,72 +159,40 @@ jobs: if docker pull "$IMAGE_NAME" >/dev/null 2>&1; then echo "✅ Using Docker build cache" echo "CACHE_AVAILABLE=true" >> $GITHUB_ENV + echo "CACHE_IMAGE=$IMAGE_NAME" >> $GITHUB_ENV else echo "⚠️ Building without cache (first run or new dependencies)" echo "CACHE_AVAILABLE=false" >> $GITHUB_ENV fi - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.26.1' - cache: true - - - name: Install dependencies - run: go mod tidy - - # SINGLE swag installation - reused for all steps - - name: Install swag (once) - run: go install github.com/swaggo/swag/cmd/swag@latest - - - name: Version bump (main branch only) - if: github.ref == 'refs/heads/main' - run: | - # Analyze last commit message - LAST_COMMIT=$(git log -1 --pretty=%B | head -1) - - # Automatic version bump based on commit type - if echo "$LAST_COMMIT" | grep -q "^✨ feat:"; then - echo "🎯 Feature commit detected - bumping MINOR version" - ./scripts/version-bump.sh minor - elif echo "$LAST_COMMIT" | grep -q "^🐛 fix:"; then - echo "🐛 Fix commit detected - bumping PATCH version" - ./scripts/version-bump.sh patch - elif echo "$LAST_COMMIT" | grep -q "BREAKING CHANGE"; then - echo "💥 Breaking change detected - bumping MAJOR version" - ./scripts/version-bump.sh major - else - echo "⏭️ No automatic version bump needed" - fi - - - name: Generate Swagger Docs + - name: Generate Swagger Docs using Docker cache run: | if [ "${{ env.CACHE_AVAILABLE }}" = "true" ]; then echo "Running in Docker cache..." - docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:latest sh -c "cd pkg/server && go generate" + docker run --rm -v "$(pwd):/workspace" -w /workspace ${{ env.CACHE_IMAGE }} sh -c "cd pkg/server && go generate" else echo "Running natively..." cd pkg/server && go generate fi - - name: Build all packages + - name: Build all packages using Docker cache run: | if [ "${{ env.CACHE_AVAILABLE }}" = "true" ]; then echo "Running in Docker cache..." - docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:latest sh -c "go build ./..." + docker run --rm -v "$(pwd):/workspace" -w /workspace ${{ env.CACHE_IMAGE }} sh -c "go build ./..." else echo "Running natively..." go build ./... fi - - name: Run tests with coverage + - name: Run tests with coverage using Docker cache run: | if [ "${{ env.CACHE_AVAILABLE }}" = "true" ]; then echo "Running in Docker cache..." docker run --rm \ -v "$(pwd):/workspace" \ -w /workspace \ - dance-lessons-coach-build-cache:latest \ + ${{ env.CACHE_IMAGE }} \ sh -c "go test ./... -coverprofile=coverage.out -v && go tool cover -func=coverage.out > coverage.txt" else echo "Running natively..." @@ -271,10 +236,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.PACKAGES_TOKEN }} - - name: Set up Docker Buildx - if: github.ref == 'refs/heads/main' - uses: docker/setup-buildx-action@v3 - - name: Build and push Docker image if: github.ref == 'refs/heads/main' run: | @@ -283,7 +244,9 @@ jobs: TAGS="$IMAGE_VERSION latest ${{ github.sha }}" echo "Building Docker image with tags: $TAGS" - docker build -t dance-lessons-coach . + + # Use the production Dockerfile that leverages the build cache + docker build -t dance-lessons-coach -f Dockerfile.prod . for TAG in $TAGS; do IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG" diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..c08c5b5 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,35 @@ +# DanceLessonsCoach Production Docker Image +# Minimal image using pre-built binary from CI cache + +# Use the build cache image as base +FROM gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:latest AS builder + +# Final minimal image +FROM alpine:3.18 + +WORKDIR /app + +# Install minimal dependencies +RUN apk add --no-cache ca-certificates tzdata + +# Copy binary from builder +COPY --from=builder /workspace/dance-lessons-coach /app/dance-lessons-coach + +# Copy configuration +COPY config.yaml /app/config.yaml + +# Set permissions +RUN chmod +x /app/dance-lessons-coach + +# Set timezone +ENV TZ=UTC + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget -q --spider http://localhost:8080/api/health || exit 1 + +# Entry point +ENTRYPOINT ["/app/dance-lessons-coach"] \ No newline at end of file diff --git a/adr/0020-docker-build-strategy.md b/adr/0020-docker-build-strategy.md index ed92bc2..c7cedb5 100644 --- a/adr/0020-docker-build-strategy.md +++ b/adr/0020-docker-build-strategy.md @@ -31,12 +31,14 @@ This approach is simpler, more reliable, and works consistently with self-signed ## Decision -**Replace Docker Buildx with traditional docker build + push** for the CI/CD pipeline. +**Replace Docker Buildx with traditional docker build + push** for the CI/CD pipeline and implement a two-stage Docker build strategy. ### Implementation +#### 1. Build Cache Strategy + ```yaml -# New approach +# Build cache using traditional docker build - name: Build and push Docker cache image if: steps.check_cache.outputs.cache_hit == 'false' run: | @@ -55,14 +57,121 @@ This approach is simpler, more reliable, and works consistently with self-signed echo "✅ Build cache image pushed successfully" ``` +#### 2. Production Build Strategy + +```yaml +# Production build using Dockerfile.prod +- name: Build and push Docker image + if: github.ref == 'refs/heads/main' + run: | + source VERSION + IMAGE_VERSION="$MAJOR.$MINOR.$PATCH${PRERELEASE:+-$PRERELEASE}" + + TAGS="$IMAGE_VERSION latest ${{ github.sha }}" + echo "Building Docker image with tags: $TAGS" + + # Use the production Dockerfile that leverages the build cache + docker build -t dance-lessons-coach -f Dockerfile.prod . + + for TAG in $TAGS; do + IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG" + echo "Tagging and pushing: $IMAGE_NAME" + docker tag dance-lessons-coach "$IMAGE_NAME" + docker push "$IMAGE_NAME" + done +``` + +#### 3. Dockerfile Structure + +**Dockerfile.build** - Build environment with all dependencies: +```dockerfile +FROM golang:1.26.1-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git bash curl make gcc musl-dev bc grep sed jq ca-certificates + +# Install Go tools +RUN go install github.com/swaggo/swag/cmd/swag@latest + +# Copy and verify dependencies +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +WORKDIR /workspace +``` + +**Dockerfile.prod** - Minimal production image: +```dockerfile +# Use the build cache image as base +FROM gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:latest AS builder + +# Final minimal image +FROM alpine:3.18 + +WORKDIR /app + +# Install minimal dependencies +RUN apk add --no-cache ca-certificates tzdata + +# Copy binary from builder +COPY --from=builder /workspace/dance-lessons-coach /app/dance-lessons-coach + +# Copy configuration +COPY config.yaml /app/config.yaml + +# Set permissions and entrypoint +RUN chmod +x /app/dance-lessons-coach +ENV TZ=UTC +EXPOSE 8080 +ENTRYPOINT ["/app/dance-lessons-coach"] +``` + +**Dockerfile** - Development Dockerfile (kept for local development): +```dockerfile +# Multi-stage build for development +FROM golang:1.26.1-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . ./ +RUN go build -o /dance-lessons-coach ./cmd/server + +FROM alpine:3.18 +WORKDIR /app +RUN apk add --no-cache ca-certificates tzdata +COPY --from=builder /dance-lessons-coach /app/dance-lessons-coach +COPY config.yaml /app/config.yaml +RUN chmod +x /app/dance-lessons-coach +ENV TZ=UTC +EXPOSE 8080 +ENTRYPOINT ["/app/dance-lessons-coach"] +``` + ## Benefits +### CI/CD Pipeline Benefits + 1. **Simplicity**: Traditional approach is easier to understand and debug 2. **Reliability**: Consistent behavior across different environments 3. **Certificate Handling**: Works seamlessly with self-signed certificates 4. **Performance**: Faster execution without Buildx overhead 5. **Compatibility**: Better compatibility with GitHub Actions environment +### Two-Stage Build Benefits + +1. **Separation of Concerns**: Clear separation between build environment and production runtime +2. **Optimized Production Image**: Minimal Alpine-based image with only necessary dependencies +3. **Reusable Build Cache**: Build environment can be reused across multiple CI runs +4. **Faster CI Execution**: Pre-built build cache reduces CI execution time +5. **Consistent Builds**: All builds use the same build environment + +### Development vs Production Clarity + +1. **Development Dockerfile**: Full build environment for local development +2. **Production Dockerfile**: Minimal runtime environment for deployment +3. **Build Cache Dockerfile**: Optimized build environment for CI/CD +4. **Clear Documentation**: Each Dockerfile has a specific purpose + ## Trade-offs ### What We Lose