diff --git a/.dockerignore b/.dockerignore index c4ba8e1bb..d465134d4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,13 @@ web/node_modules +web/dist .git +.github build/ tmp/ -memos \ No newline at end of file +memos +*.md +.gitignore +.golangci.yaml +.dockerignore +docs/ +.DS_Store \ No newline at end of file diff --git a/.github/workflows/build-and-push-canary-image.yml b/.github/workflows/build-and-push-canary-image.yml index 9908bc412..8b14ca3f7 100644 --- a/.github/workflows/build-and-push-canary-image.yml +++ b/.github/workflows/build-and-push-canary-image.yml @@ -4,37 +4,17 @@ on: push: branches: [main] -env: - DOCKER_PLATFORMS: | - linux/amd64 - linux/arm64 - concurrency: group: ${{ github.workflow }}-${{ github.repository }} cancel-in-progress: true jobs: - build-and-push-canary-image: + prepare: runs-on: ubuntu-latest - permissions: - contents: read - packages: write + outputs: + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} steps: - - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: ${{ env.DOCKER_PLATFORMS }} - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - install: true - platforms: ${{ env.DOCKER_PLATFORMS }} - - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -47,20 +27,11 @@ jobs: tags: | type=raw,value=canary - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 - # Frontend build. - uses: pnpm/action-setup@v4.1.0 with: version: 10 @@ -69,23 +40,144 @@ jobs: node-version: "22" cache: pnpm cache-dependency-path: "web/pnpm-lock.yaml" - - run: pnpm install + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install --frozen-lockfile working-directory: web - name: Run frontend build run: pnpm release working-directory: web - - name: Build and Push - id: docker_build + - name: Upload frontend artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: server/router/frontend/dist + retention-days: 1 + + build-push: + needs: [prepare, build-frontend] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + steps: + - uses: actions/checkout@v5 + + - name: Download frontend artifacts + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: server/router/frontend/dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push by digest + id: build uses: docker/build-push-action@v6 with: context: . file: ./scripts/Dockerfile - platforms: ${{ env.DOCKER_PLATFORMS }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - BUILDKIT_INLINE_CACHE=1 + platforms: ${{ matrix.platform }} + labels: ${{ needs.prepare.outputs.labels }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true + + - 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-${{ strategy.job-index }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: [prepare, build-push] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Create manifest list and push (Docker Hub) + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ needs.prepare.outputs.tags }} + + - name: Create manifest list and push (GHCR) + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map(sub("neosmemo/memos"; "ghcr.io/usememos/memos") | "-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ needs.prepare.outputs.tags }} + + - name: Inspect images + run: | + docker buildx imagetools inspect neosmemo/memos:canary + docker buildx imagetools inspect ghcr.io/usememos/memos:canary diff --git a/.github/workflows/build-and-push-stable-image.yml b/.github/workflows/build-and-push-stable-image.yml index 52a94a920..7df3e0db8 100644 --- a/.github/workflows/build-and-push-stable-image.yml +++ b/.github/workflows/build-and-push-stable-image.yml @@ -7,55 +7,23 @@ on: tags: - "v*.*.*" -env: - DOCKER_PLATFORMS: | - linux/amd64 - linux/arm/v7 - linux/arm64 - jobs: - build-and-push-image: + prepare: runs-on: ubuntu-latest - permissions: - contents: read - packages: write + outputs: + version: ${{ steps.version.outputs.version }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} steps: - - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: ${{ env.DOCKER_PLATFORMS }} - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - install: true - platforms: ${{ env.DOCKER_PLATFORMS }} - - name: Extract version + id: version run: | if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then - echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT else - echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV + echo "version=${GITHUB_REF_NAME#release/}" >> $GITHUB_OUTPUT fi - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -64,15 +32,19 @@ jobs: neosmemo/memos ghcr.io/usememos/memos tags: | - type=semver,pattern={{version}},value=${{ env.VERSION }} - type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }} + type=semver,pattern={{version}},value=${{ steps.version.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }} type=raw,value=stable flavor: | latest=false labels: | - org.opencontainers.image.version=${{ env.VERSION }} + org.opencontainers.image.version=${{ steps.version.outputs.version }} + + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 - # Frontend build. - uses: pnpm/action-setup@v4.1.0 with: version: 10 @@ -81,23 +53,145 @@ jobs: node-version: "22" cache: pnpm cache-dependency-path: "web/pnpm-lock.yaml" - - run: pnpm install + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install --frozen-lockfile working-directory: web - name: Run frontend build run: pnpm release working-directory: web - - name: Build and Push - id: docker_build + - name: Upload frontend artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: server/router/frontend/dist + retention-days: 1 + + build-push: + needs: [prepare, build-frontend] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 + steps: + - uses: actions/checkout@v5 + + - name: Download frontend artifacts + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: server/router/frontend/dist + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build and push by digest + id: build uses: docker/build-push-action@v6 with: context: . file: ./scripts/Dockerfile - platforms: ${{ env.DOCKER_PLATFORMS }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - BUILDKIT_INLINE_CACHE=1 + platforms: ${{ matrix.platform }} + labels: ${{ needs.prepare.outputs.labels }} + cache-from: type=gha,scope=build-${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }} + outputs: type=image,name=neosmemo/memos,push-by-digest=true,name-canonical=true,push=true + + - 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-${{ strategy.job-index }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + needs: [prepare, build-push] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Create manifest list and push (Docker Hub) + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ needs.prepare.outputs.tags }} + + - name: Create manifest list and push (GHCR) + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map(sub("neosmemo/memos"; "ghcr.io/usememos/memos") | "-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'neosmemo/memos@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ needs.prepare.outputs.tags }} + + - name: Inspect images + run: | + docker buildx imagetools inspect neosmemo/memos:stable + docker buildx imagetools inspect ghcr.io/usememos/memos:stable diff --git a/scripts/Dockerfile b/scripts/Dockerfile index c58a6347c..a678e6199 100644 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -1,19 +1,33 @@ FROM golang:1.25-alpine AS backend WORKDIR /backend-build + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +# Copy go mod files and download dependencies (cached layer) COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download + +# Copy source code COPY . . + # Please build frontend first, so that the static files are available. # Refer to `pnpm release` in package.json for the build command. RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - go build -ldflags="-s -w" -o memos ./cmd/memos + CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -extldflags '-static'" -o memos ./cmd/memos FROM alpine:latest AS monolithic WORKDIR /usr/local/memos -RUN apk add --no-cache tzdata -ENV TZ="UTC" +# Install runtime dependencies in single layer +RUN apk add --no-cache tzdata ca-certificates && \ + mkdir -p /var/opt/memos + +ENV TZ="UTC" \ + MEMOS_MODE="prod" \ + MEMOS_PORT="5230" COPY --from=backend /backend-build/memos /usr/local/memos/ COPY ./scripts/entrypoint.sh /usr/local/memos/ @@ -21,10 +35,6 @@ COPY ./scripts/entrypoint.sh /usr/local/memos/ EXPOSE 5230 # Directory to store the data, which can be referenced as the mounting point. -RUN mkdir -p /var/opt/memos VOLUME /var/opt/memos -ENV MEMOS_MODE="prod" -ENV MEMOS_PORT="5230" - ENTRYPOINT ["./entrypoint.sh", "./memos"]