
Wenn Sie Docker-Container erstellen möchten, sollten Sie stets versuchen, die Größe der Bilder zu minimieren. Bilder, die dieselben Ebenen verwenden und weniger wiegen - schneller übertragen und bereitgestellt.
Aber wie kann man die Größe steuern, wenn bei jeder Ausführung der RUN
Anweisung eine neue Ebene erstellt wird? Außerdem benötigen Sie noch Zwischenartefakte, bevor Sie das Bild selbst erstellen können ...
Sie wissen wahrscheinlich, dass die meisten Docker-Dateien ihre eigenen, ziemlich seltsamen Funktionen haben, zum Beispiel:
FROM ubuntu RUN apt-get update && apt-get install vim
Warum ist &&
hier? Ist es nicht einfacher, zwei RUN
Anweisungen wie hier auszuführen?
FROM ubuntu RUN apt-get update RUN apt-get install vim
Ab Docker Version 1.10 fügen die Operatoren COPY
, ADD
und RUN
dem Image eine neue Ebene hinzu. Im vorherigen Beispiel wurden zwei Ebenen anstelle einer erstellt.

Ebenen als Git Commits.
Docker-Ebenen behalten die Unterschiede zwischen der vorherigen und der aktuellen Version des Bildes bei. Und wie Git-Commits sind sie praktisch, wenn Sie sie mit anderen Repositorys oder Bildern teilen. Wenn ein Bild von der Registrierung angefordert wird, werden nur die fehlenden Ebenen geladen, was die Trennung von Bildern zwischen Containern vereinfacht.
Gleichzeitig findet jedoch jede Ebene statt, und je mehr davon vorhanden sind, desto schwerer ist das endgültige Bild. Git-Repositorys sind in dieser Hinsicht ähnlich: Die Größe des Repositorys wächst mit der Anzahl der Ebenen, da alle Änderungen zwischen Commits gespeichert werden müssen. Früher war es RUN
, mehrere RUN
Anweisungen in derselben Zeile zu kombinieren, wie im ersten Beispiel. Aber jetzt leider nein.
1. Kombinieren Sie mehrere Ebenen mithilfe der schrittweisen Zusammenstellung von Docker-Bildern zu einer
Wenn das Git-Repository wächst, können Sie einfach den gesamten Änderungsverlauf in einem Commit zusammenfassen und vergessen. Es stellte sich heraus, dass etwas Ähnliches in Docker implementiert werden kann - durch schrittweise Montage.
Erstellen wir einen Node.js.-Container
Beginnen wir mit index.js
:
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => { console.log(`Example app listening on port 3000!`) })
und package.json
:
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } }
Dockerfile
die Anwendung mit der folgenden Dockerfile
:
FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"]
Erstellen Sie ein Bild:
$ docker build -t node-vanilla .
Überprüfen Sie, ob alles funktioniert:
$ docker run -p 3000:3000 -ti --rm --init node-vanilla
Jetzt können Sie dem Link folgen: http: // localhost: 3000 und dort "Hallo Welt!" Sehen.
In der Dockerfile
jetzt die Operatoren COPY
und RUN
, sodass wir die Erhöhung um mindestens zwei Ebenen im Vergleich zum Originalbild korrigieren:
$ docker history node-vanilla IMAGE CREATED BY SIZE 075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0B bc8c3cc813ae /bin/sh -c npm install 2.91MB bac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B 500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B 78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0B b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
Wie Sie sehen können, wurde das endgültige Bild um fünf neue Ebenen vergrößert: eine für jeden Operator in unserer Dockerfile
. Probieren wir jetzt den schrittweisen Docker-Build aus. Wir verwenden dieselbe Dockerfile
, die aus zwei Teilen besteht:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8 COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
Der erste Teil der Dockerfile
erstellt drei Ebenen. Dann werden die Ebenen kombiniert und in die zweite und letzte Stufe kopiert. Dem obigen Bild werden zwei weitere Ebenen hinzugefügt. Als Ergebnis haben wir drei Schichten.

