3

I'm trying to build a Docker Image of my Nextjs frontend(React) application for production and am currently stuck at typescript integration.

Here's the Dockerfile.

FROM node:14-alpine3.14 as deps

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
EXPOSE 4500
RUN apk add --no-cache libc6-compat

RUN mkdir /app && chown -R node:node /app
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --production && npm cache clean --force

FROM node:14-alpine3.14 as build
RUN mkdir /app && chown -R node:node /app
WORKDIR /app
ENV NODE_ENV=production
COPY --chown=node:node . ./
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build


FROM node:14-alpine3.14 as prod
RUN mkdir /app && chown -R node:node /app
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=7777
COPY --from=build /app ./
USER node
CMD ["node_modules/.bin/next", "start"]

Now this results in an error:

It looks like you're trying to use TypeScript but do not have the required package(s) installed.

Basically since I'm doing npm ci --production it doesn't install devDependencies where typescript is.

After searching I've arrived at few solutions.

Solution 1: The first one is to add typescript to dependencies. Though it is advised that since typescript is only devDependency it should not be in normal dependencies.

Solution 2: Adding typescript via npm install. Basically same as solution 1. I modified the Dockerfile as:

FROM node:14-alpine3.14 as deps

COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --production && npm cache clean --force

# Added typescript and node types here
RUN npm install --save-dev typescript @types/node

In this case the total image size becomes: 981.58 MB.

Solution 3: Doing simple npm install instead of npm ci --production.

FROM node:14-alpine3.14 as deps

COPY --chown=node:node package.json package-lock.json ./

# Simple npm install
RUN npm install && npm cache clean --force

In this case I end installing all devDependencies also. In this case the total image size is: 537.32 MB.

Now I have few questions regarding this.

Question 1: Why does adding typescript via npm install --save-dev typescript @types/node in Solution 2 results in bigger file size compared to Solution 3 where we install all the dependencies?

Question 2: If in Solution 3 I do npm ci instead of npm install the total image size comes out to be 972.59 MB. Why does using npm ci increase the image size. Shouldn't it just install exact packages based on package-lock.json.

Question 3: I looked at discussion Asked to install Typescript when already installed when building Docker image.

It suggested a solution with multi-staged build like this.

FROM gcr.io/companyX/companyX-node-base:12-alpine AS build

# Copy in only the parts needed to install dependencies
# (This avoids rebuilds if the package.json hasn’t changed)
COPY package.json package.lock .

# Install dependencies (including dev dependencies)
RUN npm install

# Copy in the rest of the project
# (include node_modules in a .dockerignore file)
COPY . .

# Build the project
RUN npm run build

# Second stage: runtime
FROM gcr.io/companyX/companyX-node-base:12-alpine

ENV NODE_ENV=production

# Again get dependencies, but this time only install
# runtime dependencies
COPY package.json package.lock .
RUN npm install

# Get the built application from the first stage
COPY --from=build /app/dist dist

# Set runtime metadata
EXPOSE 3000
CMD [ "npm", "start" ]
# CMD ["node", "dist/index.js"]

Isn't this solution bad since you end up installing dependencies twice in this case. Once in the build stage and 2nd in runner stage even if you install only production dependencies in runner stage.

I tried this solution and as expected I ended up with an image size of 1.18 GB.

Question 4: Which of the above solution is better to go for? Or is there a better way of doing this?

pythonc229
  • 45
  • 1
  • 4
  • You intrinsically need two different sets of dependencies: you need the `devDependencies` to build the application but only the `dependencies` to run it. Intrinsically you must run `npm install` or `npm ci` twice, once in development mode and once in production mode, and in different build stages. The last Dockerfile you show should be fine (which suggests this question should be a duplicate of the one you link to) and I would expect it to produce a more reasonably-sized image. – David Maze Jun 10 '22 at 13:15
  • 1
    isn't it kinda redundant to do both `npm ci` and `npm ci --production`. Since you can only use `npm install` and the app will run. There's no need to do `npm ci --production` in that case. – pythonc229 Jun 10 '22 at 15:35
  • If you're concerned about image size, then you should separately run `npm ci --production` in a separate build stage to get only the runtime `dependencies` and not the `devDependencies`, then `COPY --from` the built application from the previous built stage. – David Maze Jun 10 '22 at 15:44
  • That's one of the problems that I'm facing. `npm ci --production` alone won't work since I need typescript which is in devDependencies. If I do `npm ci --production` in a seperate stage then copy over the build then the `npm build` won't work. – pythonc229 Jun 10 '22 at 17:41
  • 2
    The last Dockerfile you quote does this correctly: `npm ci` including dev dependencies in a first stage and run `npm build` there; and then in a second stage `npm ci --production` and `COPY` the results of the build from the first stage (but not its expanded `node_modules` tree). – David Maze Jun 10 '22 at 17:52
  • Oh I see. I tried copying only the `build` folder from the `build stage` and then installing only production dependencies in the `runner` stage and it works. Thank you so much for your help :) – pythonc229 Jun 11 '22 at 05:47

2 Answers2

4

Use a container intermediate to install only packages for production

FROM node:14-alpine AS build

# Disable telemetry
ENV NEXT_TELEMETRY_DISABLED 1

WORKDIR /build

# Copy package and package-lock 
COPY package.json package-lock.json ./

# Clean install dependencies based package-lock
# Note: We also install dev deps as typeScript may be needed
RUN npm ci

# Copy files
# Use .dockerignore to avoid copying node_modules and others folders and files
COPY . .

# Build application
RUN npm run build

# =======================================
# Image generate dependencies production
# =======================================
FROM node:14-alpine AS dependencies

# Environment Production
ENV NODE_ENV production

WORKDIR /dependencies

# Copy package and package-lock 
COPY --from=build /build/package.json .
COPY --from=build /build/package-lock.json ./

# Clean install dependencies based package-lock
RUN npm ci --production

# =======================================
# Image distroless final
# =======================================
FROM gcr.io/distroless/nodejs:14

# Mark as prod, disable telemetry, set port
ENV NODE_ENV production
ENV PORT 3000
ENV NEXT_TELEMETRY_DISABLED 1

WORKDIR /app

# Copy from build
COPY --from=build /build/next.config.js .
COPY --from=build /build/public/ ./public
COPY --from=build /build/.next ./.next
COPY --from=dependencies /dependencies/node_modules ./node_modules

EXPOSE 3000

# Run app command
CMD ["node_modules/.bin/next", "start"]
user44484
  • 41
  • 3
0

For this case, you can use the base image https://github.com/ryanbekhen/feserve/pkgs/container/feserve as the production stage. This is an image that I made based on the complaints that occurred on the frontend. The base image is only around 8 MB, so it doesn't take up a lot of storage.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/33651529) – Eduardo Matsuoka Jan 24 '23 at 17:04