Выбираем лучший backend-язык для контейнеризации в Docker

от автора

Привет, Хабр! Я решил выяснить, на каком языке программирования можно написать веб-приложение, чтобы при его контейнеризации Docker-образ получился легковесным, а сборка образа была быстрой.

image


Правила таковы:

  • Для веб-приложения выбирается наиболее популярный (или один из наиболее ходовых) веб-фреймворк.
  • Приложение, которое создается, должно выполнять следующее действие: присылать сообщение «Hello, world!» при обращении по единственному маршруту — «/».
  • Там, где это имеет смысл, используется многоэтапная сборка для оптимизации образа.
  • В качестве базового образа всегда используется Alpine, либо образы на его основе.
  • Время сборки измеряется Linux-командой time. Используется вот так:
    time docker build -t my-image .

    После исполнения команды выводит время, затраченное на ее выполнение.

  • Для запуска контейнера всегда используется команда
    docker run --rm -it -p port1:port2 my-image

Ну что же, приступим!

Node.JS

В качестве базового образа используем node:alpine, в качестве сервера — Express. Многоэтапная сборка в данном случае сокращает образ всего на пару мегабайтов.

Код приложения:

const express = require('express'); const app = express(); const port = 8000;  app.get('/', (req, res) => {   res.send('Hello, world!'); });  app.listen(port, () => {   console.log('The server listens on ' + port); }); 

Dockerfile:

FROM node:alpine AS builder COPY index.js /app/index.js WORKDIR /app RUN npm install express --save ENTRYPOINT [ "node", "/app/index.js" ]  FROM node:alpine COPY --from=builder /app /app ENTRYPOINT [ "node", "/app/index.js" ] 

Время сборки — 14.791 секунд.
Размер образа — 81 MB.

C#

В случае с C# использование многоэтапной сборки и Alpine в качестве production-образа сокращают размер финального образа примерно в 7 раз. В качестве фреймворка испоьзуется ASP.NET Core.

Код контроллера:

[Route("/")] [ApiController] public class ValuesController : ControllerBase {     [HttpGet]     public ActionResult<string> Get()     {         return "Hello, world!";     } } 

Dockerfile:

FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS builder COPY . /app WORKDIR /app RUN dotnet publish -c Release ENTRYPOINT [ "dotnet", "/app/bin/Release/netcoreapp3.0/publish/cs-app.dll" ]  FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-alpine3.9 COPY --from=builder /app/bin/Release/netcoreapp3.0/publish/ /app/ ENTRYPOINT [ "dotnet", "/app/cs-app.dll" ] 

Время сборки — 14.818 секунд.
Размер образа — 94.4 MB.

Java

Использование многоэтапной сборки и Alpine сокращают размер образа примерно в 6 раз. В качестве веб-фреймворка используется Spring Boot с пакетным менеджером Gradle.

Код контроллера:

@RestController public class HelloController {      @RequestMapping("/")     public String hello() {         return "Hello, world!";     } } 

Dockerfile:

FROM gradle:jdk8 AS builder COPY . /app WORKDIR /app RUN ./gradlew build ENTRYPOINT [ "java", "-jar", "build/libs/app-0.0.1-SNAPSHOT.jar" ]  FROM openjdk:8-jdk-alpine COPY --from=builder /app/build/libs/app-0.0.1-SNAPSHOT.jar /app/application.jar ENTRYPOINT [ "java", "-jar", "/app/application.jar" ] 

Время сборки — 1 минута 52.479 секунд.
Размер образа — 122 MB.

Время сборки является очень высоким из-за запуска демона Gradle и выполнения всех его тасков.

PHP

В качестве фреймворка был выбран Laravel. Конкретно в этом случае не было никаких дополнительных библиотек, только код, сгенерированный самим фреймворком, так что использование многоэтапной сборки не имело смысла. Нам достаточно изменить код файла routes/web.php:

Route::get('/', function () {     return "Hello, world!"; }); 

Короткий Dockerfile:

FROM php:7.2.19-alpine3.9 COPY . /usr/src/app WORKDIR /usr/src/app ENTRYPOINT [ "php", "artisan", "serve", "--host", "0.0.0.0" ] 

Время сборки — 15.046 секунд.
Размер образа — 116 MB.

Python

Многоэтапная сборка экономит всего пару мегабайтов. В качестве веб-фреймворка был выбран Flask. Код весьма прост:

from flask import Flask  app = Flask(__name__)  @app.route("/") def hello():     return "Hello World!"      if __name__ == "__main__":     app.run(host="0.0.0.0", port=int("5000"), debug=True) 

Dockerfile:

