Привет %username%, эта статья про то, как поместить Rust приложение в Docker и получить образ размером с бинарный файл (6 Мб). А также про причины, которые привели к переходу с NodeJS на Rust, отдельная пара слов о проблемах вначале, переходе на Go, и том, как команда Rust устранила эти проблемы. TL;DR Dockerfile в конце статьи и ссылка на example репозиторий История начинается со времён, когда Digital Ocean стоил 5$ за дроплет с 512 Мб оперативки, всё было прекрасно, до тех пор пока на нём крутился только Nginx раздающий статику, которому этого более чем достаточно. Затем понадобилось добавить NodeJS, базу данных, файловое хранилище для загрузки картинок - всё это хотелось изолировать друг от друга, поэтому нужен Docker, а ещё это всё было хобби и строить кластера за 100$ на AWS совсем не хотелось, но уже в 512 Мб уложиться не получалось - большое потребление памяти было сначала странным, так как пользователем сервиса был только один я.Как так могло выйти, что форма регистрации и загрузка картинок требовала свыше 512 Мб серверной памяти? docker stats на тот момент показал, что PostgreSQL, MinIO, Nginx потребляли до 30 Мб оперативной памяти каждый (с лимитами 128 Мб), а вот NodeJS падал с OutOfMemoryError. Экспериментальным путём было выявлено, что для NodeJS контейнера требуется минимум 300 Мб. Cначала показалось, что это связано с тем, что используются зависимости для GraphQL, но фактический пустое приложение на Express (фреймворк для веб-приложений) уже потребляло ~100 Мб. Последующий снепшот памяти раскрыл, что треть всей потребляемой памяти занимают строки - причём строки исходного кода. Оказалось это фундаментальная часть языка JavaScript и того как работает функция .toString() Если исходный код зависимостей (node_modules) весит 300 Мб, то потенциально приложение может потреблять это количество памяти, а то и больше. Поэтому решено было поискать новый способ разрабатывать веб приложения. И тогда мне показался очень перспективным язык Rust. Первое знакомство с Rust у меня не задалось. Нужно было написать небольшой микросервис Email подписки, всё просто - по урлу /subsсribe необходимо получать строку и затем сохранять её в PostgreSQL. Задачка на час максимум. В общем код был написан, осталось его только уложить в контейнер. Первая версия Dockerfil'а: В целом не сильно беспокоило, что образ контейнера весит 1.7 Гб, ведь дискового пространства было много, а кеши всегда прогреты. Зато со скоростью сборки было явно что - то не так - холодная сборка контейнера могла занимать до 60 минут и с некоторой вероятностью падала по таймауту. А также после каждого обновления/добавления пакетов, сборка снова длилась час. Выяснялось, что проблема заключалась в том, что cargo использовал для скачивания зависимостей crates индекс в виде tar архива - то есть фактический скачивал ~170 Мб кода с гитхаба, к сожалению github не позволял скачивать большие репозитории быстро и ограничивал скорость до 64 Кб/сек. Этот индекс нужен для быстрого поиска пакетов и их актуальных версий. Чтобы хоть как - то закешировать этот index приходилось идти на костыли - создавать фейк проект и через добавления зависимости загружать этот индекс: Конечно этот костыль плохо работал, так как со временем всё ровно нужно было подтянуть свежую версию индекса. Поэтому в тот момент был потрачен час другой на переписывание микросервиса на Go, с последующим анализом Issue в репозитории Rust команды. Кстати, у ребят уже тогда была идея, как оптимизировать скачивание по HTTP, и только той части индекса, которая действительно необходима. По итогу идея со Sparse Index взлетела - на сегодня этот механизм уже находится в stable релизе и успешно работает. Поэтому можно попробовать ещё раз возвращение к Rust контейнерам. И первое что приходит в голову - разделить на два этапа: большой контейнер для сборки (builder) и конечный лёгкий контейнер в котором запускается приложение. Звучит неплохо: Тем самым можно получить контейнер размером 841 Мб и приложение, которое не запускается. Если начать разбираться, то выяснится - образы Rust тянут за собой слой инструментов, которые нужны для сборки и компиляции приложения, однако они не нужны в конечном запускаемом контейнере. Поэтому можно использовать голый alpine: "FROM: alpine:3.17", тогда размер приложения уменьшится до 13.4 Мб, но проблема останется прежней - приложение не запускается со словами Если войти в контейнер, то бинарный файл есть, и если его проверить с помощью утилиты ldd, то можно увидеть, что не хватает части зависимостей Это сбивало с толку и я долго не мог понять, каких зависимостей не хватает. На самом деле приложение было неправильно скомпилировано, и в таком случае нужно компилятору явно указать под какой target (какую платформу) собирать - в данном случае x86_64-unknown-linux-musl Добавляем правильный target: Что-ж, теперь приложение быстро компилируется и запускается, а контейнер весит 13.6 Мб Если хотите, чтобы приложение ещё меньше весило, то "FROM: alpine:3.17" можно заменить на "FROM: scratch" - то есть сделать запуск на "голом" линуксе, такой образ будет весить 6.15 Мб - ровно столько, сколько весит размер исполняемого файла, в таком контейнере не будет никаких curl, sh и прочих приложений - сам образ весит 0 байт. Итоговый Dockerfile Такой контейнер потребляет 1.3 Мб оперативной памяти, считаю отличным результатом для простого hello-world веб-приложения с фреймворками axum и tokio. Схожие результаты были с Go контейнерами микросервисов. Также была опробована статическая линковка, но видимых результатов это не дало, возможно из - за того, что проект небольших размеров или что - то неправильно было приготовлено. Спасибо что прочитали, надеюсь этот опыт будет полезен. P.S. Ещё было кеширование зависимости между 7 и 8 строчками кода, но там начали возникать странные эффекты  Developer