====== Контейнеры в Linux ======
===== Docker =====
[[:linux:containers:docker-deep|Расширенная инфа по докеру]]
==== Установка docker ====
:!: Установка docker
# apt install docker docker.io && systemctl enable docker
# Установка на CentOS/Alma вместе с compose
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
curl -L "https://github.com/docker/compose/releases/download/v2.24.5/docker-compose-linux-x86_64" -o /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose && docker-compose --version
# Скачать образ из репозитория:
docker pull wallarm/node
# Загрузить образ в репозиторий:
docker push example.com:5000/my_image
# Запустить свой registry можно всего одной командой:
docker run -d -p 5000:5000 --restart=always --name registry registry:2
==== Работа с контейнерами ====
:!: Команды
* **system** - системная информация
* **df** - занимаемое место на диске
* **prune** - очистить неиспользуемые данные
* **events**, **info**
* **images** - перечень локальных образов
* **ps [-a]** || **container ls** - перечень работающих [всех] контейнеров
* **top** - процессы внутри контейнера
* **logs** - логи контейнера
*
* **stop** - "лояльная" остановка (остановить все можно так: docker stop $(docker container ls -q))
* **kill** - жесткая остановка
* **rm** - удалить **контейнер**
* **rmi** - удаление **образа** (с указанием имени и версии образа)
*
* **pull** - скачивание образа из репозитория (актуальную версию, сравнивает контрольную сумму)
* **search** - поиск образов в репозитории
*
* **run** - запуск **образа** (создание контейнера)
* **-i** - интерактивный режим
* **-t** - поддержка эмулятора
* **-d** - фоновый режим (отсоединенный)
* **--name** - указание имени для создаваемого контейнера
* ** ** -
* **start** - запуск **контейнера** (уже созданного)
* **exec** - доступ к работающему **контейнеру**
* **ctrl+ q** - отсоединиться от работающего контейнера
* **attach** - присоединиться к работающему контейнеру
:!: Инструкции Dockerfile
Комментарии в файле с помощью #\\
| **FROM** | **Обязательно**, первая инструкция. Какой базовый образ нужно использовать | FROM ubuntu:16.04 |
| **RUN** | Создает новый слой и выполняет команду. Родственная с CMD и ENTRYPOINT, во всех трех принимается как shell скрипт так и json синтаксис (в квадратных скобках) | RUN apt-get install python или RUN ["mkdir", "/my_dir"] |
| **CMD** | Запускает команду с аргументами, каждый раз при запуске контейнера. Если указано несколько, то будет выполнена последняя | CMD ["openvpn"] |
| **ENTRYPOINT** | Похожа на CMD, но ее пар-ры не переопределяются а добавляются из строки запуска контейнера | ENTRYPOINT ["/sbin/apache2"] |
| **WORKDIR** | Рабочая директория для команд ENTRYPOINT,RUN,COPY,ADD (CMD ?) | WORKDIR /opt/apps |
| **COPY** | Копирует файлы и деректории из рабочей папки в контейнер, создаст все пути | COPY ./mypassw/ /root/ |
| **ADD** | Тоже самое но распакует архив либо скачает с URL, не рекоммендуется без необходимости этих ф-й | ADD https://site/video.mp4 /my_dir/ |
| **VOLUME** | Добавляет том в контейнер для хранения постоянных данных | VOLUME ["/opt/myapp"] |
| **USER** | Задает пользователя, от которого будет запущен образ | USER user:group |
| **ENV** | Задает переменные окружения в образе | ENV PGPASSWD pass |
| **ARG** | Создает переменную, которая доступна сборщику (в т.ч. в этом же файле). Фича - можно использовать для переопределения при билде образов | ARG folder=/opt/apps WORKDIR $folder |
| **EXPOSE** | Указывает какой порт нужно пробросить из контейнера. Фактически не пробрасывает ничего, это форма документации для запускающего, для маппинга нужен аргумент -p при запуске (хост:конт). Хотя вроде используется в контейнерной сети, там доступен другим контейнерам | EXPOSE 80 |
| **MAINTAINER** | Автор образа | MAINTAINER eNod |
| **LABEL** | Добавляет информацию | LABEL version="2" |
| **SHELL** | Позволяет заменить оболочку для выполнения команд на пользовательскую | SHELL ["/bin/sh", "-c"] |
| **HEALTHCHECK** | Команда которая будет проверять работоспособность контейнера | HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost ll (верт черта) exit 1 |
| **ONBUILD** | Действия, которые выполняются, если наш образ используется как базовый для другой сборки | ONBUILD ADD ./app/src |
| **STOPSIGNAL** | Переопределяет сигнал SIGTERM для завершения контейнера | STOPSIGNAL SIGINT |
Примечания:\\
т.н. **JSON** форма записи параметров, отделяется каждый аргумент, один блок в кавычках воспринимается как цельная команда, вместе с пробелами, например '["/bin/sh -c"]' будет пытаться выполнить команду "sh -c" в директории "/bin", поэтому правильно '["/bin/sh", "-c"]'\\
**CMD**:
* Может включать исполняемый файл;
* Если не содержит никакого файла, обязательно должна быть ENTRYPOINT, в таком случае обе должны быть в формате JSON;
* Аргументы при запуске docker-образа переопределяют аргументы в CMD;
* По ходу сборки не фиксирует никакого результата;
* Полезен когда нужно переопределять аргументы из строки запуска;
**ENTRYPOINT**:
* Dockerfile должен содержать либо CMD либо ENTRYPOINT;
* Нужен когда нужно запустить одну и туже команду несколько раз;
* "Используйте когда ваш контейнер выступает в роли исполняющейся программы" (?);
**Установка пакетов**:
* В Alpine образах есть какой то менеджер apk;
* Можно использовать файл "requirements.txt" с перечислением необходимых пакетов;
**WORKDIR**:
* Создаст отсутствующие директории;
* Можно использовать несколько инструкций;
* Правило хорошего тона юзать абсолютные пути;
**ARG**:
* Значения не доступны в работающем контейнере, но можно использовать как дефолт на случай если не переопределили при билде, это фича, при запуске передаем нужное значение в "--build-arg";
* пример: ARG folder=/opt/apps WORKDIR $folder; docker build .. --build-arg folder=/my_fold
:!: Еще про CMD/Entrypoint
В Entrypoint как правило используется какой либо скрипт или команда для запуска процесса внутри контейнера, а в CMD параметры, передаваемые в Entrypoint\\
Если указать команду в квадратных скобках то внутри контейнера будет выполняться лишь процесс, без каких либо оболочек, в противном случае запускается например через shell\\
:!: Примечательно что даже пустые аргументы CMD передаются в скрипт\\
(...)
ENTRYPOINT ["/work/docker-entrypoint.sh"]
CMD ["first", "", "three", ""]
## Вывод:
count args - 4
all args - first three
Минимальный пример\\
FROM centos:7
ENTRYPOINT ["/bin/ping"]
CMD ["it-lux.ru"]
При запуске будет выполнена команда "/bin/ping it-lux.ru", домен можно переопределить при запуске контейнера, например "docker run -d my-ping google.com"\\
т.е. параметр "CMD" можно переопределить в строке запуска\\
:!: Использование docker-entrypoint.sh
Скрипт для запуска содержимого контейнера обычно используется если нужен ряд каких то подготовительных процедур и в баш скрипте все это можно удобно сделать, в докер-файле, в таком случае будет запускаться именно этот скрипт\\
:!: Но важный момент в том что PID 1 в таком случае будет принадлежать именно BASH а не полезной нагрузке и при завершении контейнера, корректно завершится баш а не ваша программа. Что бы этого избежать, запускать полезную нагрузку внутри скрипта нужно параметром **"EXEC"**\\
Эта команда корректно завершит баш и запустит ваше приложение, в скрипте, тогда PID 1 будет принадлежать именно ему\\
Например:
Dockerfile:
FROM centos:7
COPY docker-entrypoint.sh /usr/bin
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
docker-entrypoint.sh:
#!/bin/bash
exec ping ya.ru
==== Примеры ====
:!: Работа с контейнерами
docker run -d [image-name]
docker run -d -p 80:80 [image-name]
# Подключится и запустить доп процесс
docker exec -it [id] bash
# Возобновить (уже созданный, остановленный) контейнер
docker start [id]
# Подключится к работающему контейнеру (после этого сложно отключится, вроде как ctrl+p ctrl+q но нифига не рабоатет, т.ч. лучше запускать шелл)
docker attach [id]
# Работающие контейнеры
docker ps [-a]
docker image [list prune (-a) inspect]
docker container [list prune (-a) inspect]
# Процессы
docker top
# Ресурсы
docker stats
# Порты
docker port
# Проброс папок при запуске
docker run -it -v /AbsoluteExistPath:/AbsDestFold NameImage
# Создание образа.
# Параметры создания указываются в файле "Dockerfile", он может находится где угодно, при сборке указывается путь к нему, внутри него указывается исходный образ
docker build -t MyNewImage /PathToDockerfile
# С удалением промежуточных образов
docker build --rm -t local/myImage ./[Dockerfile]
# docker-compose (apt install docker-compose)
# Создаем нужный файл yml, в директории и из этой же директории запускаем проект
docker-compose up [-d для фонового запуска]
# Тегирование и пуш в registry
docker tag local_image:latest REGISTRY_URL:RELEASE-BUILD_VERSION
docker push REGISTRY_URL:RELEASE-BUILD_VERSION
Еще команды. Работа с образами и контейнерами
# -q возвращает только ID объектов
docker rmi $(docker images -q)
docker rm $(docker ps --filter status=exited -q)
# Остановить все действующие контейнеры
docker stop $(docker ps -a -q)
docker stop $(docker ps -q --filter name=^web)
# Принудительно удалить образы
docker rmi -f <имя или ID>
Экспорт/Импорт вольюма:
# *container-name - имя контейнера к которому привязан вольюм;
# *volume-path - путь монтирования вольюма внутри контейнера;
# После команды, docker запустит контейнер и создаст архив с содержимым
docker run --rm --volumes-from container-name -v $(pwd):/backup ubuntu bash -c "cd /volume-path && tar zcf /backup/backup.tar.gz ."
# Для восстановления
docker run --rm --volumes-from container-name -v $(pwd):/backup ubuntu bash -c "cd /volume-path && tar zxf /backup/backup.tar.gz"
:!: sha256 Образа
Появляется похоже только после загрузки в registry
# Почему то не во всех версиях работает
docker images --format '{{.Digest}}' $IMAGE
# В крайнем случае такой вариант, но кажется менее надежным
docker image inspect --format '{{ .RepoDigests }}' $IMAGE | grep -Po "@\\Ksha256:[^\\s\\]]*" | head -n1
:!: Пример Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
EXPOSE 3000
FROM eclipse-temurin:17-jdk-alpine as builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install
FROM eclipse-temurin:17-jre-alpine
WORKDIR /opt/app
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
:!: Пользователь и привилегии
* По умолчанию образ работает под рутом и все скопированные файлы принадлежат ему\\
* Для использования директивы USER, пользователя нужно предварительно созавать\\
* Зависит от настроек ОС, но до указания директивы USER, команды выполняются под рутом, с-но сначала назначаем права на директорию, затем указываем "USER", но в некоторых случаях приходится явно указать сначала "USER root", назначить права и только потом "USER my-user"\\
Пример создания юзера и запуск под ним
FROM openjdk:17
WORKDIR /app
COPY dir1 /app/dir1
COPY file1 /app/
ENV UID=10001
ENV GID=20001
ENV USR="usr-app"
RUN groupadd -g ${GID} ${USR} && \
useradd -u ${UID} -g ${GID} ${USR} && \
chown -R ${USR} /app
USER ${USR}
ENTRYPOINT ["/bin/sleep", "infinity"]
:!: Push в registry
Сбилженный образ выглядит так:
| REPOSITORY | TAG |
| localhost/myapp-120-40 | latest |
Присваиваем тег с указанием адреса registry
podman tag myapp-120-40:latest registry.ru/dev/my_project/myapp:myapp-120-40
Создается копия образа но уже с другим названием
| REPOSITORY | TAG |
| localhost/myapp-120-40 | latest |
| registry.ru/dev/my_project/myapp | myapp-120-40 |
Далее просто Push с указанием образа, адрес в нем уже прописан
podman push registry.ru/dev/my_project/myapp:myapp-120-40
==== Собственные образы ====
В PATH передается расположение файлов для сборки, не стоит передавать большое т.к. все содержимое передается демону докера т.к. он занимается сборкой, можно делать файл "игнора".\\
Сам образ создается в репозитории, в опциях можно указывать параметры для репозитория.\\
* **-f** - путь к файлу Dockerfile
* **-t** - опции, можно указать имя создаваемого образа
:!: Пример: цикличный вывод из контейнеров в один общий файл
PS При указании пути, тильда не работает\\
read.sh:
#!/bin/bash
while true
do
echo "--==" $(uname -a) "==--" >> /outdir/output.txt
sleep 2
done
Dockerfile:
FROM debian
MAINTAINER im
RUN apt-get update && apt-get install procps -y
COPY read.sh /scrptdir/read.sh
CMD ["/scrpt/read.sh"]
# Сборка
docker build -t imecho .
# Запуск
docker run -dit --name contecho -v /home/dn/imread/outdir:/outdir imecho
:!: Докер файл с java приложением
Сборка java уже была
FROM openjdk:17
WORKDIR /app
COPY ./build/libs/*.jar ./my_app.jar
EXPOSE 8080/tcp
CMD ["java", "-Dspring.profiles.active=stage", "-Duser.timezone=GMT", "-jar", "my_app.jar"]
Сборку делаем здесь же
FROM gradle:7-jdk17 as build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/home/gradle/.gradle . && \
gradle wrapper && \
./gradlew compileJava && \
./gradlew test && \
./gradlew bootJar
FROM openjdk:17
WORKDIR /app
COPY ./build/libs/*.jar ./my_app.jar
EXPOSE 8080/tcp
CMD ["java", "-Dspring.profiles.active=stage", "-Duser.timezone=GMT", "-jar", "my_app.jar"]
==== Использование ====
Изменения сохраняются в контейнере, устанавливаемый софт например.\\
Создание с параметром **-it** (интерактивный с эмулятором) создает контейнер сразу с запущенным КИ, в противном случае без него и с-но останавливается сразу после запуска.\\
При повторном создании образа (через Dockerfile), образ пересоздается, типа следующая версия (latest), старый остается.\\
===== Docker-compose =====
Оркестрация несколькими контейнерами\\
docker-compose up -d
docker-compose down
docker-compose -f filename -f filename2 up -d
docker-compose -f filename -f filename2 down
:!: Info
Сеть создается автоматически, общая для всех контейнеров, контейнеры доступны друг другу по имени хоста\\
Параметры можно менять/задавать, можно подключаться к уже созданной сети, разве что для этого и имеет смысл специально указывать сеть в компоуз-файле\\
:!: Пример приложения и БД postgres
Приложение загружается с собственного registry, перед этим нужно выполнить "docker login"\\
Контейнеры объединяются в общую сеть\\
Создается постоянное хранилище, используемое базой данных\\
version: '3.7'
networks:
my_app_net:
name: my_net
volumes:
pgdata:
name: db_pgdata
driver_opts:
type: None
device: /opt/pgdata
o: bind
services:
my_appication:
image: registry.myapp.io/myapp/
container_name: my_app
restart: unless-stopped
expose:
- "8080"
environment:
- "DATABASE_URL=jdbc:postgresql://postgres:5432/"
- "DATABASE_USER="
- "DATABASE_PASSWORD="
networks:
- my_app_net
depends_on:
- postgres
postgres:
image: postgres:14
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
container_name: postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
- "POSTGRES_PASSWORD="
networks:
- my_app_net
volumes:
- pgdata:/var/lib/postgresql/data
- /opt//postgres/postgresql.conf:/etc/postgresql/postgresql.conf
- /opt//postgres/pg_hba.conf:/etc/postgresql/pg_hba.conf
- /opt//postgres/initdb_stage:/docker-entrypoint-initdb.d
:!: Kafka и Zookeeper
version: '3.7'
services:
zoo1:
image: zookeeper
hostname: zoo1
container_name: zoo1
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_SERVERS: zoo1:2888:3888
kafka1:
image: confluentinc/cp-kafka:latest
hostname: kafka1
container_name: kafka1
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
# KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
# KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
# KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
# KAFKA_JMX_PORT: 9999
# KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
# KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authrizer.AclAuthorizer
# KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
depends_on:
- zoo1
* KAFKA_ADVERTISED_LISTENERS - адреса для клиентов, в т.ч. для межброкерного взаимодействия
* режим для межброкерного взаимодействия задается в "KAFKA_INTER_BROKER_LISTENER_NAME", но для этого в вышестоящем пар-ре должен быть прописан этот режим (internal/external)
:!: Еще пример с кафкой
version: "2"
services:
zookeeper:
image: docker.io/bitnami/zookeeper:3.9
ports:
- "2181:2181"
volumes:
- "zookeeper_data:/bitnami"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka0:
image: docker.io/bitnami/kafka:3.4
ports:
- "9094:9094"
volumes:
- "kafka_data0:/bitnami"
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_CFG_BROKER_ID=0
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka0:9092,EXTERNAL://localhost:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
depends_on:
- zookeeper
kafka1:
image: docker.io/bitnami/kafka:3.4
ports:
- "9096:9096"
volumes:
- "kafka_data1:/bitnami"
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- KAFKA_CFG_BROKER_ID=1
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9095,CONTROLLER://:9097,EXTERNAL://:9096
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka1:9095,EXTERNAL://localhost:9096
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT
depends_on:
- zookeeper
volumes:
zookeeper_data:
driver: local
kafka_data0:
driver: local
kafka_data1:
driver: local
version: "2"
services:
zookeeper:
image: docker.io/bitnami/zookeeper:3.9
ports:
- "2181:2181"
volumes:
- "zookeeper_data:/bitnami"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: docker.io/bitnami/kafka:3.4
ports:
- "9092:9092"
volumes:
- "kafka_data:/bitnami"
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
depends_on:
- zookeeper
volumes:
zookeeper_data:
driver: local
kafka_data:
driver: local
:!: