feat: build multi-arch docker images for amd64 and arm64

- using debian as base image
- do cross compile for build arm64 image
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..59a75b5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.git/
+config-ui/
+grafana/
+releases/
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a0957a5..bd01c65 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,9 +17,6 @@
 
 name: Build-Images-Push-Docker
 env:
-  IMAGE_LAKE: ${{ secrets.DOCKERHUB_OWNER }}/devlake
-  IMAGE_CONFIG_UI: ${{ secrets.DOCKERHUB_OWNER }}/devlake-config-ui
-  IMAGE_GRAFANA: ${{ secrets.DOCKERHUB_OWNER }}/devlake-dashboard
   DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USER }}
   DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
 on:
@@ -27,79 +24,197 @@
     tags:
       - 'v*'
 jobs:
-  build-lake:
-    name: Build and Push lake image
-    runs-on: ubuntu-20.04
+  build-and-push-builder:
+    name: Build and Push devlake builder
+    runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - name: Cache Build-Images-Push-Docker
-        id: cache-Build-Images-Push-Docker
-        uses: actions/cache@v3
+      - uses: actions/checkout@v3
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
         with:
-          path: Build-Images-Push-Docker
-          key: ${{ runner.os }}-Build-Images-Push-Docker
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+      - name: Build and push lake image
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          push: true
+          target: builder
+          tags: ${{ secrets.DOCKERHUB_OWNER }}/devlake:amd64-builder
+          platforms: linux/amd64
+          cache-from: ${{ secrets.DOCKERHUB_OWNER }}/devlake:amd64-builder
+          cache-to: ${{ secrets.DOCKERHUB_OWNER }}/devlake:amd64-builder
+  build-and-push-base:
+    name: Build and Push devlake base
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+      - name: Build and push lake image
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          push: true
+          target: base
+          tags: ${{ secrets.DOCKERHUB_OWNER }}/devlake:base
+          platforms: linux/amd64,linux/arm64
+          cache-from: ${{ secrets.DOCKERHUB_OWNER }}/devlake:base
+          cache-to: ${{ secrets.DOCKERHUB_OWNER }}/devlake:base
+  build-devlake:
+    needs: build-and-push-builder
+    name: Build and cache devlake
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        platform: ['arm64', 'amd64']
+    steps:
+      - uses: actions/checkout@v3
+      - name: Get short sha
+        id: get_short_sha
+        run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+      - uses: actions/cache@v3
+        with:
+          path: /tmp/devlake-build-cache-${{ matrix.platform }}
+          key: buildx-devlake-build-cache-${{ github.run_id }}-${{ matrix.platform }}
+      - name: Build and cache lake build
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          push: false
+          target: build
+          tags: ${{ secrets.DOCKERHUB_OWNER }}/devlake:build-cache-${{ matrix.platform }}
+          platforms: linux/${{ matrix.platform }}
+          cache-from: ${{ secrets.DOCKERHUB_OWNER }}/devlake:amd64-builder
+          cache-to: type=local,mode=min,dest=/tmp/devlake-build-cache-${{ matrix.platform }}
+          build-args: |
+            TAG=${{ github.ref_name }}
+            SHA=${{ steps.get_short_sha.outputs.SHORT_SHA }}
+  build-and-push-devlake:
+    needs: [build-devlake, build-and-push-base]
+    name: Build and Push devlake image
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Get short sha
+        id: get_short_sha
+        run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
+      - name: Login to DockerHub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ env.DOCKERHUB_USERNAME }}
+          password: ${{ env.DOCKERHUB_TOKEN }}
+      - uses: actions/cache@v3
+        with:
+          path: /tmp/devlake-build-cache-amd64
+          key: buildx-devlake-build-cache-${{ github.run_id }}-amd64
+      - uses: actions/cache@v3
+        with:
+          path: /tmp/devlake-build-cache-arm64
+          key: buildx-devlake-build-cache-${{ github.run_id }}-arm64
+      - name: Get push tags
+        id: get_push_tags
+        run: |
+            image_name=${{ secrets.DOCKERHUB_OWNER }}/devlake
+            if printf ${{ github.ref_name }} | grep -Pq '^v(\d+).(\d+).(\d+)$'; then
+                echo "TAGS=${image_name}:latest,${image_name}:${{ github.ref_name }}" >> $GITHUB_OUTPUT
+            else
+                echo "TAGS=${image_name}:${{ github.ref_name }}" >> $GITHUB_OUTPUT
+            fi
+      - name: Build and push lake image
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          push: true
+          tags: ${{ steps.get_push_tags.outputs.TAGS }}
+          platforms: linux/amd64,linux/arm64
+          cache-from: |
+            ${{ secrets.DOCKERHUB_OWNER }}/devlake:amd64-builder
+            ${{ secrets.DOCKERHUB_OWNER }}/devlake:base
+            type=local,src=/tmp/devlake-build-cache-amd64
+            type=local,src=/tmp/devlake-build-cache-arm64
+          build-args: |
+            TAG=${{ github.ref_name }}
+            SHA=${{ steps.get_short_sha.outputs.SHORT_SHA }}
+      - name: Clear cache
+        uses: actions/github-script@v6
+        if: always()
+        with:
+          script: |
+            for (const arch of ['amd64', 'arm64']) {
+              try {
+                await github.rest.actions.deleteActionsCacheByKey({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  key: `buildx-devlake-build-cache-${context.runId}-${arch}`,
+                })
+                console.log(`Clear cache buildx-devlake-build-cache-${context.runId}-${arch}`)
+              } catch (e) {
+                console.warn(`Error clear cache buildx-devlake-build-cache-${context.runId}-${arch}: ${e}`)
+              }
+            }
 
-      - name: Login to DockerHub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ env.DOCKERHUB_USERNAME }}
-          password: ${{ env.DOCKERHUB_TOKEN }}
-      - name: Build lake image
-        run: |
-          docker build --build-arg TAG=${{ github.ref_name }} --build-arg SHA=${{ github.sha }} -t ${{ env.IMAGE_LAKE }}:latest --file ./Dockerfile .
-          docker tag ${{ env.IMAGE_LAKE }}:latest ${{ env.IMAGE_LAKE }}:${{ github.ref_name }}
-          docker push ${{ env.IMAGE_LAKE }}:${{ github.ref_name }}
-          if printf ${{ github.ref_name }} | grep -Pq '^v(\d+).(\d+).(\d+)$'; then
-            echo "push latest tag"
-            docker push ${{ env.IMAGE_LAKE }}:latest
-          fi
-  build-configui:
-    name: Build and Push config-ui image
-    runs-on: ubuntu-20.04
+  build-and-push-other-image:
+    name: Build and Push ${{ matrix.build.name }} image
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        build:
+          - name: config-ui
+            image: devlake-config-ui
+            context: config-ui
+          - name: grafana
+            image: devlake-dashboard
+            context: grafana
     steps:
-      - uses: actions/checkout@v2
-      - name: Cache config-ui
-        id: cache-config-ui
-        uses: actions/cache@v3
-        with:
-          path: config-ui
-          key: ${{ runner.os }}-config-ui
+      - uses: actions/checkout@v3
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v2
       - name: Login to DockerHub
         uses: docker/login-action@v2
         with:
           username: ${{ env.DOCKERHUB_USERNAME }}
           password: ${{ env.DOCKERHUB_TOKEN }}
-      - name: Build config ui image
+      - name: Get push tags
+        id: get_push_tags
         run: |
-          cd config-ui
-          docker build -t ${{ env.IMAGE_CONFIG_UI }}:latest --file ./Dockerfile .
-          docker tag ${{ env.IMAGE_CONFIG_UI }}:latest ${{ env.IMAGE_CONFIG_UI }}:${{ github.ref_name }}
-          docker push ${{ env.IMAGE_CONFIG_UI }}:${{ github.ref_name }}
-          if printf ${{ github.ref_name }} | grep -Pq '^v(\d+).(\d+).(\d+)$'; then
-            docker push ${{ env.IMAGE_CONFIG_UI }}:latest
-          fi
-  build-grafana:
-    name: Build and Push grafana image
-    runs-on: ubuntu-20.04
-    steps:
-      - uses: actions/checkout@v2
-      - name: Cache grafana
-        id: cache-grafana
-        uses: actions/cache@v3
+            image_name=${{ secrets.DOCKERHUB_OWNER }}/${{ matrix.build.image }}
+            if printf ${{ github.ref_name }} | grep -Pq '^v(\d+).(\d+).(\d+)$'; then
+                echo "TAGS=${image_name}:latest,${image_name}:${{ github.ref_name }}" >> $GITHUB_OUTPUT
+            else
+                echo "TAGS=${image_name}:${{ github.ref_name }}" >> $GITHUB_OUTPUT
+            fi
+      - name: Build and push ${{ matrix.build.name }} image
+        uses: docker/build-push-action@v3
         with:
-          path: grafana
-          key: ${{ runner.os }}-grafana
-      - name: Login to DockerHub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ env.DOCKERHUB_USERNAME }}
-          password: ${{ env.DOCKERHUB_TOKEN }}
-      - name: Build grafana
-        run: |
-          cd grafana
-          docker build -t ${{ env.IMAGE_GRAFANA }}:latest --file ./Dockerfile .
-          docker tag ${{ env.IMAGE_GRAFANA }}:latest ${{ env.IMAGE_GRAFANA }}:${{ github.ref_name }}
-          docker push ${{ env.IMAGE_GRAFANA }}:${{ github.ref_name }}
-          if printf ${{ github.ref_name }} | grep -Pq '^v(\d+).(\d+).(\d+)$'; then
-            docker push ${{ env.IMAGE_GRAFANA }}:latest
-          fi
+          context: ${{ matrix.build.context }}
+          push: true
+          tags: ${{ steps.get_push_tags.outputs.TAGS }}
+          platforms: linux/amd64,linux/arm64
+
diff --git a/Dockerfile b/Dockerfile
index bd559c3..f28b7ba 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,43 +23,140 @@
 #While incubation status is not necessarily a reflection of the completeness or stability of the code,
 #it does indicate that the project has yet to be fully endorsed by the ASF.
 
-FROM mericodev/lake-builder:latest as builder
+
+FROM --platform=linux/amd64 debian:bullseye as debian-amd64
+RUN apt-get update
+RUN apt-get install -y libssh2-1-dev libssl-dev zlib1g-dev
+
+FROM --platform=linux/arm64 debian:bullseye as debian-arm64
+RUN apt-get update
+RUN apt-get install -y libssh2-1-dev libssl-dev zlib1g-dev
+
+FROM --platform=$BUILDPLATFORM golang:1.19-bullseye as builder
 
 # docker build --build-arg GOPROXY=https://goproxy.io,direct -t mericodev/lake .
 ARG GOPROXY=
 # docker build --build-arg HTTPS_PROXY=http://localhost:4780 -t mericodev/lake .
 ARG HTTP_PROXY=
 ARG HTTPS_PROXY=
-ARG TAG=
-ARG SHA=
+
+RUN apt-get update
+RUN apt-get install -y gcc binutils libfindbin-libs-perl cmake libssh2-1-dev libssl-dev zlib1g-dev
+
+RUN if [ "$(arch)" != "aarch64" ] ; then \
+        apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu ; \
+    fi
+RUN if [ "$(arch)" != "x86_64" ] ; then \
+        apt-get install -y gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu ; \
+    fi
+
+RUN go install github.com/vektra/mockery/v2@v2.12.3
+RUN go install github.com/swaggo/swag/cmd/swag@v1.8.4
+
+COPY --from=debian-amd64 /usr/include /rootfs-amd64/usr/include
+COPY --from=debian-amd64 /usr/lib/x86_64-linux-gnu /rootfs-amd64/usr/lib/x86_64-linux-gnu
+COPY --from=debian-amd64 /lib/x86_64-linux-gnu /rootfs-amd64/lib/x86_64-linux-gnu
+
+COPY --from=debian-arm64 /usr/include /rootfs-arm64/usr/include
+COPY --from=debian-arm64 /usr/lib/aarch64-linux-gnu /rootfs-arm64/usr/lib/aarch64-linux-gnu
+COPY --from=debian-arm64 /lib/aarch64-linux-gnu /rootfs-arm64/lib/aarch64-linux-gnu
+
+RUN for arch in aarch64 x86_64 ; do \
+        mkdir -p /tmp/build/${arch} && cd /tmp/build/${arch} && \
+        wget https://github.com/libgit2/libgit2/archive/refs/tags/v1.3.2.tar.gz -O - | tar -xz && \
+        cd libgit2-1.3.2 && \
+        mkdir build && cd build && \
+        if [ "$arch" = "aarch64" ] ; then \
+            cmake .. -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
+                -DBUILD_SHARED_LIBS=ON -DCMAKE_SYSROOT=/rootfs-arm64 \
+                -DCMAKE_INSTALL_PREFIX=/usr/local/deps/${arch} ; \
+        elif [ "$arch" = "x86_64" ] ; then \
+            cmake .. -DCMAKE_C_COMPILER=x86_64-linux-gnu-gcc \
+                -DBUILD_SHARED_LIBS=ON -DCMAKE_SYSROOT=/rootfs-amd64 \
+                -DCMAKE_INSTALL_PREFIX=/usr/local/deps/${arch} ; \
+        fi && \
+        make -j install ; \
+    done
+
+
+FROM builder as build
 
 WORKDIR /app
 COPY . /app
 ENV GOBIN=/app/bin
 
-RUN make clean && make all
+ARG TARGETPLATFORM
+ARG TAG=
+ARG SHA=
 
-FROM --platform=linux/amd64 mericodev/alpine-dbt:0.0.1
+RUN --mount=type=cache,target=/root/.cache/go-build \
+    if [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
+        ln -s /usr/local/deps/aarch64 /usr/local/deps/target && \
+        export CC=aarch64-linux-gnu-gcc && \
+        export GOARCH=arm64 ; \
+    else \
+        ln -s /usr/local/deps/x86_64 /usr/local/deps/target && \
+        export CC=x86_64-linux-gnu-gcc && \
+        export GOARCH=amd64 ; \
+    fi && \
+    export PKG_CONFIG_PATH=/usr/local/deps/target/lib/pkgconfig && \
+    export CGO_ENABLED=1 && \
+    make all
+
+# remove symlink in lib, we will recreate in final image
+RUN cd /usr/local/deps/target/lib && \
+    for file in *.so* ; do \
+        if [ -L $file ] ; then \
+            unlink $file ; \
+        fi \
+    done
+
+
+FROM debian:bullseye-slim as base
+
+ENV PYTHONUNBUFFERED=1
+
+RUN apt-get update && \
+    apt-get install -y python3-dev python3-pip tar curl libssh2-1 libssl zlib1g && \
+    apt-get clean && \
+    rm -fr /usr/share/doc/* \
+           /usr/share/info/* \
+           /usr/share/linda/* \
+           /usr/share/lintian/overrides/* \
+           /usr/share/locale/* \
+           /usr/share/man/* \
+           /usr/share/doc/kde/HTML/* \
+           /usr/share/gnome/help/* \
+           /usr/share/locale/* \
+           /usr/share/omf/*/*-*.emf \
+           /var/lib/apt/lists/*
 
 EXPOSE 8080
 
 WORKDIR /app
 
-COPY --from=builder /app/bin /app/bin
-COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
-COPY --from=builder /app/requirements.txt /app/requirements.txt
-COPY --from=builder /app/config/tap /app/config/tap
-
 # Setup Python
-RUN python -m venv /app/.venv
-RUN echo "source /app/.venv/bin/activate" >> ~/.profile
-RUN source ~/.profile
-RUN pip install --upgrade pip -r requirements.txt
-RUN apk add --no-cache curl
+COPY requirements.txt /app/requirements.txt
+RUN python3 -m pip install --no-cache --upgrade pip setuptools && \
+    python3 -m pip install --no-cache dbt-mysql dbt-postgres && \
+    python3 -m pip install --no-cache -r requirements.txt && \
+    rm -fr /usr/share/python-wheels/*
+
+
+
+FROM base as devlake-base
+
+# libraries
+ENV LD_LIBRARY_PATH=/app/libs
+RUN mkdir -p /app/libs
+COPY --from=build /usr/local/deps/target/lib/*.so* /app/libs
+RUN ldconfig -vn /app/libs
+
+# apps
+COPY --from=build /app/bin /app/bin
+COPY --from=build /app/config/tap /app/config/tap
 
 ENV PATH="/app/bin:${PATH}"
 
 CMD ["lake"]
 
-# Notes: Docker for Mac(M1) sets up qemu emulation, you can try to use the amd64 image by adding the --platform=linux/amd64 flag. 
-# Such as: FROM --platform=linux/amd64 alpine:3.15
diff --git a/Makefile b/Makefile
index d3a66f7..0b825b2 100644
--- a/Makefile
+++ b/Makefile
@@ -54,7 +54,7 @@
 all: build build-worker
 
 build-server-image:
-	docker build -t $(IMAGE_REPO)/devlake:$(TAG) --file ./Dockerfile .
+	docker build -t $(IMAGE_REPO)/devlake:$(TAG) --build-arg TAG=$(TAG) --build-arg SHA=$(SHA) --file ./Dockerfile .
 
 build-config-ui-image:
 	cd config-ui; docker build -t $(IMAGE_REPO)/devlake-config-ui:$(TAG) --file ./Dockerfile .
diff --git a/config-ui/Dockerfile b/config-ui/Dockerfile
index dcb0bf0..35296ef 100644
--- a/config-ui/Dockerfile
+++ b/config-ui/Dockerfile
@@ -23,7 +23,7 @@
 #While incubation status is not necessarily a reflection of the completeness or stability of the code,
 #it does indicate that the project has yet to be fully endorsed by the ASF.
 
-FROM node:14 as builder
+FROM --platform=$BUILDPLATFORM node:14 as builder
 WORKDIR /home/node/code
 COPY package.json /home/node/code
 COPY package-lock.json /home/node/code