Tutorial: Persistentes Speichern von Daten in einer Container-App mithilfe von Volumes in VS Code

In diesem Tutorial erfahren Sie, wie Sie Daten in einer Containeranwendung persistent speichern. Wenn Sie sie ausführen oder aktualisieren, sind die Daten weiterhin verfügbar. Es gibt zwei Haupttypen von Volumes, die zum persistenten Speichern von Daten verwendet werden. Dieses Tutorial konzentriert sich auf benannte Volumes.

Außerdem erfahren Sie mehr über Bindungsbereitstellungen, die den genauen Bereitstellungspunkt auf dem Host steuern. Sie können Bindungsbereitstellungen verwenden, um Daten persistent zu speichern, aber es können auch weitere Daten in Containern hinzugefügt werden. Wenn Sie an einer Anwendung arbeiten, können Sie eine Bindungsbereitstellung verwenden, um Quellcode in Container einzubinden, damit Codeänderungen erkannt werden können und darauf reagiert werden kann und damit Sie die Änderungen sofort sehen.

In diesem Tutorial werden auch das Schichten von Images, das Zwischenspeichern von Schichten und mehrstufige Builds eingeführt.

In diesem Tutorial lernen Sie Folgendes:

  • Verstehen von Daten in mehreren Containern
  • Persistentes Speichern von Daten mithilfe benannter Volumes
  • Verwenden von Bindungsbereitstellungen
  • Anzeigen der Imageschicht
  • Cacheabhängigkeiten
  • Informationen zu mehrstufigen Builds

Voraussetzungen

Dieses Tutorial stellt eine Fortsetzung des vorherigen Tutorials Erstellen und Freigeben einer Docker-App mit Visual Studio Code dar. Beginnen Sie mit diesem Tutorial, da dies die Voraussetzungen enthält.

Verstehen von containerübergreifenden Daten

In diesem Abschnitt starten Sie zwei Container und erstellen jeweils eine Datei pro Container. Die in einem Container erstellten Dateien sind in einem anderen Container nicht verfügbar.

  1. Starten Sie einen ubuntu-Container mithilfe des folgenden Befehls:

    docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
    

    Dieser Befehl ruft zwei Befehle mithilfe von && auf. Im ersten Abschnitt wird eine einzelne zufällige Zahl ausgewählt und in /data.txt geschrieben. Mit dem zweiten Befehl wird eine Datei überwacht, damit der Container weiter ausgeführt wird.

  2. Klicken Sie in VS Code im Bereich Docker mit der rechten Maustaste auf den Ubuntu-Container, und wählen Sie Attach Shell (Shell anfügen) aus.

    Screenshot shows the Docker extension with a container selected and a context menu with Attach Shell selected.

    Ein Terminal wird geöffnet, das eine Shell im Ubuntu-Container ausführt.

  3. Führen Sie den folgenden Befehl aus, um den Inhalt der /data.txt-Datei anzuzeigen.

    cat /data.txt
    

    Auf dem Terminal wird eine Zahl zwischen 1 und 10000 angezeigt.

    Um dieses Ergebnis über die Befehlszeile anzuzeigen, rufen Sie die Container-ID mit dem Befehl docker ps ab, und führen Sie den folgenden Befehl aus.

    docker exec <container-id> cat /data.txt
    
  4. Starten Sie einen weiteren ubuntu-Container.

    docker run -d ubuntu bash -c "shuf -i 1-10000 -n 1 -o /data.txt && tail -f /dev/null"
    
  5. Verwenden Sie diesen Befehl, um den Inhalt des Ordners anzuzeigen.

    docker run -it ubuntu ls /
    

    Es sollte hier keine data.txt-Datei vorhanden sein, da sie nur für den ersten Container in den temporären Speicher geschrieben wurde.

  6. Wählen Sie diese beiden Ubuntu-Container aus. Klicken Sie mit der rechten Maustaste, und wählen Sie Entfernen aus. Über die Befehlszeile können Sie sie mit dem Befehl docker rm -f entfernen.

Persistentes Speichern Ihrer To-do-Daten mit benannten Volumes

Die Aufgabenlisten-App speichert Daten standardmäßig in einer SQLite-Datenbank unter /etc/todos/todo.db. SQLite Database ist eine relationale Datenbank, die Daten in einer einzelnen Datei speichert. Dieser Ansatz funktioniert für kleine Projekte.

Sie können die einzelne Datei auf dem Host persistent speichern. Wenn Sie sie für den nächsten Container verfügbar machen, kann die Anwendung dort weitermachen, wo sie zuvor aufgehört hat. Indem Sie ein Volume erstellen und an den Ordner anfügen oder einbinden, in dem die Daten gespeichert sind, können Sie die Daten persistent speichern. Der Container schreibt in die Datei todo.db, und diese Daten verbleiben auf dem Host im Volume.