Lass es uns versuchen. Erstellen Sie zunächst einen Container:
$ docker build -t node-multi-stage .
Überprüfen der Historie:
$ docker history node-multi-stage IMAGE CREATED BY SIZE 331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0B bdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0B f8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MB b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
Überprüfen Sie, ob sich die Dateigröße geändert hat:
$ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB
Ja, es ist kleiner geworden, aber noch nicht wesentlich.
2. Wir entfernen alles Unnötige mit Distroless aus dem Container
Das aktuelle Bild bietet uns Node.js, yarn
, npm
, bash
und viele andere nützliche Binärdateien. Außerdem basiert es auf Ubuntu. Durch die Bereitstellung erhalten wir ein vollwertiges Betriebssystem mit vielen nützlichen Binärdateien und Dienstprogrammen.
Sie benötigen sie jedoch nicht, um den Container auszuführen. Die einzige benötigte Abhängigkeit ist Node.js.
Docker-Container müssen den Betrieb eines Prozesses unterstützen und die minimal erforderlichen Tools enthalten, um ihn auszuführen. Ein gesamtes Betriebssystem ist hierfür nicht erforderlich.
Wir können also alles außer Node.js herausholen.
Aber wie?
Google hat bereits eine ähnliche Lösung gefunden - GoogleCloudPlatform / Distroless .
Die Beschreibung für das Repository lautet:
Distroless-Images enthalten nur die Anwendung und ihre Abhängigkeiten. Es gibt keine Paketmanager, Shells oder andere Programme, die normalerweise in der Standard-Linux-Distribution enthalten sind.
Das brauchen Sie!
Führen Sie Dockerfile
, um ein neues Image zu erhalten:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
Wir sammeln das Bild wie gewohnt:
$ docker build -t node-distroless .
Die Anwendung sollte gut funktionieren. Führen Sie zur Überprüfung den Container aus:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
Und gehen Sie zu http: // localhost: 3000 . Ist das Bild ohne zusätzliche Binärdateien einfacher geworden?
$ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB
Einfach so! Jetzt wiegt es nur noch 76,7 MB, bis zu 600 MB weniger!
Alles ist cool, aber es gibt einen wichtigen Punkt. Wenn der Container ausgeführt wird und Sie ihn überprüfen müssen, können Sie eine Verbindung herstellen mit:
$ docker exec -ti <insert_docker_id> bash
Das Herstellen einer Verbindung zu einem laufenden Container und das Starten von bash
dem Erstellen einer SSH-Sitzung sehr ähnlich.
Da es sich bei Distroless jedoch um eine abgespeckte Version des ursprünglichen Betriebssystems handelt, gibt es weder zusätzliche Binärdateien noch eine Shell!
Wie verbinde ich mich mit einem laufenden Container, wenn keine Shell vorhanden ist?
Das Interessanteste ist, dass nichts.
Dies ist nicht sehr gut, da nur Binärdateien in einem Container ausgeführt werden können. Und die einzige, die gestartet werden kann, ist Node.js:
$ docker exec -ti <insert_docker_id> node
In der Tat gibt es ein Plus, denn wenn ein Angreifer Zugriff auf den Container erhält, fügt er viel weniger Schaden zu, als wenn er Zugriff auf die Shell hätte. Mit anderen Worten, weniger Binärdateien - weniger Gewicht und höhere Sicherheit. Aber auf Kosten eines komplexeren Debuggens.
Hierbei ist zu beachten, dass es sich nicht lohnt, Container in der Produktumgebung zu verbinden und zu debuggen. Es ist besser, sich auf ordnungsgemäß konfigurierte Protokollierungs- und Überwachungssysteme zu verlassen.
Aber was ist, wenn wir noch debuggen müssen und dennoch möchten, dass das Docker-Image das kleinste ist?
3. Reduzieren Sie Basisbilder mit Alpine
Sie können Distroless durch ein Alpenbild ersetzen.
Alpine Linux ist eine sicherheitsorientierte, leichtgewichtige Distribution, die auf musl libc und Busybox basiert . Aber wir werden kein Wort nehmen, sondern es überprüfen.
Führen Sie Dockerfile
mit dem folgenden node:8-alpine
:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8-alpine COPY --from=build /app / EXPOSE 3000 CMD ["npm", "start"]
Erstellen Sie ein Bild:
$ docker build -t node-alpine .
Größe prüfen:
$ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB
Am Ausgang haben wir 69,7 MB - das ist sogar weniger als ein verzweifeltes Bild.
Lassen Sie uns prüfen, ob es möglich ist, eine Verbindung zu einem Arbeitscontainer herzustellen (im Fall des Distrolles-Images konnten wir dies nicht tun).
Starten Sie den Container:
$ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000!
Und verbinden:
$ docker exec -ti 9d8e97e307d7 bash OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown
Erfolglos. Aber vielleicht hat der Container sh
'ell ...:
$ docker exec -ti 9d8e97e307d7 sh / #
Großartig! Wir haben es geschafft, eine Verbindung zum Container herzustellen, und gleichzeitig ist sein Image auch kleiner. Aber hier gab es einige Nuancen.
Alpine Images basieren auf muslc, einer alternativen Standardbibliothek für C. Während die meisten Linux-Distributionen wie Ubuntu, Debian und CentOS auf glibc basieren. Es wird angenommen, dass beide Bibliotheken dieselbe Schnittstelle für die Arbeit mit dem Kernel bieten.
Sie haben jedoch unterschiedliche Ziele: glibc ist am häufigsten und schnellsten, während musc weniger Platz beansprucht und mit einer Sicherheitsabweichung geschrieben ist. Wenn eine Anwendung kompiliert wird, wird sie in der Regel in eine bestimmte C-Bibliothek kompiliert. Wenn Sie sie mit einer anderen Bibliothek verwenden müssen, müssen Sie sie neu kompilieren.
Mit anderen Worten, das Erstellen von Containern auf Alpenbildern kann zu unerwarteten Ereignissen führen, da die darin verwendete Standard-C-Bibliothek unterschiedlich ist. Der Unterschied macht sich bemerkbar, wenn Sie mit vorkompilierten Binärdateien wie Node.js-Erweiterungen für C ++ arbeiten.
Zum Beispiel funktioniert das fertige PhantomJS-Paket nicht auf Alpine.
Also, was ist das Grundbild zu wählen?
Alpen-, Distroless- oder Vanille-Look - natürlich ist es besser, sich je nach Situation zu entscheiden.
Wenn Sie sich mit Produkten beschäftigen und Sicherheit wichtig ist, ist Distroless vielleicht am besten geeignet.
Jede dem Docker-Image hinzugefügte Binärdatei erhöht die Stabilität der gesamten Anwendung. Dieses Risiko kann verringert werden, indem nur eine Binärdatei im Container installiert ist.
Wenn ein Angreifer beispielsweise eine Sicherheitsanfälligkeit in einer Anwendung finden könnte, die auf der Grundlage eines verzweifelten Images ausgeführt wird, kann er die Shell im Container nicht ausführen, da sie nicht vorhanden ist.
Wenn aus irgendeinem Grund die Größe des Docker-Bildes für Sie extrem wichtig ist, lohnt es sich auf jeden Fall, sich die alpinen Bilder genauer anzusehen.
Sie sind wirklich klein, aber auf Kosten der Kompatibilität. Alpine verwendet eine etwas andere Standard-C-Bibliothek, muslc, sodass manchmal Probleme auftreten. Die Beispiele finden Sie unter den folgenden Links: https://github.com/grpc/grpc/issues/8528 und https://github.com/grpc/grpc/issues/6126 .
Vanillebilder sind ideal zum Testen und Entwickeln.
Ja, sie sind groß, aber sie sehen auf einem vollwertigen Computer mit installiertem Ubuntu so gut wie möglich aus. Darüber hinaus sind alle Binärdateien im Betriebssystem verfügbar.
Fassen Sie die Größe der empfangenen Docker-Bilder zusammen:
node:8
681 MB
node:8
mit inkrementellem Build von 678 MB
gcr.io/distroless/nodejs
76,7 MB
node:8-alpine
69,7 MB
Abschiedswörter vom Übersetzer
Lesen Sie weitere Artikel in unserem Blog:
Stateful Backups in Kubernetes
Sichern einer großen Anzahl heterogener Webprojekte
Telegrammbot für Redmine. Wie Sie das Leben für sich und die Menschen vereinfachen können