Reputation: 1161
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
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:
pnpm
and working dirturbo 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?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 installedfull
directory contains the code to build3. 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 applicationbuild
stage installs all dependencies (with lockfile frozen) and builds the projectNB! 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