In den Kommentaren zu meinem Artikel gab es viele Anfragen, zu erklären, was so schrecklich an dem darin beschriebenen Dockerfile ist.
Zusammenfassung der vorherigen Episode: zwei Entwickler haben eine straffe Deadline und erstellen ein Dockerfile. Währenddessen kommt Ops Igor Ivanovich dazu. Das Ergebnis-Dockerfile ist so schlecht, dass der IO an einem Herzinfarkt kratzt.

Jetzt klären wir, was mit diesem Dockerfile nicht stimmt.
Also, eine Woche ist vergangen.
Entwickler Petja trifft sich in der Kantine auf einen Kaffee mit Ops Igor Ivanovich.
P: Igor Ivanovich, sind Sie sehr beschäftigt? Ich würde gerne herausfinden, wo wir einen Fehler gemacht haben.
IO: Das ist gut, man trifft nicht oft Entwickler, die sich für den Betrieb interessieren.
Zunächst lass uns über einige Dinge klar werden:
- Docker-Philosophie: ein Container – ein Prozess.
- Je kleiner der Container, desto besser.
- Je mehr aus dem Cache verwendet wird, desto besser.
P: Warum sollte in einem Container nur ein Prozess sein?
IO: Docker überwacht beim Starten des Containers den Zustand des Prozesses mit pid 1. Wenn der Prozess stirbt, versucht Docker, den Container neu zu starten. Angenommen, in Ihrem Container laufen mehrere Anwendungen oder die Hauptanwendung wird nicht mit pid 1 gestartet. Wenn der Prozess stirbt, erfährt Docker nichts davon.
Wenn es keine weiteren Fragen gibt, zeige mir dein Dockerfile.
Und Petja zeigte:
FROM ubuntu:latest
# Kopiere den Quellcode
COPY .\/ \/app
WORKDIR \/app
# Aktualisiere die Paketliste
RUN apt-get update
# Aktualisiere die Pakete
RUN apt-get upgrade
# Installiere die benötigten Pakete
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor
# Installiere bundler
RUN gem install bundler
# Installiere nodejs, das für den Bau statischer Dateien verwendet wird
RUN curl -sL https:\/\/deb.nodesource.com\/setup_9.x | sudo bash -
RUN apt-get install -y nodejs
# Installiere die Abhängigkeiten
RUN bundle install --without development test --path vendor\/bundle
# Bereinige die Caches
RUN rm -rf \/usr\/local\/bundle\/cache\/*.gem
RUN apt-get clean
RUN rm -rf \/var\/lib\/apt\/lists\/* \/tmp\/* \/var\/tmp\/*
RUN rake assets:precompile
# Führe das Skript beim Start des Containers aus, das alles andere startet.
CMD ["\/app\/init.sh"]IO: Oh, lass uns der Reihe nach vorgehen. Fangen wir mit der ersten Zeile an:
FROM ubuntu:latestSie verwenden das Tag latest. Die Verwendung des Tags latest führt zu unvorhersehbaren Konsequenzen. Stellen Sie sich vor, der Maintainer des Images erstellt eine neue Version des Images mit einer anderen Liste von Software, dieses Image erhält das Tag latest. Und Ihr Container hört im besten Fall auf, sich zu bauen, oder im schlimmsten Fall laufen Sie in Bugs, die vorher nicht da waren.
Sie verwenden ein Image mit einem vollständigen Betriebssystem und viel unnötiger Software, die das Volumen des Containers aufbläht. Je mehr Software vorhanden ist, desto mehr Sicherheitslücken und Schwachstellen gibt es.
Zusätzlich belegt ein größeres Image mehr Speicherplatz auf dem Host und im Registry (wo speicherst du die Images?).
P: Ja, natürlich haben wir eine Registry, die hast du doch eingerichtet.
KI: Also, wo war ich stehen geblieben?.. Ach ja, Volumina… Auch die Netzwerklast steigt. Bei einem einzelnen Image merkt man das nicht, aber bei kontinuierlichem Bauen, Tests und Deployments ist das spürbar. Und wenn du keinen God’s Mode auf AWS hast, bekommst du am Ende auch noch eine schockierend hohe Rechnung.
Deshalb ist es wichtig, das am besten geeignete Image auszuwählen, mit der genauen Version und minimaler Software. Zum Beispiel nimm: FROM ruby:2.5.5-stretch
P: Oh, verstehe. Wo und wie kann ich die vorhandenen Images ansehen? Wie erkenne ich, welches ich brauche?
KI: Normalerweise werden Images von , nicht zu verwechseln mit Pornhub :). Für ein Image gibt es normalerweise mehrere Builds:
Alpine: Die Images sind auf einem minimalistischen Linux-Image gebaut, nur 5 MB groß. Sein Nachteil: Es wurde mit einer eigenen Implementierung von libc erstellt, Standardpakete funktionieren darin nicht. Die Suche und Installation des benötigten Pakets könnte viel Zeit in Anspruch nehmen.
Scratch: Basis-Image, wird nicht zum Erstellen anderer Images verwendet. Es ist ausschließlich dafür gedacht, binares, vorbereitetes Material auszuführen. Es eignet sich ideal zum Ausführen von binären Anwendungen, die alles Notwendige beinhalten, z.B. Go-Anwendungen.
Basierend auf einem bestimmten Betriebssystem, z.B. Ubuntu oder Debian. Ich denke, das muss ich hier nicht weiter erklären.
KI: Jetzt müssen wir alle zusätzlichen Pakete installieren und den Cache aufräumen. Und gleich können wir auch apt-get upgraderausschmeißen. Andernfalls werden bei jedem Build, trotz des festen Tags des Basisimages, unterschiedliche Images erstellt. Die Aktualisierung der Pakete im Image ist eine Aufgabe des Maintainers und geht mit einer Änderung des Tags einher.
P: Ja, ich habe versucht, das zu tun, und so hat es funktioniert:
WORKDIR /app
COPY ./ /app
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
RUN rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*KI: Nicht schlecht, aber hier gibt es auch noch Verbesserungsbedarf. Schau, dieser Befehl:
RUN rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* … entfernt keine Daten aus dem endgültigen Bild, sondern erstellt lediglich eine zusätzliche Schicht ohne diese Daten. So ist es richtig:
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* Aber das ist noch nicht alles. Was hast du da, Ruby? Dann musst du zu Beginn nicht das gesamte Projekt kopieren. Es reicht, Gemfile und Gemfile.lock zu kopieren.
Mit diesem Ansatz wird bundle install nicht bei jeder Änderung des Quellcodes ausgeführt, sondern nur, wenn sich Gemfile oder Gemfile.lock geändert haben.
Die gleichen Methoden gelten auch für andere Sprachen mit Paketmanagern wie npm, pip, composer und anderen, die auf einer Liste von Abhängigkeiten basieren.
Und schließlich, erinnerst du dich, dass ich am Anfang über die Docker-Ideologie „ein Container - ein Prozess“ gesprochen habe? Das bedeutet, dass ein Supervisor nicht erforderlich ist. Auch systemd sollte aus denselben Gründen nicht installiert werden. Im Grunde ist Docker selbst der Supervisor. Und wenn du versuchst, mehrere Prozesse darin zu starten, ist das so, als würde man in einem Prozess des Supervisors mehrere Anwendungen starten.
Beim Bauen erstellst du ein einziges Bild und startest dann die benötigte Anzahl an Containern, sodass in jedem ein Prozess läuft.
Aber dazu später mehr.
P: Ich glaube, ich habe es verstanden. Schau, was herauskommt:
FROM ruby:2.5.5-stretch
WORKDIR /app
COPY Gemfile* /app
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY . /app
RUN rake assets:precompile
CMD ["bundle", "exec", "passenger", "start"]Und wir werden die Daemon-Starts beim Start des Containers überschreiben?
KI: Ja, genau. Übrigens kann man sowohl CMD als auch ENTRYPOINT verwenden. Und herauszufinden, was der Unterschied ist, ist deine Hausaufgabe. Zu diesem Thema gibt es einen guten Artikel auf Habré. .
Also, lass uns weitermachen. Du lädst eine Datei für die Installation von node herunter, aber es gibt keine Garantie, dass darin das enthalten ist, was du benötigst. Du musst eine Validierung hinzufügen. Zum Beispiel so:
Führe curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x
&& echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c -
&& bash setup_9.x
&& rm -rf setup_9.x
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* Mit der Prüfziffer kannst du überprüfen, ob die heruntergeladene Datei korrekt ist.
P: Aber wenn die Datei geändert wird, schlägt der Build fehl.
II: Ja, und das ist, merkwürdigerweise, auch ein Vorteil. Du wirst wissen, dass die Datei sich geändert hat, und kannst nachsehen, was geändert wurde. Man weiß ja nie, vielleicht wurde ein Skript hinzugefügt, das alles löscht, was es erreicht, oder einen Backdoor-Installiert.
P: Danke. Das heißt, die endgültige Dockerfile sieht so aus:
FROM ruby:2.5.5-stretch
WORKDIR /app
COPY Gemfile* /app
RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x
&& echo "958c9a95c4974c918dca773edf6d18b1d1a41434 setup_9.x" | sha1sum -c -
&& bash setup_9.x
&& rm -rf setup_9.x
&& apt-get -y install libpq-dev imagemagick gsfonts nodejs
&& gem install bundler
&& bundle install --without development test --path vendor/bundle
&& rm -rf /usr/local/bundle/cache/*.gem
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY . /app
RUN rake assets:precompile
CMD ["bundle", "exec", "passenger", "start"]P: Igor Ivanovich, danke für die Hilfe. Ich muss jetzt los, ich muss heute noch 10 Commits machen.
Igor Ivanovich, der den hastigen Kollegen mit seinem Blick stoppt, nimmt einen Schluck starken Kaffees. Er denkt ein paar Sekunden über das SLA von 99,9 % und fehlerfreien Code nach, und stellt eine Frage.
II: Wo speichert ihr die Logs?
P: Natürlich in production.log. Übrigens, wie bekommen wir ohne SSH Zugriff darauf?
II: Wenn ihr sie in Dateien lasst, gibt es dafür schon eine Lösung. Der Befehl docker exec ermöglicht es euch, jeden Befehl im Container auszuführen. Zum Beispiel könnt ihr cat für die Logs verwenden. Und wenn ihr den Schlüssel -it verwendet und bash startet (falls es im Container installiert ist), erhaltet ihr interaktiven Zugriff auf den Container.
Aber es ist nicht ratsam, Logs in Dateien zu speichern. Mindestens führt das zu unkontrolliertem Wachstum des Containers, niemand rotiert die Logs. Alle Logs sollten in stdout geworfen werden. Dort können sie dann mit dem Befehl docker logs.
betrachtet werden.
P: Igor Ivanovich, vielleicht sollten wir die Logs in ein gemountetes Verzeichnis auf den physischen Knoten auslagern, wie die Nutzerdaten?
II: Gut, dass du vergessen hast, die auf die Knotenfestplatte geladenen Daten auszulagern. Mit Logs geht das auch, vergiss nur nicht, die Rotation einzustellen.Alles, du kannst loslaufen.
P: Igor Ivanowitsch, was würden Sie empfehlen, zu lesen?
II: Zunächst lese , es gibt kaum jemanden, der Docker besser kennt als sie.
Wenn du ein Praktikum machen möchtest, schau dir das an. Theorie ohne Praxis ist tote Theorie.
Quelle: habr.com
