diff --git a/.dockerignore b/.dockerignore index edbf8525..698137cb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,11 +16,11 @@ **/compose* **/Dockerfile* **/node_modules +!.next/standalone/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml -**/.next README.md config/ k3d/ diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c7d36f33..afb7fca4 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,9 +1,4 @@ -name: Docker - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +name: Docker CI on: schedule: @@ -13,7 +8,6 @@ on: - main - feature/** - dev - # Publish semver tags as releases. tags: [ 'v*.*.*' ] paths-ignore: - 'docs/**' @@ -26,89 +20,56 @@ on: merge_group: env: - # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: pre-commit: name: Linting Checks runs-on: ubuntu-22.04 steps: - - - name: Checkout repository + - name: Checkout repository uses: actions/checkout@v4 - - - name: Install python + + - name: Install python uses: actions/setup-python@v5 with: python-version: 3.x - - - name: Check files + + - name: Check files uses: pre-commit/action@v3.0.1 - - - name: Install pnpm + + - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 run_install: false - - - name: Install Node.js + + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - - - name: Install dependencies + + - name: Install dependencies run: pnpm install - - - name: Lint frontend + + - name: Lint frontend run: pnpm run lint build: name: Docker Build & Push - if: github.repository == 'gethomepage/homepage' + if: github.repository == 'gethomepage/homepage' runs-on: self-hosted - needs: - - pre-commit + needs: [ pre-commit ] permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - # Login to Docker Registry - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Login to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Setup QEMU - # https://github.com/marketplace/actions/docker-setup-buildx#with-qemu - - name: Setup QEMU - uses: docker/setup-qemu-action@v3.6.0 - - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 @@ -119,8 +80,57 @@ jobs: flavor: | latest=auto - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action + - name: Next.js build cache + uses: actions/cache@v4 + with: + path: .next/cache + key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }} + restore-keys: | + nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Build app + run: | + NEXT_PUBLIC_BUILDTIME="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}" \ + NEXT_PUBLIC_VERSION="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" \ + NEXT_PUBLIC_REVISION="${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}" \ + pnpm run build + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3.6.0 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@v6 @@ -130,18 +140,15 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | + CI=true BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} - # https://github.com/docker/setup-qemu-action#about - # platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 platforms: linux/amd64,linux/arm64 cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 + # https://github.com/docker/build-push-action/issues/252 / https://github.com/moby/buildkit/issues/1896 - name: Move cache run: | rm -rf /tmp/.buildx-cache diff --git a/Dockerfile b/Dockerfile index 7963407c..ec59c6b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,67 +1,63 @@ -# Install dependencies only when needed -FROM docker.io/node:22-alpine AS deps - -WORKDIR /app - -COPY --link package.json pnpm-lock.yaml* ./ - -SHELL ["/bin/ash", "-xeo", "pipefail", "-c"] -RUN apk add --no-cache libc6-compat \ - && apk add --no-cache --virtual .gyp python3 make g++ \ - && npm install -g pnpm - -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store" - -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline - -# Rebuild the source code only when needed -FROM docker.io/node:22-alpine AS builder +# ========================= +# Builder Stage +# ========================= +FROM node:22-slim AS builder WORKDIR /app +# Setup RUN mkdir config +COPY . . +ARG CI ARG BUILDTIME ARG VERSION ARG REVISION +ENV CI=$CI -COPY --link --from=deps /app/node_modules ./node_modules/ -COPY . . +# Install and build only outside CI +RUN if [ "$CI" != "true" ]; then \ + corepack enable && corepack prepare pnpm@latest --activate && \ + pnpm install --frozen-lockfile --prefer-offline && \ + NEXT_TELEMETRY_DISABLED=1 \ + NEXT_PUBLIC_BUILDTIME=$BUILDTIME \ + NEXT_PUBLIC_VERSION=$VERSION \ + NEXT_PUBLIC_REVISION=$REVISION \ + pnpm run build; \ + else \ + echo "✅ Using prebuilt app from CI context"; \ + fi -SHELL ["/bin/ash", "-xeo", "pipefail", "-c"] -RUN npm install -g pnpm \ - && pnpm run telemetry \ - && NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION pnpm run build - -# Production image, copy all the files and run next -FROM docker.io/node:22-alpine AS runner -LABEL org.opencontainers.image.title "Homepage" -LABEL org.opencontainers.image.description "A self-hosted services landing page, with docker and service integrations." +# ========================= +# Runtime Stage +# ========================= +FROM node:22-alpine AS runner +LABEL org.opencontainers.image.title="Homepage" +LABEL org.opencontainers.image.description="A self-hosted services landing page, with docker and service integrations." LABEL org.opencontainers.image.url="https://github.com/gethomepage/homepage" LABEL org.opencontainers.image.documentation='https://github.com/gethomepage/homepage/wiki' LABEL org.opencontainers.image.source='https://github.com/gethomepage/homepage' LABEL org.opencontainers.image.licenses='Apache-2.0' -ENV NODE_ENV=production - +# Setup WORKDIR /app -# Copy files from context (this allows the files to copy before the builder stage is done). -COPY --link --chown=1000:1000 package.json next.config.js ./ +# Copy some files from context COPY --link --chown=1000:1000 /public ./public/ - -# Copy files from builder -COPY --link --from=builder --chown=1000:1000 /app/.next/standalone ./ -COPY --link --from=builder --chown=1000:1000 /app/.next/static/ ./.next/static/ COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/ +# Copy only necessary files from the build stage +COPY --link --from=builder --chown=1000:1000 /app/.next/standalone/ ./ +COPY --link --from=builder --chown=1000:1000 /app/.next/static/ ./.next/static + RUN apk add --no-cache su-exec +ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 ENV PORT=3000 EXPOSE $PORT HEALTHCHECK --interval=10s --timeout=3s --start-period=20s \ - CMD wget --no-verbose --tries=1 --spider --no-check-certificate http://127.0.0.1:$PORT/api/healthcheck || exit 1 + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:$PORT/api/healthcheck || exit 1 ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", "server.js"]