diff --git a/.gitea/workflows/ci-cd.yaml b/.gitea/workflows/ci-cd.yaml index a4efe52..e6e5a95 100644 --- a/.gitea/workflows/ci-cd.yaml +++ b/.gitea/workflows/ci-cd.yaml @@ -109,7 +109,7 @@ jobs: # Build the image using traditional docker build docker build \ - --file Dockerfile.build \ + --file docker/Dockerfile.build \ --tag "$IMAGE_NAME" \ . @@ -232,7 +232,7 @@ jobs: echo "Using dependency hash: $DEPS_HASH" # Create Dockerfile.prod with the correct cache image tag - cat > Dockerfile.prod << EOF + cat > docker/Dockerfile.prod << EOF # DanceLessonsCoach Production Docker Image # Generated by CI/CD pipeline with dependency hash: $DEPS_HASH @@ -274,7 +274,7 @@ jobs: echo "Building Docker image with tags: $TAGS" # Build the production image - docker build -t dance-lessons-coach -f Dockerfile.prod . + docker build -t dance-lessons-coach -f docker/Dockerfile.prod . for TAG in $TAGS; do IMAGE_NAME="${{ env.CI_REGISTRY }}/${{ env.GITEA_ORG }}/${{ env.GITEA_REPO }}:$TAG" diff --git a/adr/0020-docker-build-strategy.md b/adr/0020-docker-build-strategy.md index 58ba13e..353d8a2 100644 --- a/adr/0020-docker-build-strategy.md +++ b/adr/0020-docker-build-strategy.md @@ -126,7 +126,7 @@ EXPOSE 8080 ENTRYPOINT ["/app/dance-lessons-coach"] ``` -**Dockerfile** - Development Dockerfile (kept for local development): +**docker/Dockerfile** - Development Dockerfile (kept for local development): ```dockerfile # Multi-stage build for development FROM golang:1.26.1-alpine AS builder @@ -147,6 +147,16 @@ EXPOSE 8080 ENTRYPOINT ["/app/dance-lessons-coach"] ``` +### File Organization + +All Dockerfiles are now organized in the `docker/` directory: +- `docker/Dockerfile` - Development Dockerfile +- `docker/Dockerfile.build` - Build cache Dockerfile +- `docker/Dockerfile.prod` - Production Dockerfile (development only, uses latest) +- `docker/Dockerfile.prod.template` - Template for reference + +This organization keeps the root directory clean and makes it clear which files are for development vs production. + ## Benefits ### CI/CD Pipeline Benefits diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ceaa438 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# DanceLessonsCoach Docker Image +# Multi-stage build for production deployment + +# Stage 1: Build binary +FROM golang:1.26.1-alpine AS builder + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . ./ + +# Install swag and generate Swagger docs only if they don't exist +RUN if [ ! -f pkg/server/docs/swagger.json ]; then \ + echo "📝 Generating Swagger documentation..." && \ + go install github.com/swaggo/swag/cmd/swag@latest && \ + cd pkg/server && go generate && \ + echo "✅ Swagger documentation generated"; \ + else \ + echo "✅ Swagger documentation already exists, skipping swag installation and generation"; \ + fi + +# Build binary +RUN CGO_ENABLED=0 GOOS=linux go build -o /dance-lessons-coach ./cmd/server + +# Stage 2: Final image +FROM alpine:3.18 + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache ca-certificates tzdata + +# Copy binary from builder +COPY --from=builder /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"] diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build new file mode 100644 index 0000000..9cf326b --- /dev/null +++ b/docker/Dockerfile.build @@ -0,0 +1,38 @@ +# Build environment Dockerfile with pre-installed Go tools and dependencies +# Optimized for CI/CD pipeline speed + +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 + +# 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..6e0dcdb --- /dev/null +++ b/docker/Dockerfile.prod @@ -0,0 +1,37 @@ +# DanceLessonsCoach 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..992b74f --- /dev/null +++ b/docker/Dockerfile.prod.template @@ -0,0 +1,36 @@ +# DanceLessonsCoach 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/test-build-cache-environment.sh b/scripts/test-build-cache-environment.sh index dcdb0d9..f7de75a 100755 --- a/scripts/test-build-cache-environment.sh +++ b/scripts/test-build-cache-environment.sh @@ -16,7 +16,12 @@ echo "" # 2. Build the build cache image echo "2. Building build cache image..." -docker build -t dance-lessons-coach-build-cache:$DEPS_HASH -f Dockerfile.build . +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 "" diff --git a/scripts/test-local-ci-cd.sh b/scripts/test-local-ci-cd.sh index aee631a..78c2bd3 100755 --- a/scripts/test-local-ci-cd.sh +++ b/scripts/test-local-ci-cd.sh @@ -95,10 +95,10 @@ if [ "$HAS_DOCKER" = true ]; then echo " docker build -t dance-lessons-coach:$CURRENT_VERSION ." echo "" - echo "2. Build production image using Dockerfile.prod:" - echo " # Note: Local Dockerfile.prod uses 'latest' tag for testing" - echo " docker build -t dance-lessons-coach-prod:$CURRENT_VERSION -f Dockerfile.prod ." - echo " # For CI/CD, the workflow generates correct Dockerfile.prod with dependency hash" + echo "2. Build production image using docker/Dockerfile.prod:" + echo " # Note: Local docker/Dockerfile.prod uses 'latest' tag for testing" + echo " docker build -t dance-lessons-coach-prod:$CURRENT_VERSION -f docker/Dockerfile.prod ." + echo " # For CI/CD, the workflow generates correct docker/Dockerfile.prod with dependency hash" echo "" echo "3. Compare image sizes:" @@ -140,8 +140,8 @@ if [ "$HAS_DOCKER" = true ]; then read -p "📋 Build (d)development or (p)production image? [d/p]: " -n 1 -r echo "" if [[ $REPLY =~ ^[Pp]$ ]]; then - echo "🏗️ Building production image with Dockerfile.prod..." - docker build -t dance-lessons-coach-prod:$CURRENT_VERSION -f Dockerfile.prod . + echo "🏗️ Building production image with docker/Dockerfile.prod..." + docker build -t dance-lessons-coach-prod:$CURRENT_VERSION -f docker/Dockerfile.prod . docker tag dance-lessons-coach-prod:$CURRENT_VERSION dance-lessons-coach-prod:latest echo "✅ Production Docker image built: dance-lessons-coach-prod:$CURRENT_VERSION" CONTAINER_IMAGE="dance-lessons-coach-prod:$CURRENT_VERSION"