Verwenden Sie für diesen Abschnitt ein benanntes Volume. Docker verwaltet den physischen Speicherort des Volumes auf dem Datenträger. Verweisen Sie auf den Namen des Volumes, und Docker stellt die richtigen Daten bereit.

  1. Erstellen Sie mithilfe des Befehls docker volume create ein Volume.

    docker volume create todo-db
    
  2. Wählen Sie getting-started unter CONTAINER aus, und klicken Sie mit der rechten Maustaste darauf. Wählen Sie Beenden aus, um den App-Container zu beenden.

    Verwenden Sie den Befehl docker stop, um den Container über die Befehlszeile zu beenden.

  3. Starten Sie den Container getting-started, indem Sie den folgenden Befehl verwenden.

    docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
    

    Der Volume-Parameter gibt einzubindende Volume und den Speicherort /etc/todos an.

  4. Aktualisieren Sie Ihren Browser, um die App erneut zu laden. Wenn Sie das Browserfenster geschlossen haben, wechseln Sie zu http://localhost:3000/. Fügen Sie Ihrer Aufgabenliste einige Elemente hinzu.

    Screenshot shows the sample app with several items added to the list.

  5. Entfernen Sie den Container getting-started für die To-do-App. Klicken Sie entweder im Docker-Bereich mit der rechten Maustaste auf den Container, und wählen Sie Entfernen aus, oder verwenden Sie die Befehle docker stop und docker rm.

  6. Starten Sie mithilfe desselben Befehls einen neuen Container:

    docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started
    

    Dieser Befehl bindet dasselbe Laufwerk wir zuvor ein. Aktualisieren Sie Ihren Browser. Die Elemente, die Sie hinzugefügt haben, befinden sich noch auf Ihrer Liste.

  7. Entfernen Sie den Container getting-started erneut.

Benannte Volumes und Bindungsbereitstellungen, die unten erläutert werden, sind die Haupttypen von Volumes, die von einer Standardinstallation der Docker-Engine unterstützt werden.

Eigenschaft Benannte Volumes Binden von Bereitstellungen
Hoststandort Wird von Docker ausgewählt Wird von Ihnen bestimmt
Beispiel für Volumebereitstellung (mithilfe von -v) my-volume:/usr/local/data /path/to/data:/usr/local/data
Auffüllen des neuen Volumes mit Containerinhalten Ja Nein
Unterstützung für Volumetreiber Ja Nein

Es gibt viele Volumetreiber-Plug-Ins zur Unterstützung von NFS, SFTP, NetApp und mehr. Diese Plug-Ins sind besonders wichtig, um Container auf mehreren Hosts in einer Clusterumgebung wie Swarm oder Kubernetes ausführen zu können.

Wenn Sie sich fragen, wo Docker Ihre Daten tatsächlich speichert, führen Sie den folgenden Befehl aus.

docker volume inspect todo-db

Sehen Sie sich die Ausgabe an, die diesem Ergebnis ähnelt.

[
    {
        "CreatedAt": "2019-09-26T02:18:36Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
        "Name": "todo-db",
        "Options": {},
        "Scope": "local"
    }
]

Mountpoint entspricht dem tatsächlichen Speicherort, an dem die Daten gespeichert werden. Auf den meisten Computern benötigen Sie Stammzugriff, um vom Host aus auf dieses Verzeichnis zugreifen zu können.

Verwenden von Bindungsbereitstellungen

Mithilfe von Bindungsbereitstellungen können Sie den exakten Bereitstellungspunkt auf dem Host bestimmen. Bei diesem Ansatz werden Daten persistent gespeichert, er wird aber häufig verwendet, um mehr Daten in Containern zur Verfügung zu stellen. Sie können eine Bindungsbereitstellung verwenden, um Quellcode in Container einzubinden, damit Codeänderungen erkannt werden können und darauf reagiert werden kann und damit Sie die Änderungen sofort sehen.

