Na wstępie chciałbym zaznaczyć, że mam bardzo podstawową wiedzę z Dockera. Mam taki plik Dockerfile, którego nie jestem autorem, zatem nie wiem w 100% jaki zamysł miał autor:
FROM node:16-alpine as builder
ENV NODE_ENV build
USER node
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build-ts
FROM node:16-alpine
ENV NODE_ENV production
USER node
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist/ ./dist
RUN npm ci
EXPOSE 3000
CMD ["node", "dist/app.js"]
W kontenerze builder najpierw kopiujemy package.json, potem instalujemy paczki a dopiero potem kopiujemy kod. Z tego, co wiem to pozwoli to Dockerowi utworzyć cache i zmiany w kodzie źródłowym nie będą wymuszać instalowania paczek od początku za każdym razem. W tym kroki instalowane są wszystkie paczki, również te "devDependencies". W docelowym kontenerze kopiujemy package.json, wynik kompilacji/transpilacji TS->JS (katalog dist) oraz musimy zainstalować paczki jeszcze raz. Za drugim razem zostaną zainstalowane tylko paczki produkcyjne (dzięki ustawieniu NODE_ENV na production).
Chciałem sprawdzić z ciekawości, czy taka optymalizacja pozwoli skrócić czas budowania obrazu:
FROM node:16-alpine as builder
ENV NODE_ENV build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build-ts
RUN npm prune --production # tutaj zmiana
FROM node:16-alpine
ENV NODE_ENV production
USER node
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules/ ./node_modules # tutaj zmiana
COPY --from=builder /app/dist/ ./dist
EXPOSE 3000
CMD ["node", "dist/app.js"]
Główna zmiana polega na tym, że w kontenerze builder robię npm prune --production
, a potem w tym drugim docelowym kontenerze kopiuję zawartość katalogu node_modules zamiast instalować paczki jeszcze raz (npm ci). Również i w tym przypadku w docelowym kontenerze będą tylko paczki produkcyjne bez "devDependencies", ponieważ te zostały usunięte poprzez npm prune
po zbudowaniu aplikacji. Niestety musiałem usunąć linię USER node
w builder, bo inaczej npm prune
nie chciało się wykonać.
Przy okazji zapytam, dlaczego korzysta się tutaj z USER node
?
Wynik tego zabiegu to zmniejszenie rozmiaru końcowego obrazu ze 181 MB do 168 MB oraz skrócenie czasu budowania z 39s do 34s. Wiem, że to niewiele, ale chodzi tutaj, żeby sobie poeksperymentować ;) W bardzo dużych projektach ta różnica może będzie bardziej widoczna.
Przyjmijmy, że mamy ogromny projekt, który ma bardzo dużo paczek. Pytanie jest, czy to, co zrobiłem ma jakiś sens?