Docker Compose
В данном практическом занятии предлагается ознакомиться с возможностью сборки и развертывания среды из нескольких связанных контейнеров при помощи docker-compose.
Vagrant
Для работы с docker
можно воспользоваться следующим Vagrantfile
:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/lunar64"
config.vm.provision "docker"
config.vm.network "forwarded_port", guest: 8888, host: 8888
end
Используя docker provisioner
на машину также установится docker compose
в виде плагина.
Либо можно воспользоваться инструкцией с официального сайта.
Project
Создадим файл compose.yaml
с простой конфигурацией:
name: app
services:
front:
image: nginx:1.25
ports:
- 8888:80
restart: always
Здесь мы указали:
name - имя нашего проекта, которое будет использоваться в качестве префикса к созданным нами ресурсам(по-умолчанию используется имя директории)
services - список сервисов для которых требуется запускать контейнеры
front - имя нашего первого сервиса
image - образ контейнера
ports - список пробрасываемых портов
restart - политика перезапуска при завершении
Для запуска достаточно выполнить команду docker compose up
:
$ docker compose up -d
[+] Running 8/8
✔ front 7 layers [⣿⣿⣿⣿⣿⣿⣿] 0B/0B Pulled 10.8s
✔ a803e7c4b030 Pull complete 5.6s
✔ 8b625c47d697 Pull complete 6.3s
✔ 4d3239651a63 Pull complete 0.7s
✔ 0f816efa513d Pull complete 1.7s
✔ 01d159b8db2f Pull complete 2.2s
✔ 5fb9a81470f3 Pull complete 3.0s
✔ 9b1e1e7164db Pull complete 3.6s
[+] Running 2/2
✔ Network app_default Created 0.1s
✔ Container app-front-1 Started 0.2s
Ключ -d
запускает контейнеры в фоновом режиме. Как видно из вывода - автоматически
скачалася необходимый образ, а также создалась кастомная сеть app_default
. Если
сейчас сделать запрос на адрес localhost:8888
, то мы получим стандартное приглашение
от nginx
:
$ curl -s localhost:8888 | grep title
<title>Welcome to nginx!</title>
Информацию же по запущенным контейнерам для сервисов в compose.yaml
файле можно
получить с помощью подкоманд docker compose
, таких как ls
, ps
и top
:
$ docker compose ls
NAME STATUS CONFIG FILES
app running(1) /home/vagrant/compose.yaml
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
app-front-1 nginx:1.25 "/docker-entrypoint.sh nginx -g 'daemon off;'" front 10 minutes ago Up 10 minutes 0.0.0.0:8888->80/tcp, :::8888->80/tcp
$ docker compose top
app-front-1
UID PID PPID C STIME TTY TIME CMD
root 3909 3888 0 21:18 ? 00:00:00 nginx: master process nginx -g daemon off;
syslog 3963 3909 0 21:18 ? 00:00:00 nginx: worker process
syslog 3964 3909 0 21:18 ? 00:00:00 nginx: worker process
Front
Сделаем в сервисе front
отдачу статичной html страницы, которая будет располагаться
в директории проекта, а также добавим конфигурацию для проксирования в будущий back
.
Страница будет выглядеть таким образом:
<!DOCTYPE html>
<html>
<body>
<h2>Users table:</h2>
<p id="users"></p>
<script>
var req = function() {
var http = new XMLHttpRequest();
http.onload = function() {
const users = JSON.parse(this.responseText);
let text = "<table border='1'>"
for (let x in users) {
text += "<tr><td>" + users[x].name + "</td>";
text += "<td>" + users[x].email + "</td></tr>";
}
text += "</table>"
document.getElementById("users").innerHTML = text;
}
http.open("GET", "/back");
http.send();
}
setInterval(req, 1000);
</script>
</body>
</html>
А конфигурация для nginx
так:
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location /back {
resolver 127.0.0.11;
set $backend http://back;
proxy_pass $backend;
}
}
Сохраним их в файлах index.html
и default.conf
соответственно в директорию проекта.
Сам же compose.yaml
дополним следующим образом:
name: app
services:
front:
image: nginx
ports:
- 8888:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html
configs:
- source: nginx
target: /etc/nginx/conf.d/default.conf
restart: always
configs:
nginx:
file: ./default.conf
Здесь мы используем параметр volumes
для проброса файла index.html
внутрь контейнера,
а также параметры configs
для определения конфигурационного файла и передачи его
также внутрь контейнера. Для применения данной конфигурации также запустим команду
docker compose up
:
$ docker compose up -d
[+] Running 1/1
✔ Container app-front-1 Started 0.2s
$ curl -s localhost:8888 | grep h2
<h2>Users table:</h2>
DB
Добавим сервис в наш compose.yaml
с базой данных, которая будет содержать
таблицу с пользователями. Чтобы не хранить данные для авторизации в compose.yaml
можно использовать файл .env
, в котором можно сохранить чувствительные данные, а также,
например, исключить из системы контроля версий. Создадим .env
файл с данными для
авторизации в субд:
DB_USER="app"
DB_PASS="pass"
Также создадим файл users.sql
для инициализации базы:
grant all on database app to app;
\connect app;
create table users(
id serial primary key,
name varchar(50),
email varchar(100)
);
grant all on all tables in schema public to app;
Теперь дополним compose.yaml
новым сервисом db
:
name: app
services:
front:
image: nginx
ports:
- 8888:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html
configs:
- source: nginx
target: /etc/nginx/conf.d/default.conf
restart: always
db:
image: postgres
environment:
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASS}"
volumes:
- ./users.sql:/docker-entrypoint-initdb.d/users.sql
restart: always
configs:
nginx:
file: ./default.conf
Применим новую конфигурацию:
$ docker compose up -d
[+] Running 14/14
✔ db 13 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿] 0B/0B Pulled 19.0s
✔ a803e7c4b030 Already exists 0.0s
✔ 009c876521a0 Pull complete 0.5s
✔ 9c412905cca2 Pull complete 0.9s
✔ 6463d4bf467a Pull complete 1.0s
✔ bd8b983728ed Pull complete 3.0s
✔ febc167f3560 Pull complete 1.5s
✔ d73c81c4ade3 Pull complete 2.1s
✔ 34b3b0ac6e9e Pull complete 1.9s
✔ 9bd86d074f4e Pull complete 12.7s
✔ 406f63329750 Pull complete 3.7s
✔ ec40772694b7 Pull complete 3.9s
✔ 7d3dfa1637e9 Pull complete 4.4s
✔ e217ca41159f Pull complete 4.4s
[+] Running 2/2
✔ Container app-db-1 Started 0.3s
✔ Container app-front-1 Running 0.0s
$ docker exec -it app-db-1 su - postgres -c 'psql -U app app -c \\d'
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+-------
public | users | table | app
public | users_id_seq | sequence | app
(2 rows)
Теперь у нас есть два работающих сервиса front
и db
. Как видно база была
инициализирована за счет монтирования файла users.sql
по пути
docker-entrypoint-initdb.d
- это происходит только если она пуста.
Back
Осталось добавить сервис back
, который будет принимать запросы из front
по http
и возвращать список пользователей из db
. Можете использовать свой любимый язык, на
golang
приложение может выглядеть так:
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/jackc/pgx/v5"
)
type users struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
ctx := context.Background()
connStr, err := os.ReadFile("/run/secrets/connection_string")
if err != nil {
fmt.Fprintf(os.Stderr, "Error read connection secret: %v\n", err)
os.Exit(1)
}
conn, err := pgx.Connect(ctx, string(connStr))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
defer conn.Close(ctx)
http.ListenAndServe("0.0.0.0:80", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
rows, err := conn.Query(ctx, "select * from users")
if err != nil {
fmt.Printf("error db query: %s", err)
return
}
users, err := pgx.CollectRows(rows, pgx.RowToStructByName[users])
if err != nil {
fmt.Printf("error collect rows: %s", err)
return
}
jsonUsers, err := json.Marshal(users)
if err != nil {
fmt.Printf("error marshal json: %s", err)
}
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Write(jsonUsers)
}),
)
}
Приложение будет прослушивать порт 80
и подключаться к базе взяв данные для подключения
из файла /run/secrets/connection_string
. Сами параметры подключения также занесем
в файл .env
, так они содержат имя пользователя и пароль:
DB_USER="app"
DB_PASS="pass"
DB_CONN="postgres://app:pass@db:5432/app?sslmode=disable"
Для сборки потребуется следующий Dockerfile
:
FROM golang:1.21 as build
WORKDIR /src
COPY main.go /src/main.go
RUN go mod init example \
&& go mod tidy \
&& CGO_ENABLED=0 go build -o /bin/app ./main.go
FROM scratch
COPY --from=build /bin/app /app
CMD ["/app"]
Добавим сервис back
в compose.yaml
:
name: app
services:
front:
image: nginx
ports:
- 8888:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html
configs:
- source: nginx
target: /etc/nginx/conf.d/default.conf
restart: always
back:
image: back
build: .
depends_on:
- db
restart: always
secrets:
- connection_string
db:
image: postgres
environment:
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASS}"
volumes:
- ./users.sql:/docker-entrypoint-initdb.d/users.sql
restart: always
configs:
nginx:
file: ./default.conf
secrets:
connection_string:
environment: "DB_CONN"
Для данного сервиса мы указали путь для сборки образа, также указали его зависимость от
сервиса db
, а параметры подключения к субд вынесли в отдельный блок secrets
.
После запуска команды docker compose up
при отсутствии образа происходит сборка,
либо можно явно указать опцию --build
:
$ docker compose up -d --build
[+] Running 1/1
! back Warning 1.6s
[+] Building 66.7s (10/10) FINISHED docker:default
=> [back internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 260B 0.0s
=> [back internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [back internal] load metadata for docker.io/library/golang:1.21 1.8s
=> [back build 1/4] FROM docker.io/library/golang:1.21@sha256:e9ebfe932adeff65af5338 34.5s
=> => resolve docker.io/library/golang:1.21@sha256:e9ebfe932adeff65af5338236f0b0604c8 0.0s
=> => sha256:b47a222d28fa95680198398973d0a29b82a968f03e7ef361cc8ded 24.03MB / 24.03MB 6.4s
=> => sha256:debce5f9f3a9709885f7f2ad3cf41f036a3b57b406b27ba3a8839 64.11MB / 64.11MB 16.0s
=> => sha256:e9ebfe932adeff65af5338236f0b0604c86b143c1bff3e1d0551d8f6 2.36kB / 2.36kB 0.0s
=> => sha256:e63f8cf91f5b59ce217f2804936132b2964b04bb5b06c0a79b9e3799 1.58kB / 1.58kB 0.0s
=> => sha256:6285f5529f1ba8755fd2469d255fa9e0429d5fa8c6003d52da5ea098 7.22kB / 7.22kB 0.0s
=> => sha256:167b8a53ca4504bc6aa3182e336fa96f4ef76875d158c1933d3e2 49.56MB / 49.56MB 13.8s
=> => sha256:91b457aaf04f424db4f223ea7aad4b196d4a62da58d6f45938233 92.30MB / 92.30MB 25.5s
=> => sha256:ab90286b1543edaec4dd9402a487a164307c216d646369c8e5924 66.99MB / 66.99MB 27.1s
=> => extracting sha256:167b8a53ca4504bc6aa3182e336fa96f4ef76875d158c1933d3e2fa19c57e 2.9s
=> => sha256:1bcec06b980f5d54c94a469aeb286627475f645205140fb3ed0514e32ce 156B / 156B 16.2s
=> => extracting sha256:b47a222d28fa95680198398973d0a29b82a968f03e7ef361cc8ded562e4d8 0.9s
=> => extracting sha256:debce5f9f3a9709885f7f2ad3cf41f036a3b57b406b27ba3a883928315787 3.7s
=> => extracting sha256:91b457aaf04f424db4f223ea7aad4b196d4a62da58d6f45938233e0f54bd1 3.3s
=> => extracting sha256:ab90286b1543edaec4dd9402a487a164307c216d646369c8e59248fb92da4 5.2s
=> => extracting sha256:1bcec06b980f5d54c94a469aeb286627475f645205140fb3ed0514e32cebe 0.0s
=> [back internal] load build context 0.0s
=> => transferring context: 1.22kB 0.0s
=> [back build 2/4] WORKDIR /src 0.2s
=> [back build 3/4] COPY main.go /src/main.go 0.0s
=> [back build 4/4] RUN go mod init example && go mod tidy && CGO_ENABLED=0 go b 29.7s
=> [back stage-1 1/1] COPY --from=build /bin/app /app 0.1s
=> [back] exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:c255013929d1de7c54527cbd2dd09cdb03a2cc0a932cfc44b62d924ada 0.0s
=> => naming to docker.io/library/back 0.0s
[+] Running 3/3
✔ Container app-front-1 Running 0.0s
✔ Container app-db-1 Running 0.0s
✔ Container app-back-1 Started 0.0s
Теперь у нас подняты все сервисы для полноценного функционирования нашего проекта, добавим пользователя в нашу базу:
docker exec -it app-db-1 su - postgres -c "psql -U app app -c \"insert into users (name,email) values ('alex', 'alex@mail.ru');\""
Убедимся в работе на страницы нашего проекта:
Volume
На текущий момент при пересоздании контейнеров проекта данные в базе будут стираться,
для этого можно воспользоваться командой docker compose rm
:
$ docker compose rm -sf
[+] Stopping 3/3
✔ Container app-back-1 Stopped 0.1s
✔ Container app-front-1 Stopped 0.2s
✔ Container app-db-1 Stopped 0.2s
Going to remove app-back-1, app-db-1, app-front-1
[+] Removing 3/0
✔ Container app-front-1 Removed 0.0s
✔ Container app-back-1 Removed 0.0s
✔ Container app-db-1 Removed 0.0s
$ docker compose up -d
[+] Running 3/3
✔ Container app-db-1 Started 0.0s
✔ Container app-front-1 Started 0.0s
✔ Container app-back-1 Started 0.0s
Добавим именованный том для хранения данных в compose.yaml
:
name: app
services:
front:
image: nginx
ports:
- 8888:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html
configs:
- source: nginx
target: /etc/nginx/conf.d/default.conf
restart: always
back:
image: back
build: .
depends_on:
- db
restart: always
secrets:
- connection_string
db:
image: postgres
environment:
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASS}"
volumes:
- db-data:/var/lib/postgresql/data
- ./users.sql:/docker-entrypoint-initdb.d/users.sql
restart: always
configs:
nginx:
file: ./default.conf
secrets:
connection_string:
environment: "DB_CONN"
volumes:
db-data:
Убедимся что данные сохранятся:
$ docker compose rm -sf
[+] Stopping 3/3
✔ Container app-back-1 Stopped 0.2s
✔ Container app-front-1 Stopped 0.2s
✔ Container app-db-1 Stopped 0.2s
Going to remove app-back-1, app-db-1, app-front-1
[+] Removing 3/0
✔ Container app-front-1 Removed 0.0s
✔ Container app-back-1 Removed 0.0s
✔ Container app-db-1 Removed 0.0s
$ fg^C
$ docker compose up -d
[+] Running 4/4
✔ Volume "app_db-data" Created 0.0s
✔ Container app-db-1 Started 0.0s
✔ Container app-front-1 Started 0.0s
✔ Container app-back-1 Started 0.0s
$ docker exec -it app-db-1 su - postgres -c "psql -U app app -c \"insert into users (name,email) values ('alex', 'alex@mail.ru');\""
INSERT 0 1
$ docker compose rm -sf
[+] Stopping 3/3
✔ Container app-back-1 Stopped 0.2s
✔ Container app-front-1 Stopped 0.2s
✔ Container app-db-1 Stopped 0.2s
Going to remove app-back-1, app-db-1, app-front-1
[+] Removing 3/0
✔ Container app-front-1 Removed 0.0s
✔ Container app-back-1 Removed 0.0s
✔ Container app-db-1 Removed 0.0s
$ docker compose up -d
[+] Running 3/3
✔ Container app-front-1 Started 0.0s
✔ Container app-db-1 Started 0.0s
✔ Container app-back-1 Started 0.0s
Network
По умолчанию созданные сервисы находятся в общей сети и контейнеры могут обращаться
друг к другу по именам сервисов. В целях безопасности изолируем сервис db
, чтобы
к нему можно было обратиться только из сервиса back
. Для этого создадим две сети
front
и db
и подключим сервисы к ним:
name: app
services:
front:
image: nginx
ports:
- 8888:80
volumes:
- ./index.html:/usr/share/nginx/html/index.html
configs:
- source: nginx
target: /etc/nginx/conf.d/default.conf
restart: always
networks:
- front
back:
image: back
build: .
depends_on:
- db
restart: always
secrets:
- connection_string
networks:
- front
- db
db:
image: postgres
environment:
POSTGRES_USER: "${DB_USER}"
POSTGRES_PASSWORD: "${DB_PASS}"
volumes:
- db-data:/var/lib/postgresql/data
- ./users.sql:/docker-entrypoint-initdb.d/users.sql
restart: always
networks:
- db
configs:
nginx:
file: ./default.conf
secrets:
connection_string:
environment: "DB_CONN"
networks:
db:
front:
volumes:
db-data:
Убедимся в этом:
$ docker exec app-front-1 curl -sS db:5432
curl: (52) Empty reply from server
$ docker compose up -d
[+] Running 5/5
✔ Network app_front Created 0.1s
✔ Network app_db Created 0.1s
✔ Container app-db-1 Started 0.2s
✔ Container app-front-1 Started 0.3s
✔ Container app-back-1 Started 0.2s
$ docker exec app-front-1 curl -sS db:5432
curl: (6) Could not resolve host: db