FROM python:alpine3.7 AS builder COPY . /app WORKDIR /app RUN pip install --user -r requirements.txt  FROM python:alpine3.7 COPY --from=builder /root/.local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages COPY --from=builder /app/index.py /app/index.py ENTRYPOINT [ "python", "./app/index.py" ] 

В файле requirements.txt прописана всего одна зависимость — flask.

Время сборки — 15.332 секунд.
Размер образа — 85.1 MB.

Go

Go имеет преимущество перед другими языками в плане построения веб-приложений. Ему не нужен какой-нибудь тяжеловесный фреймворк, все необходимое уже находится в стандартной библиотеке. При этом он компилируется напрямую в код той архитектуры, на которой будет запущена программа, так что нет необходимости в виртуальной машине, исполняющей байт-код. Мы можем собрать исполнимый файл и запустить его на чистом Alpine.

Код сервера:

package main  import ( 	"log" 	"net/http" )  func hello(w http.ResponseWriter, r *http.Request) { 	w.Write([]byte("Hello, world!")) }  func main() { 	http.HandleFunc("/", hello)  	err := http.ListenAndServe(":80", nil)  	if err != nil { 		log.Fatalln("Couldn't start the server:", err) 	} } 

Dockerfile:

FROM golang:1.12 AS builder COPY . /go/src/app RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/bin/app /go/src/app/ ENTRYPOINT [ "go/bin/app" ]  FROM alpine:latest COPY --from=builder /go/bin/app /bin/app ENTRYPOINT [ "/bin/app" ] 

Строка «CGO_ENABLED=0 GOOS=linux GOARCH=amd64» необходима, т.к. Alpine не имеет стандартного libc.

Время сборки — 12.568 секунд.
Размер образа — 12.9 MB.

Это просто фантастический результат. При использовании Alpine и многоэтапной сборки размер образа уменьшается в 60 раз. Бесспорно, Go — лучший язык для приложений, подлежащих упаковке в контейнер.
Что действительно может увеличить время сборки, так это скачивание библиотек при помощи go get. Пожалуй, лучше использовать для этого dep.

Ruby

Контейнеризация Rails-приложения — сущий кошмар. Пришлось столкнуться со следующими проблемами:

  1. Несовместимость версий. По умолчанию Ubuntu ставит Ruby 2.5.1 и Bundler 2.0.2. Но в контейнере для Ruby 2.5.1 был Bundler 1-ой версии. Если прописать в Dockerfile инструкцию по установке нового Bundler, то то среда Ruby все равно продолжит использовать старый. Решение нашлось на сайте, на который я смог попасть только через Tor.
  2. Сборка некоторых гемов требует исходники на Си. Хуже того, при сборке некоторых из них (конкретно — nokogiri) необходимо прописывать конфиги, валяющиеся где-то в этих исходниках. Решение этой проблемы мне повезло найти в одном японском блоге. Мало того, эти исходники необходимы даже на production’e.

Код контроллера:

class HomeController < ApplicationController     def index       render plain: 'Hello, world!'     end  end 

Маршрут:

Rails.application.routes.draw do   root to: 'home#index' end 

Кроме того, в Gemfile надо прописать следующее:

gem 'tzinfo-data' gem 'execjs' 

Поучившийся Dockerfile:

FROM ruby:2.5.1-alpine3.7 AS base ENV BUNDLER_VERSION 2.0.2 RUN apk add --no-cache --update \     build-base \     libxml2-dev \     libxslt-dev \     nodejs \     nodejs-npm \     sqlite-dev \     && gem install bundler  FROM base AS builder COPY . /usr/src/app WORKDIR /usr/src/app RUN gem install nokogiri \     -- --use-system-libraries \     --with-xml2-config=/usr/bin/xml2-config \     --with-xslt-config=/usr/bin/xslt-config \     && bundle install CMD rails server -b 0.0.0.0  FROM base COPY --from=builder /usr/src/app /usr/src/app COPY --from=builder /usr/local/bundle/ /usr/local/bundle/ WORKDIR /usr/src/app CMD rails server -b 0.0.0.0 

Есть, конечно, официальный образ Rails, но его поддержка была прекращена в 2016-ом.

Время сборки — 2 минуты 20.374 секунд.
Размер образа — 322 MB.

Это очень много. Объективно наихудший результат среди всех представленных здесь языков.

Вообще, большинство языков из присутствующих здесь появились именно в те времена, когда о контейнеризации никто и не подозревал, а кроссплатформенность достигалась с помошью виртуальных машин, исполняющих байт-код. Go пошел кардинально другим путем, но и повезо ему больше всех.

Если у вас есть какие-либо советы или замечания по оптимизации образов, пожалуйста, пишите в комментариях, все учтется.


ссылка на оригинал статьи https://habr.com/ru/post/462369/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *