Docker + Yocto: Jak przestałem się martwić i pokochałem kontenery

Jeśli kiedykolwiek budowałeś obraz Yocto, to wiesz — to nie jest “kliknij i czekaj”. To jest rytuał. Wymaga konkretnej wersji Pythona, odpowiednich pakietów systemowych, właściwego locale, basha (nie zsh, nie fish — bash). I oczywiście wystarczy, że kolega z zespołu ma Ubuntu 24.04 zamiast 22.04, żeby ten sam bitbake produkował zupełnie inne błędy.

Brzmi znajomo?

W tym artykule pokażę, jak Docker rozwiązuje te problemy na przykładzie repozytorium stm32mp1-basic-yocto-setup — środowiska, które używam m.in. jako bazę na moich kursach STM32MP1 Masterclass i Secure Boot & Chain of Trust. Ale to nie jest artykuł o kursach — to jest artykuł o tym, jak żyć wygodniej z Yocto.

Problem: “U mnie działa”

Yocto Project to potężne narzędzie. Potrafi zbudować od zera kompletny system Linux dostosowany do Twojego hardware’u — kernel, rootfs, bootloader, aplikacje. Ale ma jedną dość irytującą cechę: jest niezwykle wrażliwy na środowisko budowania.

Oficjalna dokumentacja Yocto Scarthgap jasno mówi: wspieramy konkretne dystrybucje hosta. Ubuntu 22.04 — tak. Ubuntu 24.04 — jeszcze nie. Fedora 40 — może. Twoja egzotyczna konfiguracja Archa z patchami z AUR — powodzenia.

W praktyce wygląda to tak:

  • Inżynier A ma Ubuntu 22.04 — buduje bez problemu.
  • Inżynier B zainstalował sobie nowszego Pythona — bitbake odmawia współpracy.
  • Inżynier C jest na Fedorze — połowa pakietów się nazywa inaczej.
  • Nowy członek zespołu spędza pierwszy dzień na konfiguracji środowiska zamiast na kodowaniu.

To jest marnowanie czasu i energii. I to jest dokładnie ten rodzaj problemu, do którego Docker został stworzony.

Rozwiązanie: hermetyczne środowisko budowania

Idea jest prosta: zamiast instalować na hoście kilkanaście pakietów i modlić się, żeby wersje się zgadzały, zamykamy całe środowisko w kontenerze. Raz definiujemy, wszędzie działa.

Zobaczmy, jak to wygląda w praktyce. Cały Dockerfile jest krótki i czytelny:

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

