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?

6
  • 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. Commented Jun 10, 2022 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. Commented Jun 10, 2022 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. Commented Jun 10, 2022 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. Commented Jun 10, 2022 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). Commented Jun 10, 2022 at 17:52

2 Answers 2

5

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"]
Sign up to request clarification or add additional context in comments.

Comments

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.

1 Comment

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.