DEV Community

Mohammad Reza Karimi
Mohammad Reza Karimi

Posted on • Edited on

How to use Docker Multistage to make tiny images (TS and Golang)

Have you ever wondered how you can remove redundant build dependencies from your docker image?

Did you know your image can be easily optimized?

We're not much language-related in this article, and reading all examples can be beneficial.

TS example

For example, maybe you've always been using this kind of Dockerfile to build and run your Typescript project:

FROM node:18-alpine3.15
WORKDIR /usr/app
COPY package.json .
RUN npm install --include=dev
COPY tsconfig.json .
COPY src src
RUN ["npm", "run", "build"]
CMD ["npm", "run", "start"]
Enter fullscreen mode Exit fullscreen mode

Simple, but it can be smaller without losing anything important,
Because, on the line

RUN ["npm", "run", "build"]
Enter fullscreen mode Exit fullscreen mode

We are converting some TS codes to JS codes with some Tools (dev dependencies)
What happens if we remove those tools after conversion? Nothing!

Have a look at picture below, we've built our image in three stages, instead of one, and we saved 35% of space!

optimizing docker builds with multistage builds

With code below:

FROM node:18-alpine3.15 as ts-compiler
WORKDIR /usr/app
COPY package*.json ./
COPY tsconfig.json ./
RUN npm install --include=dev
COPY src src
# Build the project (TS to JS conversion)
RUN ["npm", "run", "build"]

FROM node:18-alpine3.15 as ts-remover
WORKDIR /usr/app
# We need package.json again
COPY --from=ts-compiler /usr/app/package*.json ./
# Move built codes from last stage here
COPY --from=ts-compiler /usr/app/build ./
# We don't need dev dependencies anymore
RUN npm install --omit=dev

# Using google optimized containers can make it even smaller
FROM gcr.io/distroless/nodejs:18
WORKDIR /usr/app
COPY --from=ts-remover /usr/app ./
USER 1000
CMD ["index.js"]
Enter fullscreen mode Exit fullscreen mode

Be careful about environment variables

Imagine you are using a special package, for example Puppeteer
Puppeteer will download a chrome browser when you're installing it, but you can disable it if you want, by setting a ENV.
Have a look at the change we made on stage one of last code:

FROM node:18-alpine3.15 as ts-compiler
WORKDIR /usr/app
COPY package.json .

# This line is added 👇🏻
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

RUN npm install --include=dev
COPY tsconfig.json .
COPY src src
RUN ["npm", "run", "build"]
Enter fullscreen mode Exit fullscreen mode

So if you run multistage build again, you'll see a huge increment 😳
Increment after multistage
What is happening?
The fact is, you should use ENV and ARG commands per Stage, we are installing dependencies twice in two stage, so we should skip chrome installation twice!
Nothing talks more than a piece of code 😄:

FROM node:18-alpine3.15 as ts-compiler
WORKDIR /usr/app
COPY package.json .

# Define ENV once here
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
RUN npm install --include=dev
COPY tsconfig.json .
COPY src src
RUN ["npm", "run", "build"]

FROM node:18-alpine3.15 as ts-remover
WORKDIR /usr/app
COPY --from=ts-compiler /usr/app/package.json .
COPY --from=ts-compiler /usr/app/build .

# Define ENV once again!
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
RUN npm install --omit=dev

# We don't need that ENV in this stage, 
# because we're not installing anything more.
FROM gcr.io/distroless/nodejs:18
WORKDIR /usr/app
COPY --from=ts-remover /usr/app ./
USER 1000
CMD ["index.js"]
Enter fullscreen mode Exit fullscreen mode

Go example

Go containers (and any other compile language) can be optimized even more. (much more 😄)
Because their runtime is tied to them, you don't need something like Nodejs installed and you can drop compiler itself in the second stage and just use a binary executable.

FROM golang:1.18-alpine3.15 AS builder
WORKDIR /app
# Copy go.mod and go.sum first, because of caching reasons.
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
# Compile project
RUN CGO_ENABLED=0 GOOS=linux go build -a -o main .

# Use another clean image without Golang compiler.
FROM alpine:3.15 AS production
COPY --from=builder /app/main .
CMD ["./main"]
Enter fullscreen mode Exit fullscreen mode

Optimizing Golang Container Image Size

It's right! we saved about 96% of our space!
First result is without multistage technique,
Second is using alpine-3.15 as production.
Third is using gcr.io/distroless/static-debian11 that is the smallest image out there.

Including UI

You can even add a UI to your Dockerfile in another stage!
In this case, I have a Svelte SPA project beside my project (in ui folder) and I'm serving it from Go Backend.

FROM golang:1.18-alpine3.15 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -o main .

# This stage convert svelte project to vanilla HTML, CSS, JS.
FROM node:18-alpine3.15 AS frontend
WORKDIR /ui
COPY ui .
RUN npm install
RUN npm run build

FROM alpine:3.15 AS production
COPY --from=builder /app/main .
COPY --from=frontend /ui/public /ui
CMD ["./main"]
Enter fullscreen mode Exit fullscreen mode

This code is a part of Blogo project.
https://github.com/arshamalh/blogo

I hope you learned something from me here. 😃✌🏻
Any suggestion or correction is welcome.

Top comments (0)