====== Docker ======
===== Обновление =====
# pull latest images
docker-compose pull
# restart containers
docker-compose up -d --remove-orphans
# remove obsolete images
docker image prune
https://stackoverflow.com/questions/49316462/how-to-update-existing-images-with-docker-compose
===== Чистка =====
### Docker cleaning
0 3 * * 2,4,6 root docker container prune -f; docker image prune -af
0 3 * * 7 root docker system prune -f
https://docs.docker.com/config/pruning/
===== Тома для постоянного хранения =====
==== sshfs ====
[[https://github.com/vieux/docker-volume-sshfs|sshfs]] монтирует каталог с другого сервера через SSH.
### На сервере хранения:
# Создать каталог для тома (он должен быть, автоматически не создаётся):
mkdir -p /home/user/dockervol/bepasty
# Создать файл dump (иначе volume не подмонтируется, будет ошибка lchown permission denied, это проблема sshfs)
touch /home/user/dockervol/bepasty/dump
### На сервере докера:
# Установка дополнения sshfs
docker plugin install --grant-all-permissions vieux/sshfs
https://github.com/vieux/docker-volume-sshfs/issues/76
Пример с паролем, лучше через переменные или через ключи.
version: '3.9'
services:
bepasty:
build: .
container_name: bepasty
restart: unless-stopped
volumes:
- bepasty:/storage
ports:
- 80:5000
volumes:
bepasty:
name: "bepasty"
driver: "vieux/sshfs:latest"
driver_opts:
sshcmd: "user@server1:/home/user/dockervol/bepasty"
password: "P@ssw0rd"
https://docs.docker.com/storage/volumes/#share-data-between-machines
==== Резервное копирование тома ====
С помощью другого контейнера, который подключается к томам основного.
# Запуск
docker run -d -v /dbdata --name dbstore alpine sleep infinity
# Создание файла
docker exec -it dbstore sh -c "echo 'Hello' >> /dbdata/file.txt"
# Проверка
docker exec -it dbstore sh -c "cat /dbdata/file.txt"
# Бэкап
docker run --rm --volumes-from dbstore -v $(pwd):/backup alpine tar cvf /backup/backup.tar /dbdata
# Запуск второго контейнера
docker run -d -v /dbdata --name dbstore2 alpine sleep infinity
# Восстановление из архива
docker run --rm --volumes-from dbstore2 -v $(pwd):/backup alpine sh -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"
# Проверка
docker exec -it dbstore2 sh -c "cat /dbdata/file.txt"
https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes
===== Мониторинг =====
==== Healthcheck ====
Можно делать в Dockerfile, можно в docker-compose.yml
Варианты строки проверки
curl --fail http://localhost:8080/health
# иногда curl-а нет в образе, тогда
wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
https://docs.docker.com/engine/reference/builder/#healthcheck\\
https://docs.docker.com/compose/compose-file/#healthcheck\\
https://medium.com/geekculture/how-to-successfully-implement-a-healthcheck-in-docker-compose-efced60bc08e
==== Prometheus ====
/etc/prometheus/prometheus.yml
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=` to any timeseries scraped from this config.
- job_name: 'prometheus'
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ['localhost:9090']
- job_name: 'docker'
static_configs:
- targets: ['172.17.0.1:9323']
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
**172.17.0.1 = host.docker.internal**\\
If you're running with ''--net=host'', ''localhost'' should work fine.\\
If you're using default networking, use the static IP ''172.17.0.1''. I suspect neither will behave quite the same as those domains.\\
https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal
==== Dozzle ====
# На удалённом хосте запустить докер-прокси, чтобы можно было подключиться удалённо
docker container run --rm --name dockerproxy -e CONTAINERS=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
# Запустить Dozzle (будет мониториться как локальный сервер через сокет, так и удалённый через прокси)
docker run --rm --name dozzle -p 9999:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest --base /logs --remote-host tcp://192.168.1.8:2375
https://github.com/amir20/dozzle\\
https://dozzle.dev/guide/remote-hosts\\
https://github.com/Tecnativa/docker-socket-proxy
Докер-прокси:
version: '3.7'
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
container_name: docker-proxy
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
CONTAINERS: "1"
ports:
- 2375:2375
Dozzle (на другом сервере, локальный Докер не мониторится)
version: '3.7'
services:
dozzle:
image: amir20/dozzle
container_name: dozzle
restart: unless-stopped
environment:
DOZZLE_BASE: "/dockerlogs"
DOZZLE_REMOTE_HOST: "tcp://server1:2375,tcp://server2:2375"
ports:
- 8080:8080
===== Деплой =====
==== Blue-green ====
#!/bin/bash
replicas=2
function deploybg {
docker-compose rm -sf $2
docker-compose pull $2
docker-compose up -d --scale $2=$replicas --force-recreate $2
until [[ ${#count[@]} = $replicas ]]; do
count=( $(docker ps -qf name=$2 -f health=healthy) )
sleep 10
done
docker-compose rm -sf $1
}
if [[ $(docker ps -qf name=backend-blue) ]]
then
deploybg backend-blue backend-green
else
deploybg backend-green backend-blue
fi
В Gitlab (1 реплика и удалённый контекст, т. к. непонятно, как экранировать #):
script:
- docker context create remote --docker "host=ssh://${USER}@${HOST}"
- docker context use remote
- |
function deploybg {
docker-compose --context remote rm -sf $2
docker-compose --context remote pull $2
docker-compose --context remote up -d --force-recreate $2
until [[ $(docker ps -qf name=$2 -f health=healthy) ]]
do
sleep 10
done
docker-compose --context remote rm -sf $1
}
if [[ $(docker ps -qf name=backend-blue) ]]
then
deploybg backend-blue backend-green
else
deploybg backend-green backend-blue
fi
===== Хранение данных =====
[[https://gist.github.com/scyto/f4624361c4e8c3be2aad9b3f0073c7f9|My Docker Swarm Architecture]] (GlusterFS)
===== Решение проблем =====
==== Cannot start service : get : no such volume ====
Starting bepasty ... error
ERROR: for bepasty Cannot start service bepasty: get 441a8f149da5cbd4fe25591303b0d7885f085729819c5b8aef52dd22b25b1072: no such volume
ERROR: for bepasty Cannot start service bepasty: get 441a8f149da5cbd4fe25591303b0d7885f085729819c5b8aef52dd22b25b1072: no such volume
ERROR: Encountered errors while bringing up the project.
Решение:
docker container prune
docker-compose up -d
==== cannot stop container: permission denied ====
Невозможно остановить/удалить/пересоздать контейнеры, ошибки такие:
Error response from daemon: cannot stop container: c2a63f33d8b5: permission denied
Error response from daemon: Cannot kill container: c2a63f33d8b5: permission denied
Перезапуск ОС/Докера ничего не даёт
Решение:
It turned out that AppArmor service was messing up with Docker. AppArmor (or “Application Armor”) is a Linux kernel security module that allows the system administrator to restrict programs’ capabilities with per-program profiles. For this problem with containers, it helped me to remove the unknown from AppArmor using the following command:
sudo aa-remove-unknown
After that, I was able to stop and kill my containers.
https://debugah.com/solved-cannot-kill-docker-container-permission-denied-23902/
==== Запустить команду с конвейером (pipe) в контейнере с хоста ====
# Use "docker exec" to change user password in container from host.
docker exec ops_ubuntu_ntp2 echo "cobra_demo:cobra_pw"|chpasswd
# Fail to user chpasswd to change password of user cobra_demo in container.
# This is because the Pipe (|).
# The command chpasswd is executed in host not in container.
# However, host accept the output from docker container as it parameter.
docker exec ops_ubuntu_ntp2 echo "cobra_demo:cobra_pw"
#try another way, use "echo" to pass parameter to "docker exec"
echo "cobra_demo:cobra_pw"|chpasswd | docker exec ops_ubuntu_ntp2 /bin/bash/
# I tried many solution and found the following command works
# The -i parameter is must.
echo "echo cobra_demo:cobra_pw|chpasswd" | docker exec ops_ubuntu_ntp2 /bin/bash
# Paramater -i for docker exec. I don't know why, but it works.
echo "echo cobra_demo:cobra_pw|chpasswd" | docker exec -i ops_ubuntu_ntp2 /bin/bash
https://www.youtube.com/watch?v=6fEmryhIbr4 (текст в описании)
==== Блокировка dockerhub.io в России ====
С 30 мая 2024 г.
1. Конфиг докера (как зеркало docker.io)
^операционная система ^путь к файлу конфигурации ^
|Linux, regular setup |''/etc/docker/daemon.json'' |
|Linux, rootless mode |''~/.config/docker/daemon.json'' |
|Windows |''C:\ProgramData\docker\config\daemon.json'' |
cat << EOF |sudo tee -a /etc/docker/daemon.json
{
"registry-mirrors": [
"https://mirror.gcr.io",
"https://dockerhub.timeweb.cloud",
"https://daocloud.io",
"https://c.163.com",
"https://huecker.io",
"https://registry.docker-cn.com"
]
}
EOF
Теперь, при попытке загрузки образа, докер будет сначала пытаться использовать прокси.
2. Явное указание адреса
docker pull mirror.gcr.io/library/alpine:latest
https://huecker.io/\\
https://habr.com/ru/news/818177/comments/
==== docker.errors.DockerException: Error while fetching server API version ====
На новых системах после установки docker-compose и попыткой им воспользоваться выходит ошибка\\
docker.errors.DockerException: Error while fetching server API version: HTTPConnection.request() got an unexpected keyword argument ‘chunked’
Решение: снести старый docker-compose и установить docker-compose-v2.
sudo apt remove docker-compose -y
sudo apt install docker-compose-v2 -y
После этого команды выглядят так:
# без дефиса
docker compose up -d
docker compose ps
# и т. д.
===== Полезное =====
==== Запуск контейнера не от root ====
- [[https://americanexpress.io/do-not-run-dockerized-applications-as-root/|Don’t run Docker container processes as root]]. Santa won’t bring you any gifts if you use root.
- Use [[https://github.com/ncopa/su-exec#why-reinvent-gosu|su-exec]], [[https://github.com/tianon/gosu|gosu]], chroot, or [[https://manpages.debian.org/buster/util-linux/setpriv.1.en.html|setpriv]] to step down from root into another user inside your Docker containers.
- You can use Supervisor to manage processes in Docker containers and step down from root. But unless you have [[https://advancedweb.hu/supervisor-with-docker-lessons-learned/|a very specific use case to do so]], you shouldn’t because it’s at least 50MB heavier.
su-exec is a new-ish alternative to gosu. Both provide similar functionality, but su-exec weights only 10kb instead of 1.8MB.
This shows how to:
- Install php7-fpm and su-exec on an Alpine 3.11 Docker image;
- Step down from user root into user nobody using su-exec, and;
- Execute php-fpm using su-exec:
FROM alpine:3.11
RUN apk add --no-cache php7-fpm su-exec
ENTRYPOINT ["su-exec", "nobody", "/usr/sbin/php-fpm7", "--nodaemonize", "--force-stderr"]
==== Docker Swarm - запуск команды в контейнере сервиса ====
Чтобы не искать имя контейнера в сервисе. Надо выбирать имя, которое однозначно указывает на сервис, а то можно получить не тот результат, если запрос совпадает с несколькими сервисами.
docker exec $(docker ps -q -f name='lk_frontend_nginx' |head -n 1) ls
==== Права ====
В контейнере PHP и Nginx есть юзер www-data (ID 33), которого можно использовать вместо рута при совместной работе.
FROM composer AS vendor
WORKDIR /app
COPY composer.json .
RUN composer install --ignore-platform-reqs --no-dev --no-scripts
FROM php:8.0-fpm
COPY --chown=33:33 --from=vendor /app/vendor ./vendor
RUN \
apt update && apt install -y --no-install-recommends \
libzip-dev \
libpng-dev \
libxml2-dev && \
docker-php-ext-install \
pdo_mysql \
exif \
pcntl \
bcmath \
gd \
soap \
zip \
sockets
USER 33
То же самое, только размер в 5 раз меньше
FROM composer AS vendor
WORKDIR /app
COPY composer.json .
RUN composer install --ignore-platform-reqs --no-ansi --no-dev --no-interaction --no-progress --no-scripts --optimize-autoloader
FROM alpine:3.16
WORKDIR /var/www/html
RUN apk add --no-cache \
php8 \
php8-fpm \
php8-mbstring \
php8-pdo_mysql \
php8-exif \
php8-pcntl \
php8-bcmath \
php8-gd \
php8-soap \
php8-zip \
php8-sockets \
php8-session \
php8-fileinfo && \
addgroup -g 82 -S www-data || true && \
adduser -u 82 -D -S -G www-data www-data || true && \
sed -i '/listen = /c listen = 0.0.0.0:9000' /etc/php8/php-fpm.d/www.conf && \
chown -R 82: /var/log/php*
#sed -i '/user =/c user = www-data' /etc/php8/php-fpm.d/www.conf && \
#sed -i '/group =/c group = www-data' /etc/php8/php-fpm.d/www.conf && \
#sed -i '/php_admin_value\[memory_limit\] = /c php_admin_value[memory_limit] = 512M' /etc/php8/php-fpm.d/www.conf && \
#sed -i '/memory_limit = /c memory_limit = 512M' /etc/php8/php.ini && \
#sed -i 's/;clear_env/clear_env/' /etc/php8/php-fpm.d/www.conf && \
#sed -i 's/^;env/env/' /etc/php8/php-fpm.d/www.conf
COPY --chown=82:82 --from=vendor /app/vendor ./vendor
COPY --chown=82:82 . .
EXPOSE 9000
#websockets
#EXPOSE 6001
USER 82
#RUN php artisan key:generate
CMD ["php-fpm8", "-F"]
https://hub.docker.com/_/composer\\
https://hub.docker.com/_/php\\
http://www.inanzzz.com/index.php/post/k4r7/using-docker-multi-stage-builds-to-run-composer-install-and-copy-application-into-php-container-image\\
https://dev.to/mtk3d/read-this-before-you-start-using-the-multistage-builds-for-your-docker-images-21e7\\
https://laravel-news.com/multi-stage-docker-builds-for-laravel\\
https://nsirap.com/posts/039-docker-best-practice-multi-stage-build/\\
В стандартных томах владелец меняется на папке _data, т. е.
docker run --rm -it -v alpinetest:/alpinetest alpine:3.16 sh
chown -R 82: /alpinetest
# На хосте:
ls -l /var/lib/docker/volumes
drwx-----x 3 root root 4.0K Jun 13 11:07 alpinetest
ls -l /var/lib/docker/volumes/alpinetest
drwxr-xr-x 2 82 82 4.0K Jun 13 11:09 _data
==== nginx ====
Шаблон конфигурации - берёт переменные окружения и поставляет их в шаблон, а потом пишет как файл конфигурации, убирая ''.template'' из имени файла.\\
Стандартный путь к шаблонам: ''/etc/nginx/templates/''\\
Стандартный путь к конфигурациям: ''/etc/nginx/conf.d/''
server {
listen 80;
index index.php index.html;
root ${NGINX_ROOT};
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass ${NGINX_FASTCGI_PASS}:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
}
FROM nginx:alpine
COPY default.conf.template /etc/nginx/templates/
version: "3.7"
service:
nginx:
build: ./nginx
container_name: nginx
environment:
NGINX_ROOT: /var/www/html/public
NGINX_FASTCGI_PASS: app
volumes_from:
- app
ports:
- 80:80
https://github.com/docker-library/docs/tree/master/nginx#using-environment-variables-in-nginx-configuration\\
==== PHP ====
В alpine:3.18 - версии 8.1 и 8.2\\
В alpine:3.17 - версия 8.1\\
В alpine:3.16 - версии 8 и 8.1 (по умолчанию ''apk add php'' ставится 8)
[[https://dev.to/mtk3d/how-to-configure-php-logs-for-docker-2384|How to configure PHP logs for Docker]]
[[https://github.com/renatomefi/php-fpm-healthcheck|php-fpm healthcheck]]
==== Laravel ====
Пример проекта на Laravel - Pixelfed: https://docs.pixelfed.org/running-pixelfed/installation/
[[https://laravel-news.com/laravel-scheduler-queue-docker|Running the Laravel Scheduler and Queue with Docker ]]\\
[[https://stackoverflow.com/questions/27674597/laravel-daily-log-created-with-wrong-permissions|Laravel daily log created with wrong permissions]]\\
[[https://www.8host.com/blog/zapusk-prilozheniya-laravel-cherez-docker-compose/|Запуск приложения Laravel через Docker Compose]]\\
[[https://www.digitalocean.com/community/tutorials/how-to-containerize-a-laravel-application-for-development-with-docker-compose-on-ubuntu-18-04-ru|Контейнеризация приложения Laravel 6 для разработки с помощью Docker Compose в Ubuntu 18.04]]
==== nodejs ====
Сборка в каталоге, лежащем на хосте
docker run --rm -w /home/node -v ./source:/home/node node:16 npm run build
Сложный и неэффективный вариант (пока не понял, что можно проще).\\
Интересный приём - heredoc для ''docker build''.
workdir=/home/node
imagename=front:temp
docker build -t $imagename -f - . << EOF
FROM node:16
WORKDIR $workdir
COPY --chown=node:node source/ ./
USER node
# npm rebuild нужен для решения проблемы с правами cross-env: Permission denied
RUN npm rebuild && npm run build
EOF
# Вытащить каталоги client и admin из образа с архивацией gzip
id=$(docker create $imagename)
for i in client admin
do
docker cp $id:$workdir/$i - |gzip > result/$i.tar.gz
done
docker rm $id
==== Скачивание и пересылка образа в закрытый контур ====
В закрытом контуре интернета нет, поэтому на какой-то внешней машине:
docker pull tomcat:jre11
docker save tomcat:jre11 |gzip > tomcat_jre11.tar.gz
# Куски по 24 МБ, цифровые индексы, в конец добавить .tar.gz, входной файл, имя (префикс) выходных файлов
split -b 24M -d --additional-suffix=.tar.gz tomcat_jre11.tar.gz tomcat_jre11_
ll -h
-rw-rw-r-- 1 user user 24M May 21 11:53 tomcat_jre11_00.tar.gz
-rw-rw-r-- 1 user user 24M May 21 11:53 tomcat_jre11_01.tar.gz
-rw-rw-r-- 1 user user 24M May 21 11:53 tomcat_jre11_02.tar.gz
-rw-rw-r-- 1 user user 24M May 21 11:53 tomcat_jre11_03.tar.gz
-rw-rw-r-- 1 user user 1.5M May 21 11:53 tomcat_jre11_04.tar.gz
-rw-rw-r-- 1 user user 98M May 21 11:44 tomcat_jre11.tar.gz
Дальше вытаскивать архивы наружу и слать по почте вложением или ещё как-нибудь.\\
Выгрузка образов: https://docs.docker.com/reference/cli/docker/image/save/
На целевом сервере: объединение частей и загрузка образа.
cat tomcat_jre11_* > tomcat_jre11.tar.gz
zcat tomcat_jre11.tar.gz |docker load
==== Сохранение всех образов в архивы ====
backupdir=/tmp/docker_images_backup
mkdir $backupdir
# Кроме образов с тэгами, начинающимися на host:port/, т. е., тэгированных для частных репозиториев
images=($(docker image ls | awk '!/REPOSITORY|/ {print $1":"$2}' |egrep -v ^.*:[[:digit:]]*/))
# Замена / на _ в именах архивов
for i in "${images[@]}" ; do docker save $i |gzip > $backupdir/${i//\//_}.tar.gz ; done
user@k3:~/dockerimages$ ll -h
total 3.6G
drwxrwxr-x 2 user user 4.0K May 31 14:40 ./
drwxr-x--- 23 user user 4.0K May 31 14:26 ../
-rw-rw-r-- 1 user user 11M May 31 14:37 epoupon_fileshelter:latest.tar.gz
-rw-rw-r-- 1 user user 19M May 31 14:37 f0rc3_gokapi:latest.tar.gz
-rw-rw-r-- 1 user user 636M May 31 14:36 frooodle_s-pdf:latest.tar.gz
-rw-rw-r-- 1 user user 330M May 31 14:39 gradle:7.3.3-jdk11-alpine.tar.gz
-rw-rw-r-- 1 user user 286M May 31 14:41 maven:3.5.2.tar.gz
-rw-rw-r-- 1 user user 53M May 31 14:35 minio_minio:latest.tar.gz
-rw-rw-r-- 1 user user 143M May 31 14:34 mirror.gcr.io_library_postgres:latest.tar.gz
-rw-rw-r-- 1 user user 18M May 31 14:37 mtlynch_picoshare:latest.tar.gz
-rw-rw-r-- 1 user user 324M May 31 14:33 nexus_nexus:latest.tar.gz
-rw-rw-r-- 1 user user 487M May 31 14:40 nikosch86_onionshare:latest.tar.gz
-rw-rw-r-- 1 user user 316M May 31 14:38 openjdk:11.tar.gz
-rw-rw-r-- 1 user user 182M May 31 14:39 openjdk:17-alpine.tar.gz
-rw-rw-r-- 1 user user 129M May 31 14:37 postgres:15.0.tar.gz
-rw-rw-r-- 1 user user 75M May 31 14:36 psitrax_psitransfer:latest.tar.gz
-rw-rw-r-- 1 user user 324M May 31 14:34 sonatype_nexus3:latest.tar.gz
-rw-rw-r-- 1 user user 137M May 31 14:37 stonith404_pingvin-share:latest.tar.gz
-rw-rw-r-- 1 user user 98M May 31 14:35 tomcat:9-jre11.tar.gz
-rw-rw-r-- 1 user user 98M May 31 14:34 tomcat:jre11.tar.gz
==== Загрузка всех образов из архивов ====
for i in $(ls *.tar.gz); do zcat $i |docker load; done
==== Убрать повторяющиеся куски кода из docker-compose.yml ====
Называется "compose file extensions". Пишется код, ему присваивается якорь, а потом на него идёт ссылка.\\
''version:'' должна быть ''3.4'' и новее. Механизм негибкий, дочернюю строку в алиас не добавишь.
version: '3.7'
x-fluentd: &fluentd
driver: fluentd
options: &fluentdopts
fluentd-async-connect: 1
services:
nodeinfo:
image: nodeinfo
logging:
<<: *fluentd
options:
<<: *fluentdopts
tag: "nodeinfo"
https://docs.docker.com/compose/compose-file/11-extension/\\
https://stackoverflow.com/questions/39731125/can-i-concatenate-aliases-in-yaml
==== Сделать сеть с другим именем, не прописывая её явно для всех сервисов ====
Все сервисы стандартно ссылаются на сеть default, а тут ей задано другое имя. В результате будет сеть elastic, но прописывать её в сервисы не надо.
networks:
default:
name: elastic
external: false
https://github.com/elkninja/elastic-stack-docker-part-one/blob/main/docker-compose.yml