# Pakiety wymagane przez Yocto Scarthgap
RUN apt-get update && apt-get install -y \
    build-essential chrpath cpio diffstat file gawk gcc git \
    python3 python3-git python3-jinja2 python3-pexpect \
    python3-pip python3-subunit socat texinfo unzip wget \
    xz-utils zstd curl vim sudo lz4 gosu locales \
    && rm -rf /var/lib/apt/lists/*

# Locale — Yocto tego wymaga
RUN locale-gen en_US.UTF-8
ENV LANG=en_US.UTF-8

# Yocto wymaga bash, nie dash
RUN rm /bin/sh && ln -s /bin/bash /bin/sh

# Narzędzie repo do pobierania manifestów ST
RUN curl -o /usr/local/bin/repo \
    https://storage.googleapis.com/git-repo-downloads/repo \
    && chmod a+x /usr/local/bin/repo

Tyle. To jest zamrożone w czasie środowisko — Ubuntu 22.04 z dokładnie tymi pakietami, których Yocto oczekuje. Nie obchodzi go, jaki system masz na hoście. Możesz budować na Ubuntu, Fedorze, macOS, a nawet na CI/CD serverze w chmurze. Kontener jest identyczny.

Trzy sprytne triki, które robią różnicę

Okej, Dockerfile to fundament. Ale żeby Docker i Yocto naprawdę dobrze ze sobą współgrały, trzeba rozwiązać kilka kwestii, o których się nie myśli na początku.

1. Dynamiczne mapowanie UID/GID

Yocto generuje mnóstwo plików — kernel, rootfs, paczki, logi. Jeśli te pliki tworzą się z uprawnieniami użytkownika root wewnątrz kontenera, to na hoście masz problem — nie możesz ich usunąć bez sudo, nie możesz ich edytować w IDE.

Rozwiązanie? entrypoint.sh dynamicznie dopasowuje UID/GID użytkownika builder w kontenerze do użytkownika na hoście:

#!/bin/bash
TARGET_UID=${USER_ID:-1000}
TARGET_GID=${GROUP_ID:-1000}

# Zmiana UID/GID w runtime
groupmod -g "$TARGET_GID" -o builder 2>/dev/null || true
usermod  -u "$TARGET_UID" -o builder 2>/dev/null || true

# Uruchomienie polecenia jako builder
exec gosu builder "$@"

Efekt? Pliki tworzone w kontenerze mają na hoście Twojego właściciela. ls -la wygląda normalnie. IDE nie protestuje. Życie jest piękne.

A co najważniejsze — ten sam obraz Dockera działa dla każdego użytkownika bez przebudowy. UID/GID jest ustalany w momencie startu kontenera, nie w momencie budowania obrazu.

2. Inteligentna strategia wolumenów

Nie wszystkie dane w Yocto są sobie równe. docker-compose.yml wyraźnie to rozróżnia:

volumes:
  # Bind mount — pliki edytowane na hoście
  - ./yocto:/home/builder/yocto
  - ./custom-layers:/home/builder/custom-layers
  - ./config:/home/builder/config:ro

  # Named volumes — persystentny cache
  - yocto-downloads:/home/builder/shared-downloads
  - yocto-sstate-cache:/home/builder/shared-sstate-cache

To jest kluczowy podział:

  • Bind mounty to pliki, które chcesz widzieć i edytować na hoście — źródła, wyniki budowania, własne warstwy, konfiguracja. Otwierasz je w VS Code, modyfikujesz Device Tree, zapisujesz — zmiany natychmiast są widoczne w kontenerze.

  • Named volumes to cache, który Docker zarządza sam. DL_DIR (pobrane archiwa źródłowe) i SSTATE_DIR (shared state cache) przeżywają restart kontenera, przeżywają nawet docker compose down. Dzięki temu kolejny build nie pobiera ponownie setek megabajtów z internetu i nie rekompiluje tego, co już było skompilowane.

Kto kiedyś przypadkiem usunął sstate-cache i musiał czekać kilka godzin na ponowne budowanie — ten doceni.

3. Profile konfiguracji zamiast ręcznej edycji local.conf

To akurat jest feature specyficzny dla tego repozytorium, nie ogólna praktyka Dockerowa — ale zbyt dobry, żeby go pominąć.

Zamiast ręcznie edytować conf/local.conf (i zapominać co się zmieniło), profile konfiguracji to wersjonowane w Git fragmenty, które build.sh składa automatycznie:

# Bazowy build
./build.sh

# Secure Boot — podpisany TF-A, Chain of Trust
CONFIG=base,secure-boot DISTRO=openstlinux-weston ./build.sh st-image-weston

# Masterclass — Qt, OpenAMP, debugowanie
CONFIG=base,masterclass DISTRO=openstlinux-weston ./build.sh st-image-weston

Pod spodem build.sh generuje plik conf/auto.conf (BitBake wczytuje go automatycznie obok local.conf), łącząc zawartość wybranych profili. Zmienił profil? Wystarczy ponownie uruchomić build — auto.conf się zregeneruje, bez ręcznego czyszczenia.

Chcesz własny profil? Tworzysz plik config/my-profile.conf.inc:

# config/my-profile.conf.inc
IMAGE_INSTALL:append = " my-custom-app"
EXTRA_IMAGE_FEATURES += "debug-tweaks"

I używasz: CONFIG=base,my-profile ./build.sh. To wszystko.

Workflow z lotu ptaka

Cały proces od zera do zbudowanego obrazu Linux wygląda tak:

# 1. Klonowanie repozytorium
git clone <repo-url>
cd stm32mp1-basic-yocto-setup

# 2. Inicjalizacja — pobiera warstwy OpenSTLinux (jednorazowo)
./setup_repo.sh

# 3. Budowanie obrazu
./build.sh

# 4. Gotowe — obrazy na hoście
ls ./yocto/build-*/tmp-glibc/deploy/images/stm32mp1/

Trzy komendy. Zero konfiguracji środowiska. Zero instalowania pakietów na hoście. Zero “u mnie nie działa”.

A jeśli potrzebujesz wejść do kontenera i pogrzebać ręcznie:

./interactive.sh

Dostajesz pełny shell z zamontowanymi wolumenami, cache’em i środowiskiem gotowym do pracy.

Dlaczego to ma znaczenie w praktyce

Powtarzalność

Każdy w zespole buduje z tym samym środowiskiem. Identyczne pakiety, identyczne wersje, identyczne wyniki. Koniec z debugowaniem różnic między maszynami deweloperów.

Onboarding w minutach, nie w godzinach

Nowy członek zespołu? git clone, ./setup_repo.sh, ./build.sh. Tyle. Nie trzeba mu wysyłać 3-stronicowej instrukcji konfiguracji środowiska, której i tak nikt nie aktualizuje. Nie trzeba też pomagać mu przez Teams przez godzinę, bo ma inną wersję Pythona.