Wenn Sie einen Container so ausführen möchten, dass ein Entwicklungsworkflow unterstützt wird, müssen Sie die folgenden Schritte ausführen:

  1. Entfernen Sie alle getting-started-Container.

  2. Führen Sie im app-Ordner den folgenden Befehl aus.

    docker run -dp 3000:3000 -w /app -v ${PWD}:/app node:20-alpine sh -c "yarn install && yarn run dev"
    

    Dieser Befehl enthält die folgenden Parameter.

    • -dp 3000:3000: Wie zuvor. Führen Sie ihn im getrennten Modus aus, und erstellen Sie eine Portzuordnung.
    • -w /app: Arbeitsverzeichnis im Container.
    • -v ${PWD}:/app": Hier wird eine Bindungsbereitstellung für das aktuelle Verzeichnis vom Host im Container in das /app-Verzeichnis durchgeführt.
    • node:20-alpine: Dies ist das zu verwendende Image. Bei diesem Image handelt es sich um das Basisimage für Ihre App aus dem Dockerfile.
    • sh -c "yarn install && yarn run dev": Dies ist ein Befehl. Er startet eine Shell sh mit und führt yarn install aus, um alle Abhängigkeiten zu installieren. Anschließend wird yarn run dev ausgeführt. Wenn Sie die package.json-Datei genauer ansehen, wird nodemon vom Skript dev gestartet.
  3. Mithilfe von docker logs können Sie sich die Protokolle ansehen.

    docker logs -f <container-id>
    
    $ nodemon src/index.js
    [nodemon] 2.0.20
    [nodemon] to restart at any time, enter `rs`
    [nodemon] watching path(s): *.*
    [nodemon] watching extensions: js,mjs,json
    [nodemon] starting `node src/index.js`
    Using sqlite database at /etc/todos/todo.db
    Listening on port 3000
    

    Wenn der endgültige Eintrag in dieser Liste angezeigt wird, wird die App ausgeführt.

    Wenn Sie sich die Protokolle angesehen haben, wählen Sie einen beliebigen Schlüssel im Terminalfenster aus, oder drücken Sie STRG+C in einem externen Fenster.

  4. Öffnen Sie in VS Code src/static/js/app.js. Ändern Sie den Text der Schaltfläche Element hinzufügen in Zeile 109.

    - {submitting ? 'Adding...' : 'Add Item'}
    + {submitting ? 'Adding...' : 'Add'}
    

    Speichern Sie die Änderung.

  5. Aktualisieren Sie Ihren Browser. Daraufhin sollte die Änderung angezeigt werden.

    Screenshot shows the sample app with the new text on the button.

Anzeigen von Imageschichten

Sie können sich die Schichten ansehen, die ein Image bilden. Führen Sie den docker image history-Befehl aus, um den Befehl anzuzeigen, der verwendet wurde, um die einzelnen Schichten eines Images zu erstellen.

  1. Verwenden Sie docker image history, um die Schichten im Image getting-started anzuzeigen, das Sie zuvor im Tutorial erstellt haben.

    docker image history getting-started
    

    Das Ergebnis sollte folgender Ausgabe ähneln.

    IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
    a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "/app/src/ind…   0B                  
    f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
    a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
    9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
    b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
    <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
    <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
    <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
    <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
    <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
    <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
    <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
    <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB   
    

    Jede der Zeilen steht für eine Schicht des Images. Die Ausgabe hier zeigt die Basis ganz unten, die neueste Schicht wird ganz oben angezeigt. Mithilfe dieser Informationen können Sie sich auch die Größe der einzelnen Schichten ansehen, was bei der Diagnose großer Images hilft.

  2. Einige der Zeilen werden abgeschnitten. Wenn Sie den --no-trunc-Parameter hinzufügen, erhalten Sie die vollständige Ausgabe.

    docker image history --no-trunc getting-started
    

Cacheabhängigkeiten

Sobald eine Schicht geändert wird, müssen alle Schichten, die sich unter dieser befinden, ebenfalls neu erstellt werden. Hier sehen Sie erneut das Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "/app/src/index.js"]

Jeder Befehl im Dockerfile wird zu einer neuen Schicht im Image. Um die Anzahl der Schichten zu minimieren, können Sie Ihren Dockerfile neu strukturieren, um das Zwischenspeichern von Abhängigkeiten zu unterstützen. Bei Node-basierten Anwendungen sind diese Abhängigkeiten in der package.json-Datei definiert.

