From e2adb3bc9f208e1145f4e780d71f03cfa5d81f80 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Thu, 9 Apr 2026 00:25:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=B3=20feat:=20implement=20Docker=20mul?= =?UTF-8?q?ti-stage=20build=20with=20caching=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added Docker build infrastructure: - Multi-stage build (builder, cache, production) - Dependency hashing for cache invalidation - GNU tar support for cache compatibility - Production and development Dockerfiles - Docker Compose for local development Build Optimization: - Dependency-based cache keys - Layer caching strategy - Cross-platform compatibility - Gitea Actions cache integration Files Added: - docker/Dockerfile.build - Build environment - docker/Dockerfile.prod - Production image - docker/Dockerfile.prod.template - Template-based generation - docker-compose.yml - Development setup - scripts/calculate-deps-hash.sh - Cache key calculation - scripts/test-docker-cache.sh - Cache testing Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- docker-compose.yml | 47 +++++++ Dockerfile => docker/Dockerfile | 2 +- docker/Dockerfile.build | 43 ++++++ docker/Dockerfile.prod | 37 +++++ docker/Dockerfile.prod.template | 36 +++++ scripts/calculate-deps-hash.sh | 20 +++ scripts/test-build-cache-environment.sh | 179 ++++++++++++++++++++++++ scripts/test-docker-cache.sh | 86 ++++++++++++ 8 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml rename Dockerfile => docker/Dockerfile (97%) create mode 100644 docker/Dockerfile.build create mode 100644 docker/Dockerfile.prod create mode 100644 docker/Dockerfile.prod.template create mode 100755 scripts/calculate-deps-hash.sh create mode 100755 scripts/test-build-cache-environment.sh create mode 100755 scripts/test-docker-cache.sh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70339e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + postgres: + image: postgres:16-alpine + container_name: dance-lessons-coach-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dance_lessons_coach + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - dance-lessons-coach-network + restart: unless-stopped + + # Application service (for reference) + # app: + # build: . + # container_name: dance-lessons-coach-app + # ports: + # - "8080:8080" + # environment: + # - DLC_DATABASE_HOST=postgres + # - DLC_DATABASE_PORT=5432 + # - DLC_DATABASE_USER=postgres + # - DLC_DATABASE_PASSWORD=postgres + # - DLC_DATABASE_NAME=dance_lessons_coach + # - DLC_DATABASE_SSL_MODE=disable + # depends_on: + # postgres: + # condition: service_healthy + # restart: unless-stopped + +volumes: + postgres_data: + driver: local + +networks: + dance-lessons-coach-network: + name: dance-lessons-coach-network + driver: bridge \ No newline at end of file diff --git a/Dockerfile b/docker/Dockerfile similarity index 97% rename from Dockerfile rename to docker/Dockerfile index ceaa438..32de2c1 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -# DanceLessonsCoach Docker Image +# dance-lessons-coach Docker Image # Multi-stage build for production deployment # Stage 1: Build binary diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build new file mode 100644 index 0000000..1d3ce86 --- /dev/null +++ b/docker/Dockerfile.build @@ -0,0 +1,43 @@ +# Build environment Dockerfile with pre-installed Go tools and dependencies +# Optimized for CI/CD pipeline speed +# Updated to include Node.js for GitHub Actions compatibility + +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 \ + nodejs \ + npm \ + postgresql-client \ + tar # Add GNU tar for cache compatibility + +# Set up Go environment +ENV GOPATH=/go +ENV PATH=$GOPATH/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin +WORKDIR /go/src/dance-lessons-coach + +# Install common Go tools +RUN go install github.com/swaggo/swag/cmd/swag@latest && \ + go install golang.org/x/tools/cmd/goimports@latest && \ + go install honnef.co/go/tools/cmd/staticcheck@latest + +# Copy only go.mod and go.sum first for dependency caching +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +# Simple build environment - source code is mounted at runtime +WORKDIR /workspace + +# Pre-download common Go tools (already installed in base) +# RUN go install github.com/swaggo/swag/cmd/swag@latest \ No newline at end of file diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod new file mode 100644 index 0000000..63e433f --- /dev/null +++ b/docker/Dockerfile.prod @@ -0,0 +1,37 @@ +# dance-lessons-coach Production Docker Image +# ⚠️ DEVELOPMENT ONLY - This file uses 'latest' tag for local testing +# ⚠️ CI/CD generates the correct Dockerfile.prod with proper dependency hash +# ⚠️ For production use, see the CI/CD workflow which generates the correct file + +# Use the build cache image as base (latest for local dev only) +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/docker/Dockerfile.prod.template b/docker/Dockerfile.prod.template new file mode 100644 index 0000000..4413aa6 --- /dev/null +++ b/docker/Dockerfile.prod.template @@ -0,0 +1,36 @@ +# dance-lessons-coach Production Docker Image +# Minimal image using pre-built binary from CI cache +# Template: Replace {{DEPS_HASH}} with actual dependency hash + +# Use the build cache image as base +FROM gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:{{DEPS_HASH}} 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/scripts/calculate-deps-hash.sh b/scripts/calculate-deps-hash.sh new file mode 100755 index 0000000..c5af319 --- /dev/null +++ b/scripts/calculate-deps-hash.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Calculate dependency hash for Docker cache tag +# This script calculates the hash used for the build cache image tag + +# Calculate hash of go.mod + go.sum +# Use shasum on macOS, sha256sum on Linux +if command -v sha256sum >/dev/null 2>&1; then + DEPS_HASH=$(sha256sum go.mod go.sum | sha256sum | cut -d' ' -f1 | head -c 12) +else + DEPS_HASH=$(shasum -a 256 go.mod go.sum | shasum -a 256 | cut -d' ' -f1 | head -c 12) +fi + +echo "Dependency hash: $DEPS_HASH" +echo "$DEPS_HASH" + +# Export for use in other scripts +if [ -n "$1" ]; then + echo "DEPS_HASH=$DEPS_HASH" > "$1" + echo "Exported to: $1" +fi \ No newline at end of file diff --git a/scripts/test-build-cache-environment.sh b/scripts/test-build-cache-environment.sh new file mode 100755 index 0000000..e5f0f92 --- /dev/null +++ b/scripts/test-build-cache-environment.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# Test the build cache environment without local Go installation +# This simulates the Gitea act runner environment + +set -e + +echo "🧪 Testing Build Cache Environment" +echo "==================================" +echo "" + +# 1. Calculate dependency hash +echo "1. Calculating dependency hash..." +DEPS_HASH=$(./scripts/calculate-deps-hash.sh) +echo "✅ Dependency hash: $DEPS_HASH" +echo "" + +# 2. Build the build cache image +echo "2. Building build cache image..." +if command -v docker >/dev/null 2>&1; then + docker build -t dance-lessons-coach-build-cache:$DEPS_HASH -f docker/Dockerfile.build . +else + echo "❌ Docker not found" + exit 1 +fi +echo "✅ Build cache image built: dance-lessons-coach-build-cache:$DEPS_HASH" +echo "" + +# 3. Test Go environment inside the container +echo "3. Testing Go environment inside container..." +docker run --rm dance-lessons-coach-build-cache:$DEPS_HASH sh -c "go version" +docker run --rm dance-lessons-coach-build-cache:$DEPS_HASH sh -c "which swag" +echo "✅ Go and swag available in container" +echo "" + +# 4. Test Swagger generation +echo "4. Testing Swagger generation..." +docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:$DEPS_HASH sh -c "cd pkg/server && go generate" +if [ -f "pkg/server/docs/swagger.json" ]; then + echo "✅ Swagger documentation generated successfully" +else + echo "❌ Swagger documentation generation failed" + exit 1 +fi +echo "" + +# 5. Test Go build +echo "5. Testing Go build..." +docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:$DEPS_HASH sh -c "go build ./..." +echo "✅ Go build successful" +echo "" + +# 6. Test Go test +echo "6. Testing Go test..." +docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:$DEPS_HASH sh -c "go test ./... -v" +echo "✅ Go tests passed" +echo "" + +# 7. Test binary build +echo "7. Testing binary build..." +docker run --rm -v "$(pwd):/workspace" -w /workspace dance-lessons-coach-build-cache:$DEPS_HASH sh -c "go build -o /workspace/dance-lessons-coach ./cmd/server" +if [ -f "dance-lessons-coach" ]; then + echo "✅ Binary built successfully" + ls -la dance-lessons-coach + rm dance-lessons-coach +else + echo "❌ Binary build failed" + exit 1 +fi +echo "" + +# 8. Test production Dockerfile with the cache +echo "8. Testing production Dockerfile..." +# First, let's create a temporary Dockerfile.prod with the correct hash +TEMP_DOCKERFILE="Dockerfile.prod.test" +cat > "$TEMP_DOCKERFILE" << EOF +# dance-lessons-coach Production Docker Image +# Minimal image using pre-built binary from CI cache + +# Use the build cache image as base +FROM dance-lessons-coach-build-cache:$DEPS_HASH 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"] +EOF + +echo "✅ Created temporary production Dockerfile with correct hash" +echo "" + +# 9. Build production image +echo "9. Building production image..." +docker build -t dance-lessons-coach-prod:$DEPS_HASH -f "$TEMP_DOCKERFILE" . +echo "✅ Production image built: dance-lessons-coach-prod:$DEPS_HASH" +echo "" + +# 10. Test production image +echo "10. Testing production image..." +docker run -d -p 8081:8080 --name test-prod-container dance-lessons-coach-prod:$DEPS_HASH +sleep 5 + +# Test health endpoint +if curl -s http://localhost:8081/api/health | grep -q "healthy"; then + echo "✅ Production container is healthy" +else + echo "❌ Production container health check failed" + docker logs test-prod-container + docker stop test-prod-container + docker rm test-prod-container + rm "$TEMP_DOCKERFILE" + exit 1 +fi + +# Test greet endpoint +if curl -s http://localhost:8081/api/v1/greet/ | grep -q "Hello"; then + echo "✅ Production container greet endpoint working" +else + echo "❌ Production container greet endpoint failed" + docker logs test-prod-container + docker stop test-prod-container + docker rm test-prod-container + rm "$TEMP_DOCKERFILE" + exit 1 +fi + +echo "✅ Production container is working correctly" +echo "" + +# Clean up +echo "11. Cleaning up..." +docker stop test-prod-container > /dev/null 2>&1 || true +docker rm test-prod-container > /dev/null 2>&1 || true +rm "$TEMP_DOCKERFILE" +echo "✅ Cleanup complete" +echo "" + +echo "🎉 All tests passed!" +echo "===================" +echo "" +echo "✅ Build cache environment is working correctly" +echo "✅ All Go tools available in container" +echo "✅ Swagger generation works" +echo "✅ Go build and test work" +echo "✅ Production Dockerfile works with cache" +echo "✅ Production container runs successfully" +echo "" +echo "🚀 The build cache is ready for CI/CD use!" +echo "" +echo "💡 To use this in CI/CD:" +echo " 1. The build-cache job will build: dance-lessons-coach-build-cache:$DEPS_HASH" +echo " 2. The CI pipeline will use: docker run dance-lessons-coach-build-cache:$DEPS_HASH ..." +echo " 3. Production build will use: FROM dance-lessons-coach-build-cache:$DEPS_HASH AS builder" +echo "" +echo "📊 Dependency hash for this test: $DEPS_HASH" \ No newline at end of file diff --git a/scripts/test-docker-cache.sh b/scripts/test-docker-cache.sh new file mode 100755 index 0000000..062350c --- /dev/null +++ b/scripts/test-docker-cache.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Test Docker build cache functionality locally +# Usage: scripts/test-docker-cache.sh + +set -e + +echo "🧪 Testing Docker Build Cache" +echo "============================" +echo "" + +# Check requirements +if ! command -v docker >/dev/null 2>&1; then + echo "❌ Docker not found. Please install Docker first." + exit 1 +fi + +if ! command -v go >/dev/null 2>&1; then + echo "❌ Go not found. Please install Go 1.26.1+." + exit 1 +fi + +echo "✅ Requirements met" +echo "" + +# 1. Calculate dependency hash (same as CI) +echo "1. Calculating dependency hash..." +# Use shasum on macOS, sha256sum on Linux +if command -v sha256sum >/dev/null 2>&1; then + DEPS_HASH=$(sha256sum go.mod go.sum | sha256sum | cut -d' ' -f1 | head -c 12) +else + DEPS_HASH=$(shasum -a 256 go.mod go.sum | shasum -a 256 | cut -d' ' -f1 | head -c 12) +fi +echo " Dependency hash: $DEPS_HASH" +echo "" + +# 2. Build Docker cache image +echo "2. Building Docker cache image..." +IMAGE_NAME="dance-lessons-coach-build-cache:$DEPS_HASH" +echo " Image name: $IMAGE_NAME" + +docker build -t "$IMAGE_NAME" -f Dockerfile.build . +echo "✅ Docker image built successfully" +echo "" + +# 3. Test running commands in Docker +echo "3. Testing Docker execution..." + +echo " Testing 'go version'..." +docker run --rm -v "$(pwd):/workspace" -w /workspace "$IMAGE_NAME" go version +echo " ✅ Go version command works" + +echo " Testing 'go build'..." +docker run --rm -v "$(pwd):/workspace" -w /workspace "$IMAGE_NAME" go build -o /tmp/test ./cmd/greet +echo " ✅ Go build command works" + +echo " Testing 'swag' availability..." +docker run --rm -v "$(pwd):/workspace" -w /workspace "$IMAGE_NAME" swag --version || echo " ⚠️ Swag not available" +echo "" + +# 4. Performance comparison +echo "4. Performance comparison..." + +echo " Running 'go build' natively..." +START=$(date +%s%N) +go build -o /tmp/native-test ./cmd/greet > /dev/null 2>&1 +NATIVE_TIME=$((($(date +%s%N) - $START)/1000000)) +echo " Native build: ${NATIVE_TIME}ms" + +echo " Running 'go build' in Docker..." +START=$(date +%s%N) +docker run --rm -v "$(pwd):/workspace" -w /workspace "$IMAGE_NAME" go build -o /tmp/docker-test ./cmd/greet > /dev/null 2>&1 +DOCKER_TIME=$((($(date +%s%N) - $START)/1000000)) +echo " Docker build: ${DOCKER_TIME}ms" + +echo " Overhead: $((DOCKER_TIME - NATIVE_TIME))ms" +echo "" + +# Clean up +rm -f /tmp/native-test /tmp/docker-test + +echo "✅ Docker cache testing complete!" +echo "" +echo "💡 The Docker image is ready for CI use." +echo "💡 Push this image to your registry for CI caching:" +echo " docker tag $IMAGE_NAME your-registry/$IMAGE_NAME" +echo " docker push your-registry/$IMAGE_NAME" \ No newline at end of file