From 1ee0cee1714e424f4155362c9a294042e830e6eb Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 7 Oct 2025 15:03:53 +0300 Subject: [PATCH] feat: distroless image --- .github/workflows/nightly.yml | 161 +++++++++++++++++++++++++++++- Dockerfile | 4 +- Dockerfile.distroless | 56 +++++++++++ cmd/{health.go => healthcheck.go} | 30 ++++-- cmd/root.go | 2 +- 5 files changed, 236 insertions(+), 17 deletions(-) create mode 100644 Dockerfile.distroless rename cmd/{health.go => healthcheck.go} (74%) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 75ae89f..70f434d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -171,6 +171,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -190,6 +192,64 @@ jobs: if-no-files-found: error retention-days: 1 + image-build-distroless: + runs-on: ubuntu-latest + needs: + - create-release + - generate-metadata + - image-build + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: nightly + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/amd64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-distoless-linux-amd64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + image-build-arm: runs-on: ubuntu-24.04-arm needs: @@ -217,10 +277,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Set version - run: | - echo nightly > internal/assets/version - - name: Build and push uses: docker/build-push-action@v6 id: build @@ -229,6 +285,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: ghcr.io/${{ github.repository_owner }}/tinyauth outputs: type=image,push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.generate-metadata.outputs.VERSION }} COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} @@ -248,6 +306,64 @@ jobs: if-no-files-found: error retention-days: 1 + image-build-arm-distroless: + runs-on: ubuntu-24.04-arm + needs: + - create-release + - generate-metadata + - image-build-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: nightly + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + id: build + with: + platforms: linux/arm64 + labels: ${{ steps.meta.outputs.labels }} + tags: ghcr.io/${{ github.repository_owner }}/tinyauth + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + file: Dockerfile.distroless + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ needs.generate-metadata.outputs.VERSION }} + COMMIT_HASH=${{ needs.generate-metadata.outputs.COMMIT_HASH }} + BUILD_TIMESTAMP=${{ needs.generate-metadata.outputs.BUILD_TIMESTAMP }} + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-distroless-linux-arm64 + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + image-merge: runs-on: ubuntu-latest needs: @@ -285,6 +401,43 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + image-merge-distroless: + runs-on: ubuntu-latest + needs: + - image-build-distroless + - image-build-arm-distroless + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-distroless-* + merge-multiple: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/tinyauth + tags: | + type=raw,nightly-distroless + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth@sha256:%s ' *) + update-release: runs-on: ubuntu-latest needs: diff --git a/Dockerfile b/Dockerfile index f61a32a..3005617 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,4 +51,6 @@ EXPOSE 3000 VOLUME ["/data"] -ENTRYPOINT ["./tinyauth"] \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless new file mode 100644 index 0000000..37723c6 --- /dev/null +++ b/Dockerfile.distroless @@ -0,0 +1,56 @@ +# Site builder +FROM oven/bun:1.2.23-alpine AS frontend-builder + +WORKDIR /frontend + +COPY ./frontend/package.json ./ +COPY ./frontend/bun.lock ./ + +RUN bun install + +COPY ./frontend/public ./public +COPY ./frontend/src ./src +COPY ./frontend/eslint.config.js ./ +COPY ./frontend/index.html ./ +COPY ./frontend/tsconfig.json ./ +COPY ./frontend/tsconfig.app.json ./ +COPY ./frontend/tsconfig.node.json ./ +COPY ./frontend/vite.config.ts ./ + +RUN bun run build + +# Builder +FROM golang:1.25-alpine3.21 AS builder + +ARG VERSION +ARG COMMIT_HASH +ARG BUILD_TIMESTAMP + +WORKDIR /tinyauth + +COPY go.mod ./ +COPY go.sum ./ + +RUN go mod download + +COPY ./main.go ./ +COPY ./cmd ./cmd +COPY ./internal ./internal +COPY --from=frontend-builder /frontend/dist ./internal/assets/dist + +RUN CGO_ENABLED=0 go build -ldflags "-s -w -X tinyauth/internal/config.Version=${VERSION} -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" + +# Runner +FROM gcr.io/distroless/static-debian12:latest AS runner + +WORKDIR /tinyauth + +COPY --from=builder /tinyauth/tinyauth ./ + +EXPOSE 3000 + +VOLUME ["/data"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/tinyauth/tinyauth", "healthcheck"] + +ENTRYPOINT ["tinyauth"] \ No newline at end of file diff --git a/cmd/health.go b/cmd/healthcheck.go similarity index 74% rename from cmd/health.go rename to cmd/healthcheck.go index 9550807..7589afc 100644 --- a/cmd/health.go +++ b/cmd/healthcheck.go @@ -17,24 +17,24 @@ type healthzResponse struct { Message string `json:"message"` } -type healthCmd struct { +type healthcheckCmd struct { root *cobra.Command cmd *cobra.Command viper *viper.Viper } -func newHealthCmd(root *cobra.Command) *healthCmd { - return &healthCmd{ +func newHealthcheckCmd(root *cobra.Command) *healthcheckCmd { + return &healthcheckCmd{ root: root, viper: viper.New(), } } -func (c *healthCmd) Register() { +func (c *healthcheckCmd) Register() { c.cmd = &cobra.Command{ - Use: "health", - Short: "Health check", + Use: "healthcheck [app-url]", + Short: "Perform a health check", Long: `Use the health check endpoint to verify that Tinyauth is running and it's healthy.`, Run: c.run, } @@ -46,26 +46,34 @@ func (c *healthCmd) Register() { } } -func (c *healthCmd) GetCmd() *cobra.Command { +func (c *healthcheckCmd) GetCmd() *cobra.Command { return c.cmd } -func (c *healthCmd) run(cmd *cobra.Command, args []string) { +func (c *healthcheckCmd) run(cmd *cobra.Command, args []string) { log.Logger = log.Level(zerolog.InfoLevel) - appUrl := "http://127.0.0.1:3000" + var appUrl string port := c.viper.GetString("PORT") address := c.viper.GetString("ADDRESS") - if address != "" && port != "" { - appUrl = "http://" + address + ":" + port + if port == "" { + port = "3000" } + if address == "" { + address = "127.0.0.1" + } + + appUrl = "http://" + address + ":" + port + if len(args) > 0 { appUrl = args[0] } + log.Info().Str("appUrl", appUrl).Msg("Performing health check") + client := http.Client{} req, err := http.NewRequest("GET", appUrl+"/api/healthz", nil) diff --git a/cmd/root.go b/cmd/root.go index 92abab3..a0ce20d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,7 +140,7 @@ func Run() { newVerifyUserCmd(userCmd).Register() newGenerateTotpCmd(totpCmd).Register() newVersionCmd(root).Register() - newHealthCmd(root).Register() + newHealthcheckCmd(root).Register() root.AddCommand(userCmd) root.AddCommand(totpCmd)