Arjen
Arjen

Reputation: 1161

How to build an astro.build app inside a dockerfile (turborepo)

This is my current setup:

FROM node:lts-alpine AS base

FROM base AS builder
RUN apk update && apk add --no-cache libc6-compat
# Set working directory
WORKDIR /repo
RUN npm install -g turbo
COPY . .
COPY ./apps/page/.env.docker ./apps/page/.env

RUN turbo prune --scope=@myturborepo/page --docker

# # Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk update && apk add --no-cache libc6-compat
WORKDIR /repo

# # First install the dependencies (as they change less often)
COPY --from=builder /repo/out/json/ .
COPY --from=builder /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable
RUN pnpm install --frozen-lockfile

COPY --from=builder /repo/out/full/ .

RUN pnpm dlx turbo run build --filter=@myturborepo/page

FROM base AS production
WORKDIR /app

ENV NODE_ENV=production

COPY --from=installer /repo/apps/page/astro.config.mjs .
COPY --from=installer /repo/apps/page/package.json .
COPY --from=installer /repo/apps/page/node_modules .
COPY --from=installer /repo/apps/page/dist ./dist

# # Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 astrojs

USER astrojs

ARG PORT=4321
ENV PORT=$PORT
ENV HOST="0.0.0.0"

CMD ["node", "dist/server/entry.mjs"]

Almost every step is copied from the nextjs expample but when i run this it can't find the react package (which is a dependency in the package.json). When i inspect the files in the docker container it seems that it has symlinks in the node_modules pointing to ../../node_modules which now don't exist anymore because of the pruning etc.

I got it to work by just copying the entire app and running it then but that caused the image to be around 700mb. In comparison, nextjs is around 190mb.

Does anyone know what steps i'm missing or what i'm doing wrong?

Upvotes: 0

Views: 161

Answers (1)

e3stpavel
e3stpavel

Reputation: 101

First things first I highly recommend you to check turborepo + docker docs. You can also refer to Astro + Docker docs.

Given the fact that you've set up the monorepo correctly and you are able to run and build your setup locally let's "dockerize" our Astro app.

The steps you need to perform:

  1. Set up the base stage where you can install pnpm and working dir
  2. Use turbo prune to prune unnecessary dependencies - the reason is that we want to build only one (in your case @myturborepo/page) app, so why do we need to install dependencies for some other apps in monorepo as well? Seems like a waste of time and resources, doesn't it?
  3. Install the necessary dependencies and build our Astro app
  4. Copy the artifact over to some tiny image (like alpine or distroless), set up non-root user and run the server entry.mjs

Let's take a look at each step in more detail:

1. The base stage

# I am using alpine here but you might need to use node:<version> or node:<version>-slim
FROM node:lts-alpine AS base

# Install pnpm and enable corepack
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

# Set the working dir
WORKDIR /app

2. Prune unnecessary dependencies

# https://turbo.build/repo/docs/guides/tools/docker
FROM base AS prune

# Install turbo as global dependency
RUN pnpm install --global turbo

# Copy all code (make sure you have proper .dockerignore or this will take forever)
COPY . .

# Prune dependencies
RUN turbo prune @myturborepo/page --docker

turbo prune <scope> --docker outputs to two directories (json and full, refer to docs).

  • json directory contains the data about packages needed to be installed
  • full directory contains the code to build

3. Install dependencies and build the Astro app

# Install **runtime** dependencies
FROM base AS dependencies

COPY --from=prune /app/out/json .

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod

# Install **all** dependencies
#  and then build project
FROM base AS build

COPY --from=prune /app/out/json .

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

COPY --from=prune /app/out/full .

RUN pnpm turbo build

Here we have two stages:

  • dependencies stage installs prod dependencies (with lockfile frozen) needed to run the application
  • build stage installs all dependencies (with lockfile frozen) and builds the project

NB! build stage uses your turbo pipeline to build your app. This means that if you set up, for instance check-types command to run before the build (see example turbo.json), it will run.

Example turbo.json

"tasks": {
    "check-types": {
      "outputs": [".astro/**"]
    },
    "build": {
      "dependsOn": ["check-types"],
      "outputs": ["dist/**"]
    }
}

pnpm turbo build will trigger pnpm check-types and then pnpm build. Learn more in turborepo docs.

4. Serve your app

# Serve Astro app
#  https://github.com/GoogleContainerTools/distroless
FROM gcr.io/distroless/nodejs20-debian12:nonroot

WORKDIR /app

# Copy artifact
COPY --from=dependencies --chown=nonroot:nonroot /app/node_modules node_modules
COPY --from=build --chown=nonroot:nonroot /app/apps/page/dist dist

# Run server
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD [ "./dist/server/entry.mjs" ]

This stage uses distroless Node.js non-root image to host Astro application.

We set up the working directory. Then we copy installed prod dependencies from dependencies stage and dist directory from build stage.

Lastly, we run our server entry.mjs with node.

The complete Dockerfile will look like this:

FROM node:lts-alpine AS base

# Install pnpm and enable corepack
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

# Set the working dir
WORKDIR /app

# https://turbo.build/repo/docs/guides/tools/docker
FROM base AS prune

# Install turbo as global dependency
RUN pnpm install --global turbo

# Copy all code (make sure you have proper .dockerignore or this will take forever)
COPY . .

# Prune dependencies
RUN turbo prune @myturborepo/page --docker

# Install **runtime** dependencies
FROM base AS dependencies

COPY --from=prune /app/out/json .

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod

# Install **all** dependencies
#  and then build project
FROM base AS build

COPY --from=prune /app/out/json .

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

COPY --from=prune /app/out/full .

RUN pnpm turbo build

# Serve Astro app
#  https://github.com/GoogleContainerTools/distroless
FROM gcr.io/distroless/nodejs20-debian12:nonroot

WORKDIR /app

# Copy artifact
COPY --from=dependencies --chown=nonroot:nonroot /app/node_modules node_modules
COPY --from=build --chown=nonroot:nonroot /app/apps/page/dist dist

# Run server
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD [ "./dist/server/entry.mjs" ]

One more thing

Before you can build the docker image you need to make sure you're doing this from the root of your project/repository (from the directory where your root package.json lives).

Why? This is due to fact that you're copying all the code with COPY . . in the prune stage. So if you're not going to copy root package.json then you will end up with errors like: Package was not found, pnpn-lock.yaml was not found etc. So it is important that you build the image from the root of your project.

One example to build image and run container can be to use this docker-compose.yaml in your project root:

services:
  page: # or your app name
    restart: unless-stopped
    build:
      dockerfile: apps/page/Dockerfile # path to dockerfile
      # the context here defaults to `.`, meaning that the dockerfile will use the project root as context
    ports:
      - 4321:4321

To run this simply use docker-compose up -d.

To rebuild the container you can use docker-compose up -d --build or docker-compose build.

Hope this helps :)

Upvotes: 0

Related Questions