diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..452d12382 --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -0,0 +1,70 @@ +name: Build Docker Image + +on: + workflow_call: + inputs: + tags: + required: true + type: string + description: "The tags to use for the Docker image" + target: + required: true + type: string + description: "The target to use for the Docker image" + build_args: + required: true + type: string + description: "The build args to use for the Docker image" + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [20.16] + os: [ubuntu-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-buildx-${{ github.sha }} + path: /tmp/.buildx-cache + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + username: ${{ secrets.DOCKER_HUB_USERNAME }} + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + build-args: | + ${{ inputs.build_args }} + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG=${{ vars.SENTRY_ORG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + context: . + target: ${{ inputs.target }} + push: true + tags: ${{ inputs.tags }} + + # Temp fix + # 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 + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/push-to-dokku.yml b/.github/workflows/push-to-dokku.yml new file mode 100644 index 000000000..0e81a7cf1 --- /dev/null +++ b/.github/workflows/push-to-dokku.yml @@ -0,0 +1,25 @@ +name: Push to Dokku + +on: + workflow_call: + inputs: + git_remote_url: + required: true + type: string + description: "The remote URL to push to" + deploy_docker_image: + required: true + type: string + description: "The name of the image to push" + +jobs: + push: + runs-on: ubuntu-latest + + steps: + - name: Push + uses: dokku/github-action@v1.4.0 + with: + git_remote_url: ${{ inputs.git_remote_url }} + ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} + deploy_docker_image: ${{ inputs.deploy_docker_image }} diff --git a/.github/workflows/techlabblog-deploy-dev.yml b/.github/workflows/techlabblog-deploy-dev.yml new file mode 100644 index 000000000..81b273aab --- /dev/null +++ b/.github/workflows/techlabblog-deploy-dev.yml @@ -0,0 +1,35 @@ +name: Techlab Blog | Dev | Deploy + +on: + push: + branches: [main] + paths: + - "apps/techlabblog/**" + - "Dockerfile" + - ".github/workflows/techlabblog-deploy-dev.yml" + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-docker-image: + name: Build Docker Image + uses: ./.github/workflows/build-docker-image.yml + secrets: inherit + with: + tags: "codeforafrica/techlabblog:${{ github.sha }}" + target: "techlabblog-runner" + build_args: | + SENTRY_ENVIRONMENT=development + SENTRY_DSN: ${{ vars.TECHLABBLOG_SENTRY_DSN }} + + push-to-dokku: + name: Push to Dokku + needs: [build-docker-image] + uses: ./.github/workflows/push-to-dokku.yml + secrets: inherit + with: + git_remote_url: "ssh://azureuser@ui-1.dev.codeforafrica.org/techlabblog-ui" + deploy_docker_image: "codeforafrica/techlabblog:${{ github.sha }}" diff --git a/Dockerfile b/Dockerfile index 4310757e4..6ac4102b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -813,4 +813,78 @@ USER nextjs # https://nextjs.org/docs/pages/api-reference/next-config-js/output CMD ["node", "apps/vpnmanager/server.js"] +# ============================================================================ +# Techlab Blog +# ============================================================================ + +# +# techlabblog-deps: image with all techlabblog dependencies +# ----------------------------------------------------- + +FROM base-deps AS techlabblog-deps + +COPY apps/techlabblog/package.json ./apps/techlabblog/package.json + +# Use virtual store: https://pnpm.io/cli/fetch#usage-scenario +RUN pnpm --filter "./apps/techlabblog" install --offline --frozen-lockfile + +# +# techlabblog-builder: image that uses deps to build shippable output +# ------------------------------------------------------------------ + +FROM base-builder AS techlabblog-builder + +ARG NEXT_TELEMETRY_DISABLED \ + # Next.js / Payload (build time) + PORT \ + # Next.js (runtime) + NEXT_PUBLIC_APP_NAME="Techlab Blog" \ + NEXT_PUBLIC_APP_URL \ + NEXT_PUBLIC_SENTRY_DSN \ + NEXT_PUBLIC_SEO_DISABLED \ + NEXT_PUBLIC_GOOGLE_ANALYTICS \ + # Sentry (build time) + SENTRY_AUTH_TOKEN \ + SENTRY_ENVIRONMENT \ + SENTRY_ORG \ + SENTRY_PROJECT + +# This is in app-builder instead of base-builder just incase app-deps adds deps +COPY --from=techlabblog-deps /workspace/node_modules ./node_modules + +COPY --from=techlabblog-deps /workspace/apps/techlabblog/node_modules ./apps/techlabblog/node_modules + +COPY apps/techlabblog ./apps/techlabblog + +RUN pnpm --filter "./apps/techlabblog" build + +# +# techlabblog-runner: final deployable image +# ----------------------------------------- + +FROM base-runner AS techlabblog-runner + +RUN set -ex \ + # Create nextjs cache dir w/ correct permissions + && mkdir -p ./apps/techlabblog/.next \ + && chown nextjs:nodejs ./apps/techlabblog/.next + +# PNPM +# symlink some dependencies +COPY --from=techlabblog-builder --chown=nextjs:nodejs /workspace/node_modules ./node_modules + +# Next.js +# Public assets +COPY --from=techlabblog-builder --chown=nextjs:nodejs /workspace/apps/techlabblog/public ./apps/techlabblog/public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=techlabblog-builder --chown=nextjs:nodejs /workspace/apps/techlabblog/.next/standalone ./apps/techlabblog +COPY --from=techlabblog-builder --chown=nextjs:nodejs /workspace/apps/techlabblog/.next/static ./apps/techlabblog/.next/static +USER nextjs + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD ["node", "apps/techlabblog/server.js"] + diff --git a/Makefile b/Makefile index 17e7a31f3..ecdbd18f5 100644 --- a/Makefile +++ b/Makefile @@ -33,5 +33,9 @@ pesayetu: roboshield: $(COMPOSE_BUILD_ENV) $(COMPOSE) --env-file apps/roboshield/.env.local up roboshield --build +techlabblog: + $(COMPOSE_BUILD_ENV) $(COMPOSE) --env-file apps/techlabblog/.env.local up techlabblog --build + vpnmanager: $(COMPOSE_BUILD_ENV) $(COMPOSE) --env-file apps/vpnmanager/.env.local up vpnmanager --build + diff --git a/apps/techlabblog/next.config.mjs b/apps/techlabblog/next.config.mjs index 378cf0147..289f50824 100644 --- a/apps/techlabblog/next.config.mjs +++ b/apps/techlabblog/next.config.mjs @@ -2,6 +2,7 @@ import createMDX from "@next/mdx"; /** @type {import('next').NextConfig} */ const nextConfig = { + output: "standalone", pageExtensions: ["mdx", "tsx"], reactStrictMode: true, transpilePackages: ["@commons-ui/core", "@commons-ui/next"], diff --git a/apps/techlabblog/package.json b/apps/techlabblog/package.json index 28c2156f2..bcd9e5b63 100644 --- a/apps/techlabblog/package.json +++ b/apps/techlabblog/package.json @@ -22,6 +22,7 @@ "@mui/material-nextjs": "catalog:", "@mui/utils": "catalog:", "@next/mdx": "catalog:", + "@next/third-parties": "catalog:", "date-fns": "catalog:", "gray-matter": "catalog:", "@types/mdx": "catalog:", diff --git a/docker-compose.yml b/docker-compose.yml index c39e1ed87..1932969de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,6 +92,27 @@ services: ports: - 3000:3000 + mongodb: + image: mongo:6.0.13 + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME:-root} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD:-rootpassword} + ports: + - 27017:27017 + volumes: + - db_data:/data/db + - ./mongo-keyfile:/etc/mongo-keyfile + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + # https://medium.com/workleap/the-only-local-mongodb-replica-set-with-docker-compose-guide-youll-ever-need-2f0b74dd8384 + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'host.docker.internal:27017'}]}) }" | mongosh --port 27017 --quiet + interval: 5s + timeout: 30s + start_period: 0s + retries: 30 + command: + ["--replSet", "rs0", "--bind_ip_all", "--keyFile", "/etc/mongo-keyfile"] pesayetu: build: context: . @@ -142,27 +163,22 @@ services: ports: - 3000:3000 - mongodb: - image: mongo:6.0.13 + techlabblog: + build: + context: . + target: techlabblog-runner + args: + - SENTRY_AUTH_TOKEN + - SENTRY_ORG + - SENTRY_ENV + - SENTRY_PROJECT environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME:-root} - MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD:-rootpassword} + SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN} + SENTRY_ORG: ${SENTRY_ORG} + SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT} + SENTRY_PROJECT: ${SENTRY_PROJECT} ports: - - 27017:27017 - volumes: - - db_data:/data/db - - ./mongo-keyfile:/etc/mongo-keyfile - extra_hosts: - - "host.docker.internal:host-gateway" - healthcheck: - # https://medium.com/workleap/the-only-local-mongodb-replica-set-with-docker-compose-guide-youll-ever-need-2f0b74dd8384 - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'host.docker.internal:27017'}]}) }" | mongosh --port 27017 --quiet - interval: 5s - timeout: 30s - start_period: 0s - retries: 30 - command: - ["--replSet", "rs0", "--bind_ip_all", "--keyFile", "/etc/mongo-keyfile"] + - 3000:3000 vpnmanager: build: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1c121ba2..30660f143 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -870,7 +870,7 @@ importers: version: 14.2.8 '@next/third-parties': specifier: 'catalog:' - version: 14.2.8(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1) + version: 14.2.8(next@14.2.8(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1) '@payloadcms/bundler-webpack': specifier: 'catalog:' version: 1.0.7(@swc/core@1.7.23(@swc/helpers@0.5.5))(ajv@8.17.1)(payload@2.28.0(@swc/helpers@0.5.5)(@types/react@18.3.5)(encoding@0.1.13)(typescript@5.5.4)(webpack@5.93.0(@swc/core@1.7.23(@swc/helpers@0.5.5))))(sass@1.69.4) @@ -1919,7 +1919,7 @@ importers: version: 14.2.8 '@next/third-parties': specifier: 'catalog:' - version: 14.2.8(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1) + version: 14.2.8(next@14.2.8(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1) '@payloadcms/bundler-webpack': specifier: 'catalog:' version: 1.0.7(@swc/core@1.7.23(@swc/helpers@0.5.5))(ajv@8.17.1)(payload@2.28.0(@swc/helpers@0.5.5)(@types/react@18.3.5)(encoding@0.1.13)(typescript@5.5.4)(webpack@5.93.0(@swc/core@1.7.23(@swc/helpers@0.5.5))))(sass@1.69.4) @@ -2095,6 +2095,9 @@ importers: '@next/mdx': specifier: 'catalog:' version: 14.2.8(@mdx-js/loader@3.0.1(webpack@5.93.0))(@mdx-js/react@3.0.1(@types/react@18.3.5)(react@18.3.1)) + '@next/third-parties': + specifier: 'catalog:' + version: 14.2.8(next@14.2.8(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1) '@types/mdx': specifier: 'catalog:' version: 2.0.13 @@ -17858,7 +17861,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.8': optional: true - '@next/third-parties@14.2.8(next@14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1)': + '@next/third-parties@14.2.8(next@14.2.8(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4))(react@18.3.1)': dependencies: next: 14.2.8(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.46.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.69.4) react: 18.3.1