skip to Main Content

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?

2

Answers


  1. 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"]
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search