Ein Blog

Posts mit Tag "docker"

Ich hab ja die Serie “HTML-Elemente und -Attribute, die kaum jemand kennt, aber doch ganz praktisch sind”. Da stoße ich meistens drauf, wenn ich mal durch die MDN-Doku scrolle. Oder einen Browser-Changelog lese.

Heute habe ich mal durch die Doku von Dockerfiles gescrollt. Hier ein paar Sachen, die man in der Praxis selten sieht, aber dennoch super sind.

Git-Repo als Build-Context

Docker-Build direkt aus einem Git-Repo:

docker build -t mein-image https://github.com/paperless-ngx/paperless-ngx.git#main

Das ist übrigens ein super Argument dafür, Multi-Stage-Dockerfiles zu machen, die ohne Verarbeitung außerhalb der Dockerfile funktionieren. Ich sehe immer noch Dockerfiles, wo einfach nur eine außerhalb des Buildprozesses kompilierte Binary reinkopiert wird. Wenn man alles in Build-Stages hat, kann man das direkt so bauen. Vielleicht jetzt nicht die super-option für die CI-Pipeline, aber um mal schnell ein Image von einem Git-Repo zu bauen, ist das super.

Dementsprechend kann man das auch in einer compose.yaml 1 als Build-Context nehmen:

services:
  paperless:
    build: https://github.com/paperless-ngx/paperless-ngx.git#main
    # ...

COPY

Wo wir gerade bei Multi-Stage-Builds waren: COPY --from kann nicht nur von anderen Stages kopieren, sondern auch aus lokalen und remote-Images. Z. B. empfiehlt composer so, wie man sich die aktuelle Binary holt:

# Specific release
COPY --from=composer/composer:2-bin /composer /usr/bin/composer

COPY kann aber auch --chmod, --chown, womit man direkt noch die Permissions anpassen kann.

Auch interessant für Multi-Stage-Build ist: COPY --link. Das ist ein etwas komplexerer Tipp, deshalb belasse ich es beim Link auf die Doku.

Mich hat aus den Socken gehauen:

# syntax=docker/dockerfile:1
FROM scratch

COPY --parents ./src/**/*.txt /parents/

# Build context:
# ./src/a.txt
# ./src/x/b.txt
# ./src/x/y/c.txt
#
# Output:
# /parents/src/a.txt
# /parents/src/x/b.txt
# /parents/src/x/y/c.txt

Globbing mit Erhaltung der Verzeichnisstruktur!

Kommen wir zu einem weiteren unterschätztem Command:

ADD

ADD kann --chmod, --chown und --link wie auch COPY.

Was aber viele nicht wissen, die Quelle kann auch ein Git-Repo oder eine HTTP-URL sein:

ADD https://example.com/archive.zip /usr/src/things/
ADD git@github.com:user/repo.git /usr/src/things/

Das Git-Repo wird dann geklont. Die URL runtergeladen.

Sicher habt ihr schonmal curl && sha256sum -c && tar -xzf gemacht oder gesehen. Ich jedenfalls schon sehr oft. ADD kann mit --unpack=true auch direkt das Zip-Archiv entpacken. Und noch besser, mit --checksum kann man auch direkt eine Checksum angeben.

ADD --unpack=true \
    --checksum=sha256:24454f830cdb571e2c4ad15481119c43b3cafd48dd869a9b2945d1036d1dc68d \
    https://example.com/archive.tar.gz /usr/src/things/

Da spart man sich direkt curl und tar zu installieren.

--unpack ist per Default übrigens true, wenn die Quelle ein lokales Archiv ist. Bei remote-Archiven ist es false.

Ist die Quelle ein Git-Repo, dann kann man mit --checksum die Commit-Id angeben.

Mal gucken, ob ich noch weitere Scahen finde. Hier gibt es noch weitere Tipps auf meinem Blog. Der mit dem Copy-From-Image war schonmal dabei.

Footnotes

  1. Das ist der neue Name für die docker-compose.yml, die auch kein version-Feld mehr hat.

Docker kann mehrere Build-Contexte. Damit kann man dann shared-Directorys haben, ohne den Build-Context auf den gemeinsamen Root zu legen.

Docker kann beim Bauen linten. Vielleicht ist euch mal eine Warning aufgefallen, wenn gebaut wird. Wenn man z. B. FROM x as y schreibt, gibt es eine Warning, die besagt, dass das as in einem anderen Casing als FROM ist.

Das kann man jetzt zu einem Fehler machen. Dazu entweder docker build mit --check aufrufen oder den Anfang der Dockerfile ein bisschen konfigurieren;

# syntax=docker/dockerfile:1
# check=error=true

Diese Checks gibt es aktuell.

Zu Docker hab ich noch was neues gelernt. Man kann jetzt (mit DOCKER_BUILDKIT=1) auch bei einzelnen RUN-Commands Dinge mounten. Das ist ganz praktisch für Lockfiles oder Dinge, die man nur ein Mal braucht. Dazu gibt es neben Bind-Mounts auch Cahce-Mounts, womit man ein Cache-Verzeichnis in den Build-Container mounten kann. Nimmt natürlich etwas Reproduzierbarkeit, aber in gewissen Fällen (npm, rust) kann man damit echt viel Zeit sparen:

Aus

COPY package.json package-lock.json ./
RUN npm ci --no-audit

COPY ./Cargo.toml ./Cargo.toml
RUN cargo fetch

COPY ./clitool.c /clitool.c
RUN g++ -O3 -o /bin/clitool /clitool.c

Wird:

RUN --mount=type=bind,source=package.json,target=package.json \
	--mount=type=bind,source=package-lock.json,target=package-lock.json \
	--mount=type=cache,target=/root/.npm \ # mit cache des globalen npm-caches
	npm ci --no-audit


RUN --mount=type=bind,source=Cargo.toml,target=Cargo.toml \
	cargo fetch

RUN --mount=type=bind,source=clitool.c,target=/clitool.c \
	g++ -O3 -o /bin/clitool /clitool.c

Je nach Fall kann man sich damit sogar eine Multi-Stage-Dockerfile ersparen.

TIL docker system df:

# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          5         5         1.318GB   858.6MB (65%)
Containers      5         5         3.416kB   0B (0%)
Local Volumes   5         1         442B      354B (80%)
Build Cache     0         0         0B        0B

Man kennt ja aus Docker-Multistage-Builds das COPY --from=build, um eine Datei aus einer anderen Stage zu kopieren.

Gerade habe ich beim Docker-Image von composer gesehen, dass das nicht nur beschränkt auf Images im selben Multi-stage-Build ist. Man kann dort jedes Image angeben.

Also kann man statt dem hier:

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
        && php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
        && php -r "unlink('composer-setup.php');" \
        && composer --version

Auch das hier machen und die Binary aus dem offiziellen composer-Image kopieren:

COPY --from=composer /usr/bin/composer /usr/bin/composer

Nachteil dabei ist, dass der Builder dafür das komplette Image runterladen muss. Ist in manchen Situationen vielleicht ganz praktisch.

Letztens hatte ich einen Post zu selten verwendeten Features bei docker-compose. Hier ein guter, aber oberflächlicher Artikel: 6 Docker Compose Best Practices for Dev and Prod

Daraufhin hat sich noch ein weiteres Feature aufgetan: profiles. Beispiel:

version: "3.9"
services:
  frontend:
    image: frontend
    profiles: ["frontend"]

  phpmyadmin:
    image: phpmyadmin
    depends_on:
      - db
    profiles:
      - debug

  backend:
    image: backend

  db:
    image: mysql

Here the services frontend and phpmyadmin are assigned to the profiles frontend and debug respectively and as such are only started when their respective profiles are enabled.

In den Kommentaren auf HN gibt es auch noch weitere Tipps.

Heute habe ich gelernt: In docker-compose kann man sich sowas basteln, was ähnlich zu den Init-Containern aus k8s ist. Verwenden tut man dafür die depends_on-Property des Services.

Bisher ging ich davon aus, dass man mit der Property nur festlegen kann, in welcher Reihenfolge die Container gestartet werden. Das bringt einem aber nicht viel, weil die Container üblicherweise unterschiedlich lange brauchen, um zu starten. Das geht mittlerweile besser, nur die Dokumentation erwähnt es (noch) nicht:

Bei den Elementen von depends_on kann man conditions angeben. Dort kann man sagen, in welchem Zustand die Abhängigkeit sein soll, bevor der Container gestartet wird.

services:
  db:
    image: mariadb:10
    ports:
      - "3306:3306"

  init_db:
    image: mariadb:10
    command: /init-db.sh
    volumes:
      - ./init-db.sh:/init-db.sh
    env_file:
      - credentials.env
    depends_on:
      db:
        condition: service_started

  application:
    # ...
    depends_on:
      init_db:
          condition: service_completed_successfully

Durch das service_completed_successfully sagt man, dass application erst startet, wenn init_db mit einem 0-Exit-Code beendet ist. service_started ist offenbar das, was sonst auch immer der Fall war (die Doku nennt es “legacy behaviour”).

Ich hab dann noch ein bisschen Recherche betrieben und rausgefunden: Da geht noch mehr! Definiert man z. B. die Healthchecks, über die ich mal berichtet habe, kann man den Condition service_healthy angeben. Diesen Status erhält ein Container, wenn er hochgefahren und sich durch den Healthcheck als “gesund” erkannt wurde. Definiert man das in der Dockerfile oder in der docker-compose.yml, kann man damit sogar ein ./wait-for-it.sh loswerden. Da hat sich ja echt was getan bei docker-compose.

Heute lernte ich die HEALTHCHECK-Instruktion bei Dockerfiles. Damit kann ein Container einen Befehl definieren, mit dem man rausbekommen kann, ob der Container gerade “Healthy” ist. Erinnert ein bisschen an die Probes aus K8s.

HEALTHCHECK --start-period=10s --timeout=10s \
            CMD curl --fail http://localhost:80 || exit 1

Man kann das auch in einer Compose-File definieren[hinweis].