diff --git a/.github/workflows/build-and-push-canary-image.yml b/.github/workflows/build-and-push-canary-image.yml index 8b14ca3f7..bbd8fb96f 100644 --- a/.github/workflows/build-and-push-canary-image.yml +++ b/.github/workflows/build-and-push-canary-image.yml @@ -9,24 +9,6 @@ concurrency: cancel-in-progress: true jobs: - prepare: - runs-on: ubuntu-latest - outputs: - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - steps: - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - neosmemo/memos - ghcr.io/usememos/memos - flavor: | - latest=false - tags: | - type=raw,value=canary - build-frontend: runs-on: ubuntu-latest steps: @@ -64,7 +46,7 @@ jobs: retention-days: 1 build-push: - needs: [prepare, build-frontend] + needs: build-frontend runs-on: ubuntu-latest permissions: contents: read @@ -110,7 +92,6 @@ jobs: context: . file: ./scripts/Dockerfile 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 @@ -130,7 +111,7 @@ jobs: retention-days: 1 merge: - needs: [prepare, build-push] + needs: build-push runs-on: ubuntu-latest permissions: contents: read @@ -146,6 +127,18 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + neosmemo/memos + ghcr.io/usememos/memos + flavor: | + latest=false + tags: | + type=raw,value=canary + - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -159,23 +152,13 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: Create manifest list and push (Docker Hub) + - name: Create manifest list and push working-directory: /tmp/digests run: | - docker buildx imagetools create \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + 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 }} + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} - name: Inspect images run: | diff --git a/.github/workflows/build-and-push-stable-image.yml b/.github/workflows/build-and-push-stable-image.yml index 7df3e0db8..1aefa9cb4 100644 --- a/.github/workflows/build-and-push-stable-image.yml +++ b/.github/workflows/build-and-push-stable-image.yml @@ -12,8 +12,6 @@ jobs: 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 @@ -24,22 +22,6 @@ jobs: 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: @@ -124,7 +106,6 @@ jobs: context: . file: ./scripts/Dockerfile 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 @@ -160,6 +141,22 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + neosmemo/memos + ghcr.io/usememos/memos + tags: | + type=semver,pattern={{version}},value=${{ needs.prepare.outputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.prepare.outputs.version }} + type=raw,value=stable + flavor: | + latest=false + labels: | + org.opencontainers.image.version=${{ needs.prepare.outputs.version }} + - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -173,23 +170,13 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: Create manifest list and push (Docker Hub) + - name: Create manifest list and push working-directory: /tmp/digests run: | - docker buildx imagetools create \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + 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 }} + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} - name: Inspect images run: | diff --git a/scripts/Dockerfile b/scripts/Dockerfile index a678e6199..109d9c06e 100644 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS backend +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS backend WORKDIR /backend-build # Install build dependencies @@ -9,32 +9,47 @@ COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download -# Copy source code +# Copy source code (use .dockerignore to exclude unnecessary files) COPY . . # Please build frontend first, so that the static files are available. # Refer to `pnpm release` in package.json for the build command. +ARG TARGETOS TARGETARCH VERSION COMMIT RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -extldflags '-static'" -o memos ./cmd/memos + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ + go build \ + -trimpath \ + -ldflags="-s -w -extldflags '-static'" \ + -tags netgo,osusergo \ + -o memos \ + ./cmd/memos -FROM alpine:latest AS monolithic +# Use minimal Alpine with security updates +FROM alpine:3.21 AS monolithic WORKDIR /usr/local/memos -# Install runtime dependencies in single layer +# Install runtime dependencies and create non-root user in single layer RUN apk add --no-cache tzdata ca-certificates && \ - mkdir -p /var/opt/memos + addgroup -g 10001 -S nonroot && \ + adduser -u 10001 -S -G nonroot -h /var/opt/memos nonroot && \ + mkdir -p /var/opt/memos && \ + chown -R nonroot:nonroot /var/opt/memos + +# Copy binary and entrypoint +COPY --from=backend /backend-build/memos /usr/local/memos/memos +COPY --from=backend --chmod=755 /backend-build/scripts/entrypoint.sh /usr/local/memos/entrypoint.sh + +# Switch to non-root user +USER nonroot:nonroot + +# Data directory +VOLUME /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/ - EXPOSE 5230 -# Directory to store the data, which can be referenced as the mounting point. -VOLUME /var/opt/memos - -ENTRYPOINT ["./entrypoint.sh", "./memos"] +ENTRYPOINT ["/usr/local/memos/entrypoint.sh", "/usr/local/memos/memos"]