Der Ansatz sieht so aus, dass Sie zuerst nur diese Datei kopieren, die Abhängigkeiten installieren und dann den Rest kopieren. In diesem Prozess werden die YARN-Abhängigkeiten nur erneut erstellt, wenn eine Änderung an package.json vorgenommen wurde.

  1. Aktualisieren Sie das Dockerfile, damit zuerst package.json kopiert wird, installieren Sie dann die Abhängigkeiten, und kopieren Sie dann den Rest. Dies ist die neue Datei:

    FROM node:20-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production
    COPY . .
    CMD ["node", "/app/src/index.js"]
    
  2. Erstellen Sie mithilfe von docker build ein neues Image.

    docker build -t getting-started .
    

    Die Ausgabe sollte wie die folgenden Ergebnisse aussehen:

    Sending build context to Docker daemon  219.1kB
    Step 1/6 : FROM node:12-alpine
    ---> b0dc3a5e5e9e
    Step 2/6 : WORKDIR /app
    ---> Using cache
    ---> 9577ae713121
    Step 3/6 : COPY package* yarn.lock ./
    ---> bd5306f49fc8
    Step 4/6 : RUN yarn install --production
    ---> Running in d53a06c9e4c2
    yarn install v1.17.3
    [1/4] Resolving packages...
    [2/4] Fetching packages...
    info fsevents@1.2.9: The platform "linux" is incompatible with this module.
    info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
    [3/4] Linking dependencies...
    [4/4] Building fresh packages...
    Done in 10.89s.
    Removing intermediate container d53a06c9e4c2
    ---> 4e68fbc2d704
    Step 5/6 : COPY . .
    ---> a239a11f68d8
    Step 6/6 : CMD ["node", "/app/src/index.js"]
    ---> Running in 49999f68df8f
    Removing intermediate container 49999f68df8f
    ---> e709c03bc597
    Successfully built e709c03bc597
    Successfully tagged getting-started:latest
    

    Alle Schichten wurden neu erstellt. Dieses Ergebnis wird erwartet, da Sie das Dockerfile geändert haben.

  3. Nehmen Sie eine Änderung an src/static/index.html vor. Ändern Sie beispielsweise den Titel in „The Awesome Todo App“.

  4. Erstellen Sie nun mithilfe von docker build das Docker-Image noch mal. Dieses Mal sollte Ihre Ausgabe etwas anders aussehen.

    Sending build context to Docker daemon  219.1kB
    Step 1/6 : FROM node:12-alpine
    ---> b0dc3a5e5e9e
    Step 2/6 : WORKDIR /app
    ---> Using cache
    ---> 9577ae713121
    Step 3/6 : COPY package* yarn.lock ./
    ---> Using cache
    ---> bd5306f49fc8
    Step 4/6 : RUN yarn install --production
    ---> Using cache
    ---> 4e68fbc2d704
    Step 5/6 : COPY . .
    ---> cccde25a3d9a
    Step 6/6 : CMD ["node", "/app/src/index.js"]
    ---> Running in 2be75662c150
    Removing intermediate container 2be75662c150
    ---> 458e5c6f080c
    Successfully built 458e5c6f080c
    Successfully tagged getting-started:latest
    

    Da Sie den Buildcache verwenden, sollte der Vorgang viel schneller gehen.

Mehrstufige Builds

Mehrstufige Builds sind eine äußerst effiziente Methode, mit der Sie mehrere Stufen zum Erstellen eines Images verwenden können. Daraus entstehen mehrere Vorteile:

  • Abgrenzen von Abhängigkeiten zur Buildzeit von Laufzeitabhängigkeiten
  • Reduzieren der Imagegesamtgröße, indem nur geliefert wird, was die App für ihre Ausführung benötigt

Dieser Abschnitt enthält kurze Beispiele.

Maven/Tomcat-Beispiel

Wenn Sie Java-basierte Anwendungen erstellen, ist ein JDK erforderlich, um den Quellcode in Java-Bytecode zu kompilieren. Dieses JDK ist nicht für die Produktion erforderlich. Sie verwenden möglicherweise Tools wie Maven oder Gradle für die Erstellung der App. Diese Tools sind aber ebenfalls nicht für Ihr endgültiges Image erforderlich.

FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

In diesem Beispiel wird eine Stufe namens build verwendet, um den tatsächlichen Java-Buildvorgang mithilfe von Maven durchzuführen. Die zweite Stufe (ab „FROM tomcat“) kopiert Dateien aus der build-Stufe. Beim endgültigen Image handelt es sich nur um die letzte Stufe, die erstellt wird. Diese kann jedoch mithilfe des --target-Parameters überschrieben werden.

React-Beispiel

Für die Erstellung von React-Anwendungen benötigen Sie eine Node-Umgebung, um z. B. den JavaScript-Code und SASS-Stylesheets in statische HTML-, JavaScript- und CSS-Sprache zu kompilieren. Wenn Sie kein serverseitiges Rendering vornehmen, benötigen Sie auch keine Node-Umgebung für den Produktionsbuild.

FROM node:20-alpine AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

In diesem Beispiel wird ein node:20-Image verwendet, um den Build auszuführen, wodurch die Zwischenspeicherung von Schichten maximiert und anschließend die Ausgabe in einen nginx-Container kopiert wird.

Bereinigen von Ressourcen

Behalten Sie alles, was Sie bereits erarbeitet haben, so bei, um mit dieser Tutorialreihe fortzufahren.

Nächste Schritte

Sie haben die Optionen zum persistenten Speichern von Daten für Container-Apps kennengelernt.

Was möchten Sie als Nächstes tun?