From 341c7066c6d178f7cd5b2f282082092015165952 Mon Sep 17 00:00:00 2001 From: vmelikyan Date: Mon, 25 Aug 2025 16:21:50 -0700 Subject: [PATCH] Split Dockerfile into app and base dockerfiles for faster builds --- .github/workflows/base-release.yaml | 222 ++++++++++++++++++++++++++++ .github/workflows/release.yaml | 12 +- app.Dockerfile | 34 +++++ base.Dockerfile | 63 ++++++++ 4 files changed, 325 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/base-release.yaml create mode 100644 app.Dockerfile create mode 100644 base.Dockerfile diff --git a/.github/workflows/base-release.yaml b/.github/workflows/base-release.yaml new file mode 100644 index 00000000..55a784c1 --- /dev/null +++ b/.github/workflows/base-release.yaml @@ -0,0 +1,222 @@ +name: Build & Push Base Docker Image + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag for the base image (e.g., v1.0.0)' + required: true + type: string + push_latest: + description: 'Also tag as latest' + required: false + type: boolean + default: false + +jobs: + prepare-metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.meta.outputs.version }} + major: ${{ steps.meta.outputs.major }} + major_minor: ${{ steps.meta.outputs.major_minor }} + tags_suffix: ${{ steps.tags.outputs.tags_suffix }} + steps: + - name: Validate version format + run: | + VERSION="${{ github.event.inputs.version }}" + if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format v1.2.3" + exit 1 + fi + + - name: Extract version metadata + id: meta + run: | + VERSION="${{ github.event.inputs.version }}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Extract major and major.minor versions + if [[ "$VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + MAJOR="v${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + MAJOR_MINOR="v${BASH_REMATCH[1]}.${MINOR}" + echo "major=$MAJOR" >> $GITHUB_OUTPUT + echo "major_minor=$MAJOR_MINOR" >> $GITHUB_OUTPUT + fi + + - name: Determine Docker tags + id: tags + run: | + # Start with the full version tag + TAGS_SUFFIX="${{ steps.meta.outputs.version }}" + + # Add major and major.minor tags + TAGS_SUFFIX="$TAGS_SUFFIX,${{ steps.meta.outputs.major }}" + TAGS_SUFFIX="$TAGS_SUFFIX,${{ steps.meta.outputs.major_minor }}" + + # Optionally add latest tag + if [[ "${{ github.event.inputs.push_latest }}" == "true" ]]; then + TAGS_SUFFIX="$TAGS_SUFFIX,latest" + fi + + echo "tags_suffix=$TAGS_SUFFIX" >> $GITHUB_OUTPUT + + build-amd64: + runs-on: ubuntu-latest + needs: prepare-metadata + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --debug + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push AMD64 image + id: build + uses: docker/build-push-action@v4 + with: + context: . + file: base.Dockerfile + platforms: linux/amd64 + outputs: type=image,name=lifecycleoss/app-base,push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=lifecycleoss/app-base:buildcache-amd64 + cache-to: type=registry,ref=lifecycleoss/app-base:buildcache-amd64,mode=max + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-amd64 + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + build-arm64: + runs-on: ubuntu-latest + needs: prepare-metadata + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --debug + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push ARM64 image + id: build + uses: docker/build-push-action@v4 + with: + context: . + file: base.Dockerfile + platforms: linux/arm64 + outputs: type=image,name=lifecycleoss/app-base,push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=lifecycleoss/app-base:buildcache-arm64 + cache-to: type=registry,ref=lifecycleoss/app-base:buildcache-arm64,mode=max + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-arm64 + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-and-push: + runs-on: ubuntu-latest + needs: + - prepare-metadata + - build-amd64 + - build-arm64 + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create manifest list and push + env: + TAGS_SUFFIX: ${{ needs.prepare-metadata.outputs.tags_suffix }} + run: | + # Find digest files + AMD64_DIGEST=$(find /tmp/digests/digests-amd64 -type f | head -1) + ARM64_DIGEST=$(find /tmp/digests/digests-arm64 -type f | head -1) + + # Get the digest values (filenames) + AMD64_SHA=$(basename "$AMD64_DIGEST") + ARM64_SHA=$(basename "$ARM64_DIGEST") + + # Build the digest list + DIGESTS="lifecycleoss/app-base@sha256:$AMD64_SHA lifecycleoss/app-base@sha256:$ARM64_SHA" + + # Convert comma-separated tag suffixes to array + IFS=',' read -ra TAG_ARRAY <<< "$TAGS_SUFFIX" + + # Create and push manifest for each tag + for TAG_SUFFIX in "${TAG_ARRAY[@]}"; do + TAG_SUFFIX=$(echo "$TAG_SUFFIX" | xargs) # Trim whitespace + FULL_TAG="lifecycleoss/app-base:${TAG_SUFFIX}" + docker buildx imagetools create -t "$FULL_TAG" $DIGESTS + done + + - name: Inspect final image + run: | + docker buildx imagetools inspect lifecycleoss/app-base:${{ needs.prepare-metadata.outputs.version }} + + - name: Create GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: base-${{ needs.prepare-metadata.outputs.version }} + release_name: Base Image ${{ needs.prepare-metadata.outputs.version }} + body: | + Base image release for lifecycle application. + + Docker Hub: `lifecycleoss/app-base:${{ needs.prepare-metadata.outputs.version }}` + + Tags created: + - `lifecycleoss/app-base:${{ needs.prepare-metadata.outputs.version }}` + - `lifecycleoss/app-base:${{ needs.prepare-metadata.outputs.major }}` + - `lifecycleoss/app-base:${{ needs.prepare-metadata.outputs.major_minor }}` + ${{ github.event.inputs.push_latest == 'true' && '- `lifecycleoss/app-base:latest`' || '' }} + draft: false + prerelease: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 13e7acf4..a20f4fc0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -84,7 +84,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - file: Dockerfile + file: app.Dockerfile platforms: linux/amd64 outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/app,push-by-digest=true,name-canonical=true,push=true build-args: | @@ -128,7 +128,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . - file: Dockerfile + file: app.Dockerfile platforms: linux/arm64 outputs: type=image,name=${{ secrets.DOCKERHUB_USERNAME }}/app,push-by-digest=true,name-canonical=true,push=true build-args: | @@ -178,17 +178,17 @@ jobs: # Find digest files AMD64_DIGEST=$(find /tmp/digests/digests-amd64 -type f | head -1) ARM64_DIGEST=$(find /tmp/digests/digests-arm64 -type f | head -1) - + # Get the digest values (filenames) AMD64_SHA=$(basename "$AMD64_DIGEST") ARM64_SHA=$(basename "$ARM64_DIGEST") - + # Build the digest list DIGESTS="${DOCKER_USERNAME}/app@sha256:$AMD64_SHA ${DOCKER_USERNAME}/app@sha256:$ARM64_SHA" - + # Convert comma-separated tag suffixes to array IFS=',' read -ra TAG_ARRAY <<< "$TAGS_SUFFIX" - + # Create and push manifest for each tag for TAG_SUFFIX in "${TAG_ARRAY[@]}"; do TAG_SUFFIX=$(echo "$TAG_SUFFIX" | xargs) # Trim whitespace diff --git a/app.Dockerfile b/app.Dockerfile new file mode 100644 index 00000000..b2f76d63 --- /dev/null +++ b/app.Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2025 GoodRx, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG BASE_IMAGE_TAG=v1 +FROM lifecycleoss/app-base:${BASE_IMAGE_TAG} AS packages + +ARG PORT + +ENV PORT=$PORT + +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install + +FROM packages + +COPY . . + +RUN pnpm run build + +EXPOSE $PORT + +ENTRYPOINT [ "./scripts/k8-start.sh" ] \ No newline at end of file diff --git a/base.Dockerfile b/base.Dockerfile new file mode 100644 index 00000000..7c13b7fa --- /dev/null +++ b/base.Dockerfile @@ -0,0 +1,63 @@ +# Copyright 2025 GoodRx, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:20-slim + +ARG TARGETARCH + +RUN apt-get update && apt-get install -y \ + wget \ + unzip \ + curl \ + jq \ + git \ + procps \ + postgresql-client \ + net-tools \ + build-essential \ + python3 \ + ruby \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && \ + chmod 700 get_helm.sh && \ + ./get_helm.sh && \ + rm get_helm.sh + +RUN if [ "$TARGETARCH" = "arm64" ]; then \ + curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" && \ + unzip awscliv2.zip && \ + ./aws/install && \ + rm -rf awscliv2.zip aws/ && \ + curl -LO https://dl.k8s.io/release/v1.30.0/bin/linux/arm64/kubectl; \ + else \ + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ + unzip awscliv2.zip && \ + ./aws/install && \ + rm -rf awscliv2.zip aws/ && \ + curl -LO https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl; \ + fi && \ + chmod +x ./kubectl && \ + mv ./kubectl /usr/local/bin/kubectl + +RUN npm install pnpm --global + +RUN npm install codefresh@0.81.5 --global + +RUN npm install dotenv-cli --global + +ENV BUILD_MODE=yes +ENV DATABASE_URL=no-db + +WORKDIR /app \ No newline at end of file