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
Use a container intermediate to install only packages for production
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.