Глиф Учебники

Урок 6. Свой образ: Dockerfile и docker build

Курс «Docker» · урок 6 из 10 · 5 минут чтения

Готовых образов много, но свою программу придётся упаковать самому. Рецепт образа записывают в файл Dockerfile: с какого образа начать, какие файлы положить, что установить и какую команду запустить. По рецепту команда docker build собирает образ.

Самый простой Dockerfile

Даже тестовый образ hello-world собран по Dockerfile — вот он целиком:

Dockerfile
FROM scratch
COPY hello /
CMD ["/hello"]

FROM scratch — начать с пустого образа, COPY — положить в корень готовую программу hello, CMD — запускать её. Обычно образы начинают не с пустоты, а с готовой основы.

Программа для примера

Создайте папку проекта и в ней файл app.py — маленький веб-сервер на Python без сторонних библиотек:

Python
from http.server import BaseHTTPRequestHandler, HTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = "Привет из контейнера!\n".encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


print("Сервер запущен на порту 8000")
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

Рядом — файл Dockerfile (именно так, с большой буквы и без расширения):

Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY app.py .
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
CMD ["python", "app.py"]

Инструкции по строкам

ИнструкцияЧто делает
FROM python:3.13-slimоснова — официальный образ с Python 3.13; slim — урезанный вариант без лишних программ
WORKDIR /appрабочая папка внутри образа; создаётся, если её нет. Дальнейшие команды и относительные пути — от неё
COPY app.py .скопировать файл из папки проекта в рабочую папку образа (точка — это /app)
ENV PYTHONUNBUFFERED=1переменная окружения; эта заставляет Python сразу выводить текст, иначе docker logs покажет его с задержкой
EXPOSE 8000отметка, что программа слушает порт 8000. Порт она не публикует — это делает -p при запуске
CMD ["python", "app.py"]команда по умолчанию при запуске контейнера

Ещё две инструкции нужны почти в каждом Dockerfile:

Не путайте RUN и CMD. RUN выполняется один раз, когда собирается образ. CMD ничего не делает при сборке — он лишь записывает, что запускать, когда из образа создают контейнер. Чем CMD отличается от ENTRYPOINT — в справке Чем отличаются CMD и ENTRYPOINT.

Сборка

В папке проекта выполните:

Bash
docker build -t hello-web:1.0 .
Папка проекта контекст сборки Dockerfile app.py docker build -t hello-web:1.0 . RUN и COPY — при сборке образ hello-web:1.0 COPY app.py . WORKDIR /app FROM python:3.13-slim CMD ["python", "app.py"] docker run -d --name hello -p 8000:8000 hello-web:1.0 CMD — при запуске контейнера контейнер hello python app.py работает
Команда docker build собирает образ из папки проекта по Dockerfile, а docker run создаёт из образа контейнер и запускает команду CMD.

Docker по очереди выполнит инструкции и в конце напишет, что образ готов. Проверьте docker images — там появится hello-web с тегом 1.0. Запуск — как у любого образа:

Bash
docker run -d --name hello -p 8000:8000 hello-web:1.0
Терминал
$ curl http://localhost:8000
Привет из контейнера!

docker logs hello покажет строку Сервер запущен на порту 8000 и запросы к серверу.

Почему 0.0.0.0, а не 127.0.0.1

Самая частая ошибка своих образов: программа слушает 127.0.0.1 (или localhost). Внутри контейнера это адрес самого контейнера, а подключения через -p приходят на его сетевой адрес. В итоге контейнер работает, порт опубликован, а браузер получает обрыв соединения. Программа в контейнере должна слушать 0.0.0.0 — все адреса. У веб-фреймворков (готовых основ для сайтов и веб-приложений) это обычно отдельный параметр, например --host 0.0.0.0.

программа слушает 127.0.0.1 браузер localhost:8000 контейнер сетевой адрес 127.0.0.1 app.py порт 8000 -p 8000:8000 запрос пришёл на сетевой адрес, а там программа не слушает — обрыв соединения программа слушает 0.0.0.0 браузер localhost:8000 контейнер сетевой адрес 127.0.0.1 app.py порт 8000 -p 8000:8000 0.0.0.0 — все адреса контейнера: запрос доходит до программы
Подключение через -p приходит на сетевой адрес контейнера: программа, слушающая только 127.0.0.1, его не получит, а слушающая 0.0.0.0 — получит.

RUN и установка пакетов

Пример с программой из пакетов системы и библиотеками Python:

Dockerfile
FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Слои и кэш

Каждая инструкция создаёт слой. При повторной сборке Docker берёт из кэша все слои до первой изменившейся инструкции, а всё после неё собирает заново. Поэтому порядок строк важен:

  1. сначала то, что меняется редко: основа, системные пакеты;
  2. потом список библиотек (COPY requirements.txt .) и их установка;
  3. в конце — код программы (COPY . .), который меняется постоянно.

Тогда после правки кода сборка занимает секунды: библиотеки берутся из кэша. Если копировать весь код до pip install, библиотеки будут ставиться заново при каждой правке.

Пересборка после правки app.py код копируется последним код копируется до pip install FROM python:3.13-slim из кэша WORKDIR /app из кэша COPY requirements.txt . из кэша RUN pip install … из кэша COPY . . заново CMD ["python", "app.py"] заново FROM python:3.13-slim из кэша WORKDIR /app из кэша COPY . . заново RUN pip install … заново CMD ["python", "app.py"] заново библиотеки из кэша — сборка за секунды pip install — при каждой правке
После правки app.py Docker берёт из кэша слои до первой изменившейся инструкции, поэтому код копируют после установки библиотек.

Образ не обновляется сам. Поменяли код — пересоберите образ (docker build) и пересоздайте контейнер (docker rm -f hello, затем снова docker run). Старый контейнер так и работает со старым образом.

Теги

Тег отличает версии одного образа. Один образ может носить несколько тегов:

Bash
docker build -t hello-web:1.1 -t hello-web:latest .
docker tag hello-web:1.1 hello-web:stable

Чтобы отправить образ в реестр, его имя должно начинаться с имени учётной записи или адреса реестра. Для Docker Hub:

Bash
docker login
docker tag hello-web:1.1 ivanov/hello-web:1.1
docker push ivanov/hello-web:1.1

Здесь ivanov — имя учётной записи на Docker Hub. Публичный образ видят все, поэтому пароли и ключи в образ не кладут никогда: из образа их достаёт любой.

Частые ошибки

Коротко

Проверьте себя

1. Когда выполняется инструкция RUN?
2. Что означает точка в конце docker build -t app:1.0 .?
3. Контейнер с вашей программой работает, порт опубликован через -p 8000:8000, но страница не открывается. Какая причина самая частая?
4. Почему COPY requirements.txt . и RUN pip install ставят до COPY . .?