Выбираем лучший 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/


Интересные статьи

Интересные статьи

Много всякого сыпется в мой ящик, в том числе и от Битрикса (справедливости ради стоит отметить, что я когда-то регистрировался на их сайте). Но вот мне надоели эти письма и я решил отписатьс...
Привет, Хабр. В этой статье я хочу рассказать о своем опыте создания учебной среды для экспериментов с микросервисами. При изучении каждого нового инструмента мне всегда хотелось его попробова...
В прошлой статье я высказал мысль о том, что сейчас время такое, что можно довольно малыми вложениями создать успешный стартап. В обсуждении было очень много возражений на тему от "это нево...
Те, кто собираются открывать интернет-магазин, предварительно начитавшись в интернете о важности уникального контента, о фильтрах, накладываемых поисковиками за копирование материалов с других ресурсо...