Въведение в Docker Compose

1. Общ преглед

При широко използване на Docker управлението на няколко различни контейнера бързо става тромаво.

Docker Compose е инструмент, който ни помага да преодолеем този проблем и лесно да боравим с множество контейнери наведнъж.

В този урок ще разгледаме основните му характеристики и мощни механизми.

2. Обяснена конфигурация на YAML

Накратко, Docker Compose работи, като прилага много правила, декларирани в един конфигурационен файл на docker-compose.yml .

Тези YAML правила, както четими от човека, така и оптимизирани за машина, ни предоставят ефективен начин да направим снимка на целия проект от десет хиляди фута в няколко реда.

Почти всяко правило замества конкретна команда на Docker, така че в крайна сметка просто трябва да стартираме:

docker-compose up

Можем да получим десетки конфигурации, приложени от Compose под капака. Това ще ни спести главоболието да ги скриптираме с Bash или нещо друго.

В този файл трябва да посочим версията на файловия формат Compose, поне една услуга и по желание томове и мрежи :

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

Нека да видим какви всъщност са тези елементи.

2.1. Услуги

На първо място, услугите се отнасят до конфигурацията на контейнери .

Например, нека вземем докернизирано уеб приложение, състоящо се от преден край, заден край и база данни: Вероятно бихме разделили тези компоненти на три изображения и бихме ги определили като три различни услуги в конфигурацията:

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

Има множество настройки, които можем да приложим към услугите и ще ги проучим задълбочено по-късно.

2.2. Обеми и мрежи

Томовете , от друга страна, са физически области от дисково пространство, споделяни между хоста и контейнера, или дори между контейнери. С други думи, томът е споделена директория в хоста , видима от някои или от всички контейнери.

По същия начин мрежите дефинират правилата за комуникация между контейнери и между контейнер и хоста . Общите мрежови зони ще направят услугите на контейнерите видими един за друг, докато частните зони ще ги разделят във виртуални пясъчници.

Отново ще научим повече за тях в следващия раздел.

3. Разчленяване на услуга

Нека сега да започнем да проверяваме основните настройки на услугата.

3.1. Издърпване на изображение

Понякога изображението, от което се нуждаем за нашата услуга, вече е публикувано (от нас или от други) в Docker Hub или друг регистър на Docker.

Ако случаят е такъв, тогава се отнасяме към него с атрибута на изображението , като посочваме името и маркера на изображението:

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

3.2. Изграждане на имидж

Вместо това може да се наложи да изградим изображение от изходния код, като прочетем неговия Dockerfile .

Този път ще използваме ключовата дума build , като предаваме пътя към Dockerfile като стойност:

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

Можем да използваме и URL вместо път:

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

Освен това можем да посочим име на изображение във връзка с атрибута build , което ще даде име на изображението, след като е създадено, като го направи достъпно за използване от други услуги:

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

3.3. Конфигуриране на мрежата

Контейнерите на Docker комуникират помежду си в мрежи, създадени неявно или чрез конфигурация от Docker Compose . Услугата може да комуникира с друга услуга в същата мрежа, като просто я препраща към името на контейнера и порта (например network-example-service: 80 ), при условие че сме направили порта достъпен чрез ключовата дума expose :

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

В този случай, между другото, той също би работил, без да го излага, защото директивата expose вече е в изображението Dockerfile.

За да достигнете до контейнер от хоста , портовете трябва да бъдат изложени декларативно чрез ключовата дума ports , което също ни позволява да изберем дали да излагаме порта по различен начин в хоста:

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" ... 

Порт 80 вече ще се вижда от хоста, докато порт 3000 от другите два контейнера ще бъде достъпен на портове 8080 и 8081 в хоста. Този мощен механизъм ни позволява да изпълняваме различни контейнери, излагащи едни и същи портове без сблъсъци .

И накрая, можем да дефинираме допълнителни виртуални мрежи, които да разделят нашите контейнери:

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: {} 

В този последен пример можем да видим, че друга услуга-в-същата мрежа ще може да пинг и да достигне порт 80 на мрежа-пример-услуга , докато друга услуга-в-собствената си мрежа спечели 'т.

3.4. Настройване на томовете

Има три вида на обема: анонимни , именувани и приемащите такива.

Docker управлява анонимни и именувани томове , като автоматично ги монтира в самогенерирани директории в хоста. Докато анонимните томове бяха полезни при по-старите версии на Docker (преди 1.9), именуваните са препоръчителният начин за движение в днешно време. Обемите на хоста също ни позволяват да посочим съществуваща папка в хоста.

Можем да конфигурираме обеми на хоста на ниво услуга и именувани томове във външното ниво на конфигурацията, за да направим последните видими за други контейнери, а не само за този, към който принадлежат:

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: 

Тук и двата контейнера ще имат достъп за четене / запис до споделената папка my-named-global-volume , независимо от различните пътища, към които са я картографирали. Вместо това двата хостови тома ще бъдат достъпни само за том-пример-услуга .

В / ПТУ папка на хоста на файловата система се отнасят за най- / ми-тома / хост обем папка на контейнера.

Тази част от файловата система може да се записва, което означава, че контейнерът може не само да чете, но и да записва (и изтрива) файлове в хост машината.

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

За да спрем безопасно активните услуги, можем да използваме stop , който ще запази контейнери, обеми и мрежи, заедно с всяка модификация, направена в тях:

docker-compose stop

За да възстановите състоянието на нашия проект, вместо това, ние просто тече надолу , което ще унищожи всичко, само с изключение на обема на външната :

docker-compose down

8. Заключение

В този урок научихме за Docker Compose и как работи.

Както обикновено, можем да намерим изходния файл docker-compose.yml на GitHub, заедно с полезна батерия от тестове, налични веднага на следното изображение: