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,6 +27,69 @@ jobs: tags: | type=raw,value=canary + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v4.1.0 + with: + version: 10 + - uses: actions/setup-node@v5 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - 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: 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: @@ -60,32 +103,81 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - # Frontend build. - - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - uses: actions/setup-node@v5 - with: - node-version: "22" - cache: pnpm - cache-dependency-path: "web/pnpm-lock.yaml" - - run: pnpm install - working-directory: web - - name: Run frontend build - run: pnpm release - working-directory: web - - - name: Build and Push - id: docker_build + - 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,41 +7,102 @@ on: tags: - "v*.*.*" -env: - DOCKER_PLATFORMS: | - linux/amd64 - linux/arm/v7 - linux/arm64 - jobs: - build-and-push-image: + prepare: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + steps: + - name: Extract version + id: version + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF_NAME#release/}" >> $GITHUB_OUTPUT + fi + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + neosmemo/memos + ghcr.io/usememos/memos + tags: | + 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=${{ steps.version.outputs.version }} + + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v4.1.0 + with: + version: 10 + - uses: actions/setup-node@v5 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: "web/pnpm-lock.yaml" + - 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: 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 - 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 - run: | - if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then - echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV - else - echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV - fi - name: Login to Docker Hub uses: docker/login-action@v3 @@ -56,48 +117,81 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - neosmemo/memos - ghcr.io/usememos/memos - tags: | - type=semver,pattern={{version}},value=${{ env.VERSION }} - type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }} - type=raw,value=stable - flavor: | - latest=false - labels: | - org.opencontainers.image.version=${{ env.VERSION }} - - # Frontend build. - - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - uses: actions/setup-node@v5 - with: - node-version: "22" - cache: pnpm - cache-dependency-path: "web/pnpm-lock.yaml" - - run: pnpm install - working-directory: web - - name: Run frontend build - run: pnpm release - working-directory: web - - - name: Build and Push - id: docker_build + - 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"]