Wprowadzenie do Docker Compose

1. Przegląd

Przy intensywnym korzystaniu z Dockera zarządzanie kilkoma różnymi kontenerami szybko staje się uciążliwe.

Docker Compose to narzędzie, które pomaga nam przezwyciężyć ten problem i łatwo obsługiwać wiele kontenerów jednocześnie.

W tym samouczku przyjrzymy się jego głównym funkcjom i potężnym mechanizmom.

2. Wyjaśnienie konfiguracji YAML

Krótko mówiąc, Docker Compose działa poprzez zastosowanie wielu reguł zadeklarowanych w jednym pliku konfiguracyjnym docker-compose.yml .

Te reguły YAML, zarówno czytelne dla człowieka, jak i zoptymalizowane pod kątem maszyn, zapewniają nam skuteczny sposób tworzenia migawek całego projektu z odległości dziesięciu tysięcy stóp w kilku wierszach.

Prawie każda reguła zastępuje konkretne polecenie Dockera, tak że na koniec wystarczy uruchomić:

docker-compose up

Możemy dostać dziesiątki konfiguracji zastosowanych przez Compose pod maską. Pozwoli nam to zaoszczędzić nam kłopotów ze skryptowaniem ich za pomocą Bash lub czegoś innego.

W tym pliku musimy określić wersję formatu pliku Compose, co najmniej jedną usługę i opcjonalnie woluminy i sieci :

version: "3.7" services: ... volumes: ... networks: ... 

Zobaczmy, czym właściwie są te elementy.

2.1. Usługi

Usługi dotyczą przede wszystkim konfiguracji kontenerów .

Na przykład weźmy dokeryzowaną aplikację internetową składającą się z interfejsu, zaplecza i bazy danych: prawdopodobnie podzielilibyśmy te komponenty na trzy obrazy i zdefiniowalibyśmy je jako trzy różne usługi w konfiguracji:

services: frontend: image: my-vue-app ... backend: image: my-springboot-app ... db: image: postgres ... 

Istnieje wiele ustawień, które możemy zastosować do usług, które później szczegółowo zbadamy.

2.2. Woluminy i sieci

Z drugiej strony woluminy to fizyczne obszary przestrzeni dyskowej współdzielone między hostem a kontenerem, a nawet między kontenerami. Innymi słowy, wolumin jest wspólnym katalogiem na hoście , widocznym z niektórych lub wszystkich kontenerów.

Podobnie sieci definiują reguły komunikacji między kontenerami oraz między kontenerem a hostem . Wspólne strefy sieciowe umożliwią wzajemne wykrywanie usług kontenerów, podczas gdy strefy prywatne będą segregować je w wirtualnych piaskownicach.

Ponownie, dowiemy się o nich więcej w następnej sekcji.

3. Analiza usługi

Zacznijmy teraz sprawdzać główne ustawienia usługi.

3.1. Ciągnięcie obrazu

Czasami obraz, którego potrzebujemy do naszej usługi, został już opublikowany (przez nas lub przez innych) w Docker Hub lub innym rejestrze Docker Registry.

W takim przypadku odwołujemy się do niego za pomocą atrybutu obrazu , podając nazwę i tag obrazu:

services: my-service: image: ubuntu:latest ... 

3.2. Tworzenie obrazu

Zamiast tego może być konieczne utworzenie obrazu z kodu źródłowego, odczytując jego plik Dockerfile .

Tym razem użyjemy słowa kluczowego build , przekazując ścieżkę do Dockerfile jako wartość:

services: my-custom-app: build: /path/to/dockerfile/ ... 

Zamiast ścieżki możemy też użyć adresu URL:

services: my-custom-app: build: //github.com/my-company/my-project.git ... 

Dodatkowo możemy określić nazwę obrazu w połączeniu z atrybutem build , który po utworzeniu nada nazwę obrazowi, udostępniając go innym usługom:

services: my-custom-app: build: //github.com/my-company/my-project.git image: my-project-image ... 

3.3. Konfiguracja sieci

Kontenery platformy Docker komunikują się między sobą w sieciach utworzonych niejawnie lub poprzez konfigurację przez Docker Compose . Usługa może komunikować się z inną usługą w tej samej sieci, po prostu odwołując się do niej poprzez nazwę kontenera i port (na przykład sieć-przykład-usługa: 80 ), pod warunkiem, że udostępniliśmy port za pomocą słowa kluczowego expose :

services: network-example-service: image: karthequian/helloworld:latest expose: - "80" 

Nawiasem mówiąc, w tym przypadku działałoby to również bez ujawniania go, ponieważ dyrektywa expose jest już w pliku Dockerfile obrazu.

Aby dotrzeć do kontenera z hosta , porty muszą być ujawnione deklaratywnie za pomocą słowa kluczowego ports , co pozwala nam również wybrać, czy port ma być inaczej eksponowany na hoście:

services: network-example-service: image: karthequian/helloworld:latest ports: - "80:80" ... my-custom-app: image: myapp:latest ports: - "8080:3000" ... my-custom-app-replica: image: myapp:latest ports: - "8081:3000" ... 

Port 80 będzie teraz widoczny z hosta, a port 3000 pozostałych dwóch kontenerów będzie dostępny na portach 8080 i 8081 na hoście. Ten potężny mechanizm pozwala nam uruchamiać różne kontenery, udostępniając te same porty bez kolizji .

Na koniec możemy zdefiniować dodatkowe sieci wirtualne, aby posegregować nasze kontenery:

services: network-example-service: image: karthequian/helloworld:latest networks: - my-shared-network ... another-service-in-the-same-network: image: alpine:latest networks: - my-shared-network ... another-service-in-its-own-network: image: alpine:latest networks: - my-private-network ... networks: my-shared-network: {} my-private-network: {} 

W tym ostatnim przykładzie widzimy, że inna usługa-w-tej-samej-sieci będzie w stanie pingować i dotrzeć do portu 80 przykładowej-usługi-sieci , podczas gdy inna-usługa-w-swojej-własnej-sieci wygrywa 't.

3.4. Konfigurowanie woluminów

Istnieją trzy typy woluminów: anonimowe , nazwane i gospodarze z nich.

Docker zarządza zarówno anonimowymi, jak i nazwanymi woluminami , automatycznie montując je w samodzielnie wygenerowanych katalogach na hoście. Podczas gdy anonimowe tomy były przydatne w starszych wersjach Dockera (przed 1.9), nazwane są obecnie sugerowaną drogą. Woluminy hosta pozwalają nam również określić istniejący folder na hoście.

Możemy skonfigurować woluminy hosta na poziomie usług, a nazwane woluminy na zewnętrznym poziomie konfiguracji, aby te ostatnie były widoczne dla innych kontenerów, a nie tylko dla tego, do którego należą:

services: volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/my-volumes/named-global-volume - /tmp:/my-volumes/host-volume - /home:/my-volumes/readonly-host-volume:ro ... another-volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/another-path/the-same-named-global-volume ... volumes: my-named-global-volume: 

W tym przypadku oba kontenery będą miały dostęp do odczytu / zapisu do folderu współdzielonego o nazwie mój globalny wolumin , bez względu na różne ścieżki, na które go zmapowali. Zamiast tego dwa woluminy hosta będą dostępne tylko dla woluminów-przykład-usługa .

/ Tmp folderze systemu plików hosta jest mapowany do / my-tomy / host-objętość folderze pojemnika.

Ta część systemu plików jest zapisywalna, co oznacza, że ​​kontener może nie tylko czytać, ale także zapisywać (i usuwać) pliki na komputerze hosta.

We can mount a volume in read-only mode by appending :ro to the rule, like for the /home folder (we don't want a Docker container erasing our users by mistake).

3.5. Declaring the Dependencies

Often, we need to create a dependency chain between our services, so that some services get loaded before (and unloaded after) other ones. We can achieve this result through the depends_on keyword:

services: kafka: image: wurstmeister/kafka:2.11-0.11.0.3 depends_on: - zookeeper ... zookeeper: image: wurstmeister/zookeeper ... 

We should be aware, however, that Compose will not wait for the zookeeper service to finish loading before starting the kafka service: it will simply wait for it to start. If we need a service to be fully loaded before starting another service, we need to get deeper control of startup and shutdown order in Compose.

4. Managing Environment Variables

Working with environment variables is easy in Compose. We can define static environment variables, and also define dynamic variables with the ${} notation:

services: database: image: "postgres:${POSTGRES_VERSION}" environment: DB: mydb USER: "${USER}" 

There are different methods to provide those values to Compose.

For example, one is setting them in a .env file in the same directory, structured like a .properties file, key=value:

POSTGRES_VERSION=alpine USER=foo

Otherwise, we can set them in the OS before calling the command:

export POSTGRES_VERSION=alpine export USER=foo docker-compose up 

Finally, we might find handy using a simple one-liner in the shell:

POSTGRES_VERSION=alpine USER=foo docker-compose up 

We can mix the approaches, but let's keep in mind that Compose uses the following priority order, overwriting the less important with the higher ones:

  1. Compose file
  2. Shell environment variables
  3. Environment file
  4. Dockerfile
  5. Variable not defined

5. Scaling & Replicas

In older Compose versions, we were allowed to scale the instances of a container through the docker-compose scale command. Newer versions deprecated it and replaced it with the scale option.

On the other side, we can exploit Docker Swarm – a cluster of Docker Engines – and autoscale our containers declaratively through the replicas attribute of the deploy section:

services: worker: image: dockersamples/examplevotingapp_worker networks: - frontend - backend deploy: mode: replicated replicas: 6 resources: limits: cpus: '0.50' memory: 50M reservations: cpus: '0.25' memory: 20M ... 

Under deploy, we can also specify many other options, like the resources thresholds. Compose, however, considers the whole deploy section only when deploying to Swarm, and ignores it otherwise.

6. A Real-World Example: Spring Cloud Data Flow

While small experiments help us understanding the single gears, seeing the real-world code in action will definitely unveil the big picture.

Spring Cloud Data Flow is a complex project, but simple enough to be understandable. Let's download its YAML file and run:

DATAFLOW_VERSION=2.1.0.RELEASE SKIPPER_VERSION=2.0.2.RELEASE docker-compose up 

Compose will download, configure, and start every component, and then intersect the container's logs into a single flow in the current terminal.

It'll also apply unique colors to each one of them for a great user experience:

We might get the following error running a brand new Docker Compose installation:

lookup registry-1.docker.io: no such host

While there are different solutions to this common pitfall, using 8.8.8.8 as DNS is probably the simplest.

7. Lifecycle Management

Let's finally take a closer look at the syntax of Docker Compose:

docker-compose [-f ...] [options] [COMMAND] [ARGS...] 

While there are many options and commands available, we need at least to know the ones to activate and deactivate the whole system correctly.

7.1. Startup

We've seen that we can create and start the containers, the networks, and the volumes defined in the configuration with up:

docker-compose up

After the first time, however, we can simply use start to start the services:

docker-compose start

In case our file has a different name than the default one (docker-compose.yml), we can exploit the -f and file flags to specify an alternate file name:

docker-compose -f custom-compose-file.yml start

Compose can also run in the background as a daemon when launched with the -d option:

docker-compose up -d

7.2. Shutdown

Aby bezpiecznie zatrzymać aktywne usługi, możemy użyć stop , który zachowa kontenery, woluminy i sieci, wraz z każdą wprowadzoną w nich modyfikacją:

docker-compose stop

Aby przywrócić stan naszego projektu, zamiast, po prostu uruchomić w dół , która zniszczy wszystko, tylko z wyjątkiem woluminów zewnętrznych :

docker-compose down

8. Wniosek

W tym samouczku dowiedzieliśmy się o Docker Compose i o tym, jak to działa.

Jak zwykle możemy znaleźć źródłowy plik docker-compose.yml na GitHub, wraz z pomocną baterią testów natychmiast dostępną na poniższym obrazku: