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

в 13:12, , рубрики: C#, devops, docker, Go, java, node.js, nodejs, php, python, ruby, Программирование

Привет! Я решил выяснить, на каком языке программирования можно написать веб-приложение, чтобы при его контейнеризации 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 пошел кардинально другим путем, но и повезо ему больше всех.

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

Автор: zergon321

Источник


* - обязательные к заполнению поля