diff --git a/.github/workflows/docker-publish-arm.yaml b/.github/workflows/docker-publish-arm.yaml index 9e43e473..0147fa1c 100644 --- a/.github/workflows/docker-publish-arm.yaml +++ b/.github/workflows/docker-publish-arm.yaml @@ -30,7 +30,6 @@ env: # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build: @@ -38,36 +37,35 @@ jobs: permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. attestations: write id-token: write steps: + # Step 1: Checkout repository - name: Checkout repository uses: actions/checkout@v4 - # Set up BuildKit Docker container builder to be able to build - # multi-platform images and export cache - # https://github.com/docker/setup-buildx-action + # Step 2: Set up Buildx without specifying driver + # Let it use default settings to avoid the 'no remote endpoint' issue - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 # v3.0.0 + uses: docker/setup-buildx-action@v3.0.0 + with: + install: true # Ensure Buildx is installed and set up properly + use: true # Use Buildx instance directly for this job - # Login against a Docker registry except on PR - # https://github.com/docker/login-action + # Step 3: Login against Docker registry except on PR - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v3.0.0 # v3.0.0 + uses: docker/login-action@v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action + # Step 4: Extract metadata for Docker images - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5.0.0 # v5.0.0 + uses: docker/metadata-action@v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -80,11 +78,10 @@ jobs: flavor: | suffix=-arm,onlatest=true - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action + # Step 5: Build and push the Docker image - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v5.0.0 # v5.0.0 + uses: docker/build-push-action@v5.0.0 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -95,8 +92,9 @@ jobs: cache-to: type=gha,mode=max build-args: | VERSION=${{ github.ref_name }} - COMMIT=${{ github.sha }} - + COMMIT=${{ github.sha }} + + # Step 6: Attest built image to prove build provenance - name: Attest uses: actions/attest-build-provenance@v1 id: attest diff --git a/.github/workflows/docker-publish-rootless-arm.yaml b/.github/workflows/docker-publish-rootless-arm.yaml index fde1e86a..c40cd55b 100644 --- a/.github/workflows/docker-publish-rootless-arm.yaml +++ b/.github/workflows/docker-publish-rootless-arm.yaml @@ -24,14 +24,12 @@ on: - '.dockerignore' - '.github/workflows' - env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build-rootless: @@ -39,36 +37,34 @@ jobs: permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. attestations: write id-token: write steps: + # Step 1: Checkout repository - name: Checkout repository uses: actions/checkout@v4 - # Set up BuildKit Docker container builder to be able to build - # multi-platform images and export cache - # https://github.com/docker/setup-buildx-action + # Step 2: Set up Buildx without specifying driver - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 # v3.0.0 + uses: docker/setup-buildx-action@v3.0.0 + with: + install: true # Ensure Buildx is installed and set up properly + use: true # Use Buildx instance directly for this job - # Login against a Docker registry except on PR - # https://github.com/docker/login-action + # Step 3: Login to Docker registry except on PR - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v3.0.0 # v3.0.0 + uses: docker/login-action@v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action + # Step 4: Extract metadata for Docker images - name: Extract Docker metadata id: metadata - uses: docker/metadata-action@v5.0.0 # v5.0.0 + uses: docker/metadata-action@v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -80,12 +76,11 @@ jobs: type=schedule,pattern=nightly flavor: | suffix=-rootless-arm,onlatest=true - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action + + # Step 5: Build and push the Docker image - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v5.0.0 # v5.0.0 + uses: docker/build-push-action@v5.0.0 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -98,6 +93,7 @@ jobs: VERSION=${{ github.ref_name }} COMMIT=${{ github.sha }} + # Step 6: Attest built image to prove build provenance - name: Attest uses: actions/attest-build-provenance@v1 id: attest diff --git a/Dockerfile b/Dockerfile index 85097bd7..878a1231 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,69 +1,91 @@ -# Node dependencies -FROM node:18-alpine AS frontend-dependencies +# Node dependencies stage +FROM --platform=$TARGETPLATFORM node:18-alpine AS frontend-dependencies WORKDIR /app + +# Install pnpm globally (caching layer) RUN npm install -g pnpm + +# Copy package.json and lockfile to leverage caching COPY frontend/package.json frontend/pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile --shamefully-hoist -# Build Nuxt -FROM node:18-alpine AS frontend-builder -WORKDIR /app +# Build Nuxt (frontend) stage +FROM --platform=$TARGETPLATFORM node:18-alpine AS frontend-builder +WORKDIR /app + +# Install pnpm globally again (it can reuse the cache if not changed) RUN npm install -g pnpm -COPY frontend . + +# Copy over source files and node_modules from dependencies stage +COPY frontend . COPY --from=frontend-dependencies /app/node_modules ./node_modules RUN pnpm build -FROM golang:alpine AS builder-dependencies +# Go dependencies stage +FROM --platform=$TARGETPLATFORM golang:alpine AS builder-dependencies WORKDIR /go/src/app -COPY ./backend . + +# Copy go.mod and go.sum for better caching +COPY ./backend/go.mod ./backend/go.sum ./ RUN go mod download -# Build API -FROM golang:alpine AS builder +# Build API stage +FROM --platform=$TARGETPLATFORM golang:alpine AS builder ARG BUILD_TIME ARG COMMIT ARG VERSION + +# Install necessary build tools RUN apk update && \ apk upgrade && \ - apk add --update git build-base gcc g++ + apk add --no-cache git build-base gcc g++ WORKDIR /go/src/app + +# Copy Go modules (from dependencies stage) and source code +COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod COPY ./backend . + +# Clear old public files and copy new ones from frontend build RUN rm -rf ./app/api/public COPY --from=frontend-builder /app/.output/public ./app/api/static/public -COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod -RUN --mount=type=cache,target=/root/.cache/go-build \ + +# Use cache for Go build artifacts +RUN --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=linux go build \ - -ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \ + -ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \ -o /go/bin/api \ -v ./app/api/*.go -FROM gcr.io/distroless/java:latest - -# Production Stage -FROM alpine:latest - +# Production stage +FROM --platform=$TARGETPLATFORM alpine:latest ENV HBOX_MODE=production ENV HBOX_STORAGE_DATA=/data/ ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1 -RUN apk --no-cache add ca-certificates +# Install necessary runtime dependencies +RUN apk --no-cache add ca-certificates wget + +# Create application directory and copy over built Go binary RUN mkdir /app COPY --from=builder /go/bin/api /app - RUN chmod +x /app/api -RUN apk add --no-cache wget +# Labels and configuration for the final image LABEL Name=homebox Version=0.0.1 LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox" + +# Expose necessary ports EXPOSE 7745 WORKDIR /app -HEALTHCHECK --interval=30s \ - --timeout=5s \ - --start-period=5s \ - --retries=3 \ - CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ] + +# Healthcheck configuration +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD [ "wget", "--no-verbose", "--tries=1", "-O", "-", "http://localhost:7745/api/v1/status" ] + +# Persist volume VOLUME [ "/data" ] +# Entrypoint and CMD ENTRYPOINT [ "/app/api" ] CMD [ "/data/config.yml" ] diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 3a0799d8..91ac930e 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -7,15 +7,15 @@ RUN pnpm install --frozen-lockfile --shamefully-hoist # Build Nuxt FROM node:18-alpine AS frontend-builder -WORKDIR /app -RUN npm install -g pnpm -COPY frontend . +WORKDIR /app +COPY frontend ./ COPY --from=frontend-dependencies /app/node_modules ./node_modules RUN pnpm build +# Build Go dependencies FROM golang:alpine AS builder-dependencies WORKDIR /go/src/app -COPY ./backend . +COPY ./backend/go.mod ./backend/go.sum ./ RUN go mod download # Build API @@ -23,48 +23,51 @@ FROM golang:alpine AS builder ARG BUILD_TIME ARG COMMIT ARG VERSION -RUN apk update && \ - apk upgrade && \ - apk add --update git build-base gcc g++ + +RUN apk update && apk upgrade && apk add --no-cache git build-base gcc g++ WORKDIR /go/src/app COPY ./backend . RUN rm -rf ./app/api/public COPY --from=frontend-builder /app/.output/public ./app/api/static/public COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod -RUN --mount=type=cache,target=/root/.cache/go-build \ - CGO_ENABLED=0 GOOS=linux go build \ - -ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \ - -o /go/bin/api \ - -v ./app/api/*.go -FROM gcr.io/distroless/java:latest +# Use cache for Go build +RUN --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 GOOS=linux go build \ + -ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \ + -o /go/bin/api ./app/api/*.go -# Production Stage +# Production stage with distroless FROM gcr.io/distroless/static:latest ENV HBOX_MODE=production ENV HBOX_STORAGE_DATA=/data/ ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1 -# Copy the binary and the (empty) /data dir and -# change the ownership to the low-privileged user +# Copy the binary and data directory, change ownership COPY --from=builder --chown=nonroot /go/bin/api /app COPY --from=builder --chown=nonroot /data /data -RUN apk add --no-cache wget +# Add wget to the image +# Note: If using distroless, this may not be applicable +# as distroless images do not include package managers. +# This line may be omitted if you're relying on another way to handle healthchecks. +COPY --from=alpine:latest /bin/wget /usr/bin/wget LABEL Name=homebox Version=0.0.1 LABEL org.opencontainers.image.source="https://github.com/sysadminsmedia/homebox" EXPOSE 7745 + HEALTHCHECK --interval=30s \ --timeout=5s \ --start-period=5s \ --retries=3 \ - CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ] -VOLUME [ "/data" ] + CMD ["/usr/bin/wget", "--no-verbose", "--tries=1", "-O", "-", "http://localhost:7745/api/v1/status"] + +VOLUME ["/data"] -# Drop root and run as low-privileged user +# Drop root and run as a low-privileged user USER nonroot -ENTRYPOINT [ "/app" ] -CMD [ "/data/config.yml" ] +ENTRYPOINT ["/app"] +CMD ["/data/config.yml"]