Лаборатория кодера

Справочник · Терминал и система

Docker: команды, Dockerfile и Compose

Образы и контейнеры, тома, журналы, хороший Dockerfile с многоэтапной сборкой и docker compose для локальной разработки. Плюс уборка места на диске.

Обновлено · 8 мин чтения

Docker упаковывает приложение вместе со всем окружением в образ, а из образа запускает изолированные контейнеры. Ниже команды и приёмы, которые нужны в обычной работе, без углубления во внутренности.

Образ и контейнер

Образ — неизменяемый шаблон: файловая система и настройки запуска. Контейнер — запущенный экземпляр образа со своим слоем для записи. Из одного образа можно запустить сколько угодно контейнеров. Удаление контейнера стирает всё, что было записано внутри, если это не лежало в томе.

Контейнеры

bash
docker run --rm -it ubuntu:24.04 bash          # временный контейнер с оболочкой
docker run -d --name web -p 8080:80 nginx:1.27 # в фоне, порт 8080 хоста -> 80 контейнера
docker ps                     # запущенные
docker ps -a                  # все, включая остановленные
docker logs -f --tail 100 web # журнал, последние 100 строк и дальше
docker exec -it web sh        # войти в работающий контейнер
docker stop web && docker rm web
docker restart web
docker inspect web            # все настройки в JSON
docker stats                  # процессор и память по контейнерам
docker cp web:/etc/nginx/nginx.conf .   # скопировать файл из контейнера
Ключ runСмысл
-dзапустить в фоне
-itинтерактивный терминал
--rmудалить контейнер после остановки
--nameсвоё имя вместо случайного
-p 8080:80опубликовать порт: хост слева, контейнер справа
-p 127.0.0.1:8080:80опубликовать только на локальном адресе
-e KEY=valueпеременная окружения
--env-file .envпеременные из файла
-v data:/var/lib/dataименованный том
-v "$PWD":/appсмонтировать папку хоста
-w /appрабочий каталог внутри
--restart unless-stoppedподнимать после перезапуска Docker и сбоев
-u 1000:1000от какого пользователя запускать процесс

docker inspect выдаёт большой JSON. Отдельное поле удобно достать шаблоном, например docker inspect -f '{{.State.Status}}' web, а весь вывод разобрать в JSON-форматтере.

Образы

bash
docker images                         # локальные образы
docker pull postgres:16               # скачать
docker build -t myapp:1.4.0 .         # собрать из Dockerfile в текущей папке
docker build -t myapp:dev -f docker/Dockerfile.dev .
docker tag myapp:1.4.0 registry.local/myapp:1.4.0
docker push registry.local/myapp:1.4.0
docker history myapp:1.4.0            # из каких слоёв состоит и сколько весит каждый
docker rmi myapp:old

Хороший Dockerfile

Пример для приложения на Python с многоэтапной сборкой:

dockerfile
# этап сборки: компиляторы и заголовки нужны только здесь
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# итоговый образ: только то, что нужно для работы
FROM python:3.12-slim
WORKDIR /app
RUN useradd --create-home --uid 1000 app
COPY --from=build /install /usr/local
COPY --chown=app:app . .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import socket; socket.create_connection(('127.0.0.1', 8000), 2)"
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]

На что обратить внимание:

  • Порядок слоёв. Docker кеширует каждый шаг. Сначала копируйте файл зависимостей и ставьте их, и только потом копируйте код. Тогда правка кода не будет заново скачивать все пакеты.
  • Многоэтапная сборка. Компиляторы, исходники и кеши остаются в первом этапе, итоговый образ получается заметно меньше.
  • Не root. Инструкция USER запускает процесс от обычного пользователя. Если приложение взломают, у атакующего будет меньше прав внутри контейнера.
  • Форма CMD. Запись в виде JSON-массива запускает процесс напрямую, и он получает сигнал остановки. В строковой форме процесс запускается через sh -c, и docker stop будет ждать 10 секунд, прежде чем убить его.
  • Один процесс на контейнер. Веб-приложение и база в разных контейнерах проще обновлять и масштабировать.

Рядом с Dockerfile положите .dockerignore, чтобы в контекст сборки не попадало лишнее:

text
.git
node_modules
__pycache__
*.log
.env

CMD и ENTRYPOINT

ИнструкцияРольКак переопределить
ENTRYPOINTпрограмма, которая всегда запускаетсяdocker run --entrypoint
CMDаргументы по умолчанию или команда целикомаргументами после имени образа

Если задан ENTRYPOINT ["python", "manage.py"] и CMD ["runserver"], то docker run app migrate выполнит python manage.py migrate.

Тома и данные

bash
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=dev postgres:16
docker volume ls
docker volume inspect pgdata
ВидЗаписьКогда использовать
именованный том-v pgdata:/dataданные баз и сервисов, Docker сам управляет местом
папка хоста-v "$PWD"/src:/app/srcразработка: правите код на хосте, контейнер видит сразу
только чтение-v "$PWD"/conf:/etc/app:roконфиги, которые контейнер не должен менять
tmpfs--tmpfs /tmpвременные файлы в памяти

Резервная копия тома через временный контейнер:

bash
docker run --rm -v pgdata:/data -v "$PWD":/backup alpine tar -czf /backup/pgdata.tar.gz -C /data .

Для баз данных надёжнее делать дамп средствами самой базы, например docker exec db pg_dump -U postgres app > app.sql, чем копировать файлы работающей базы.

docker compose

Compose описывает несколько контейнеров одним файлом compose.yaml. Это удобнее, чем помнить длинные команды run.

yaml
services:
  app:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      DB_HOST: db
      DB_PORT: "5432"
      DB_USER: app
      DB_PASSWORD: dev
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10

volumes:
  pgdata:

Сервисы одного файла видят друг друга по имени: приложение подключается к базе по адресу db:5432.

bash
docker compose up -d            # поднять всё в фоне
docker compose up -d --build    # пересобрать образы и поднять
docker compose ps
docker compose logs -f app
docker compose exec db psql -U app
docker compose run --rm app pytest   # одноразовая команда
docker compose down             # остановить и удалить контейнеры
docker compose down -v          # то же и удалить тома, то есть данные
docker compose config           # итоговый файл со всеми подстановками

Compose подставляет переменные из файла .env рядом с compose.yaml: в YAML пишут ${POSTGRES_PASSWORD}. Отступы в YAML коварны; если файл не читается, его удобно проверить конвертером YAML в JSON, который сразу покажет структуру.

Уборка места

Docker быстро съедает диск: старые образы, остановленные контейнеры, кеш сборки.

bash
docker system df                # сколько занимает каждая категория
docker container prune          # удалить остановленные контейнеры
docker image prune              # удалить образы без тегов
docker image prune -a           # удалить все образы, которые не используются контейнерами
docker builder prune            # очистить кеш сборки
docker system prune             # всё перечисленное, кроме томов
docker volume prune             # неиспользуемые тома, вместе с данными

Перед docker volume prune стоит посмотреть docker volume ls: том остановленной, но нужной базы тоже считается неиспользуемым, если контейнер удалён.

Отладка

  • Контейнер сразу завершается: docker logs имя покажет последнее, что он вывел. Код выхода виден в docker ps -a.
  • Нужно посмотреть, что внутри образа, у которого не запускается основная команда: docker run --rm -it --entrypoint sh myapp:1.4.0.
  • Приложение не открывается с хоста: проверьте, что оно слушает 0.0.0.0, а не 127.0.0.1 внутри контейнера, и что порт опубликован (docker port имя).
  • Изменения в коде не видны: образ собран раньше, пересоберите с --build или смонтируйте папку с кодом томом.