CI/CD

Skoro build odbywa się w kontenerze, to przeniesienie go na serwer CI/CD (Jenkins, GitLab CI, GitHub Actions) jest trywialne. Ten sam Dockerfile, te same skrypty, te same wyniki. Automatyczne nightly buildy? Proszę bardzo.

Izolacja

Yocto wymaga specyficznych pakietów i konfiguracji (choćby ten bash zamiast dash). W kontenerze możesz to robić bez zaśmiecania hosta. Twój system pozostaje czysty, a Yocto ma swoje własne, izolowane królestwo.

Wieloprojektowość

Budujesz jeden projekt na Yocto Scarthgap i drugi na Kirkstone? Dwa różne kontenery, dwa różne środowiska, zero konfliktów. Na hoście byłoby to problematyczne — w Dockerze to naturalny sposób pracy.

Własne warstwy — edycja na hoście, build w kontenerze

Jednym z najciekawszych aspektów tego setupu jest to, że warstwy Yocto edytujesz normalnie na hoście — w swoim ulubionym IDE — a kontener je automatycznie widzi i rejestruje.

Katalog custom-layers/ jest bind-mountem. Wszystko, co tam umieścisz i co ma poprawny conf/layer.conf, zostanie automatycznie wykryte przez build.sh i dodane do bblayers.conf. Nie trzeba nic konfigurować ręcznie.

Przykładowo, żeby zmodyfikować Device Tree (typowy scenariusz przy pracy z koprocesorami STM32MP1):

  1. Edytujesz plik .dts w custom-layers/meta-training/recipes-kernel/linux/files/ — w VS Code, na hoście.
  2. Uruchamiasz ./build.sh — kontener widzi zmianę i buduje.
  3. Wynikowy obraz jest na hoście w ./yocto/build-*/tmp-glibc/deploy/images/stm32mp1/.

Bez kopiowania plików, bez docker cp, bez montowania czego nie trzeba. Po prostu działa.

Kilka słów o wydajności

Wiem, co myślisz: “Docker to narzut, build będzie wolniejszy.”

W przypadku Yocto — praktycznie nie. Dlaczego?

  • Yocto jest I/O-bound, nie CPU-bound. Kontener Dockera na Linuksie używa natywnego kernela — nie ma warstwy wirtualizacji. Dostęp do dysku i CPU jest identyczny jak na hoście.
  • Named volumes na Linuksie mają natywną wydajność (są zwykłymi katalogami w /var/lib/docker/volumes/).
  • Bind mounty również — na Linuksie to bezpośrednie mapowanie, bez narzutu.

Jedyny zauważalny narzut to start kontenera (kilkaset milisekund) i mapowanie UID/GID (sekundy). Przy buildzie trwającym godziny — to szum statystyczny.

Uwaga dla użytkowników macOS: Na macOS Docker działa w lekkiej maszynie wirtualnej, więc wydajność bind mountów jest znacznie niższa. Do buildów Yocto na macOS warto rozważyć użycie named volumes również na workspace, albo budować na zdalnym serwerze Linux.

Podsumowanie: Czy warto?

Tak. Jednoznacznie tak.

Docker nie dodaje tu niepotrzebnej złożoności — wręcz przeciwnie, usuwa złożoność, która i tak istnieje (konfiguracja środowiska, zależności, wersje pakietów). Zamienia ją w deklaratywny, powtarzalny, wersjonowany w Git setup.

Patrząc na to repozytorium:

Bez DockeraZ Dockerem
Instalacja kilkunastu pakietówdocker compose build (jednorazowo)
Konfiguracja locale, bash, repoJuż w Dockerfile
Problem z prawami dostępuAutomatyczne mapowanie UID/GID
Ręczna edycja local.confProfile konfiguracji (CONFIG=base,secure-boot)
Cache ginie przy reinstalacjiNamed volumes przeżywają wszystko
“U mnie działa”U wszystkich działa

Jeśli pracujesz z Yocto profesjonalnie — czy to w ramach kursu, czy w produkcji — konteneryzacja środowiska to nie luksus. To higiena.


Ten setup jest używany jako baza na kursach STM32MP1 Masterclass oraz Secure Boot & Chain of Trust prowadzonych przez wolosewicz.com. Profile masterclass i secure-boot odpowiadają scenariuszom kursowym — ale samo repozytorium jest uniwersalne i można je dostosować do dowolnego projektu opartego na STM32MP1.

Repozytorium: stm32mp1-basic-yocto-setup na GitLab