iTranslated by AI
Migrating Docusaurus Documentation from App Engine to Cloud Run
This article is for the 19th day of the Google Cloud Champion Innovators Advent Calendar 2024.
Hello. In the Zenn team, we have internal documentation primarily for development members, where we record specifications and architecture. We use Docusaurus because we don't want to spend too much effort, but we are happy to be able to manage versions on GitHub along with the source code.
If you write Markdown files according to the conventions, you can build them as HTML for manuals, which can be served using Node.js.
Want to migrate from App Engine to Cloud Run
Docusaurus handles everything from building to server startup via the docusaurus command, so we don't have any trouble in this regard. One point is that since it's internal documentation, there was a requirement to restrict access to specific members only. To address this, we used to deploy to App Engine and protect it with IAP.
Since the viewing timing is limited and the users are development members, we have configured it to scale to zero. Naturally, it results in a cold start when accessed, and the App Engine standard environment VM has a slight lag in startup (about 5 seconds), which caused a tiny bit of stress. Because of this, I wanted to migrate to a container-based environment, specifically Cloud Run. However, to protect an app running on Cloud Run with IAP, you must include Cloud Load Balancing. We decided to postpone the migration as it felt like too much for internal documentation.
However, I wanted to try running it on Cloud Run so that I'm ready whenever we decide to migrate. In this article, I will create a Docker image and deploy it to Cloud Run.
Changing the build method to Docker
Regarding the documentation, on App Engine, we were configuring the application in app.yml, building with yarn build, and starting it with yarn start. In migrating to Cloud Run, I decided to build a Docker image and deploy from that image. So, first, let's prepare the Dockerfile.
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
COPY . .
RUN yarn build
FROM node:20-slim AS runner
ENV NODE_ENV=production
WORKDIR /app
# We use a whitelist to ensure unnecessary files are not copied
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.docusaurus ./.docusaurus
COPY --from=builder /app/build ./build
COPY --from=builder /app/public ./public
COPY --from=builder /app/docs-custom.css ./
COPY --from=builder /app/docs-sidebars.js ./
COPY --from=builder /app/docusaurus.config.js ./
COPY --from=builder /app/package.json ./
CMD ["yarn", "start"]
It might not be necessary to use multi-stage builds, but I am moving only the necessary files to keep the deployed image size as small as possible. Note that for the Dockerfile, guidelines are provided in the official documentation, and you can follow those if you prefer. In the official Dockerfile, they don't explicitly move files one by one but use build artifacts by doing something like FROM prod AS serve.
package.json also requires attention. Since it needs to start on the port number determined by Cloud Run, we write the startup script to use the PORT environment variable.
{
"scripts": {
"docusaurus": "docusaurus",
"build": "docusaurus build",
"start": "docusaurus serve -p $PORT",
},
...
}
Creating cloudbuild.yml
I will use Cloud Build to build the image and deploy it to Cloud Run. It is assumed that the source code will be integrated from GitHub. Let's use the prepared Dockerfile.
steps:
- id: build
name: gcr.io/cloud-builders/docker
args:
- build
- --file=Dockerfile
- --tag=$_ARTIFACT_REPOSITORY_IMAGE_TAG
- .
dir: "docs"
- id: push
name: gcr.io/cloud-builders/docker
args:
- push
- $_ARTIFACT_REPOSITORY_IMAGE_TAG
dir: "docs"
waitFor: ["build"]
- id: deploy
name: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine
entrypoint: gcloud
args:
- run
- deploy
- docs
- --quiet
- --platform=managed
- --project=$PROJECT_ID
- --region=$_REGION
- --image=$_ARTIFACT_REPOSITORY_IMAGE_TAG
- --service-account=$_CLOUD_RUN_SERVICE_ACCOUNT
- --revision-suffix=${SHORT_SHA}-${_SHORT_BUILD_ID}
- --tag=latest
- --concurrency=80
- --cpu=1
- --memory=512Mi
- --max-instances=1
- --min-instances=0
- --no-use-http2
- --no-allow-unauthenticated
dir: "docs"
waitFor: ["push"]
substitutions:
_REGION: asia-northeast1
_CLOUD_RUN_SERVICE_ACCOUNT: terraform
_ARTIFACT_REPOSITORY_IMAGE_NAME: terraform
_ARTIFACT_REPOSITORY_IMAGE_TAG: "${_ARTIFACT_REPOSITORY_IMAGE_NAME}:${SHORT_SHA}"
# Display image information generated in the build result
# https://cloud.google.com/build/docs/building/build-containers
images:
- $_ARTIFACT_REPOSITORY_IMAGE_TAG
options:
logging: CLOUD_LOGGING_ONLY
Please fill in the Artifact Registry image name and other details to suit your environment. Now we are ready.
Deploying to Cloud Run
Using the cloudbuild.yml we just prepared, I will deploy the documentation to Cloud Run. Depending on your Cloud Build trigger configuration, if you are using the GitHub App trigger, you can push to a branch to start the build.

I was able to view the documentation built by Docusaurus. (It is Forbidden by default because of --no-allow-unauthenticated, but I temporarily connected it to Cloud Load Balancing to display it after IAP authentication.)
Conclusion
I migrated the documentation built with Docusaurus so that it can run on Cloud Run. You can enable IAP by connecting it with Cloud Load Balancing. If the time comes to fully transition to Cloud Run, I would like to add those steps as well.
Discussion