🕒 9 мин.
Из статьи вы узнаете что такое образ Docker и из чего он состоит. Как создавать свои образы с помощью файла Dockerfile. И как выкладывать собственные образы в репозиторий Docker Hub. Статья поможет вам сэкономить ваше время если вы только начинаете изучать Docker.
Хранилище образов Docker Hub
Я уже писал статью про основы Docker: [Первое знакомство с Docker — установка и базовые понятия]. В ней я упомянул про хранилище образов — Docker Hub (https://hub.docker.com/). Именно там, по умолчанию, Docker ищет необходимые образы. Таким образом, если вы не указываете другой реестр, система автоматически обращается именно к Docker Hub.
Для поиска образов вручную можно использовать команду docker search:
$ docker search ubuntu NAME DESCRIPTION STARS OFFICIAL ubuntu Ubuntu is a Debian-based Linux operating sys… 17731 [OK] ubuntu/squid Squid is a caching proxy for the Web. Long-t… 121 ubuntu/nginx Nginx, a high-performance reverse proxy & we… 133 ubuntu/cortex Cortex provides storage for Prometheus. Long… 4 ubuntu/kafka Apache Kafka, a distributed event streaming … 57 ubuntu/bind9 BIND 9 is a very flexible, full-featured DNS… 117 (часть вывода я убрал)
В Docker Hub образов много. Официальные образы (помечены [ОК] в колонке OFFICIAL, а STARS — это количество лайков от пользователей. В Docker Hub можно выкладывать свои образы, но для начала нужно научиться их создавать.
Что такое образ и из чего он состоит
Образ — это неизменяемый шаблон для контейнера который состоит из слоёв (layers). Слои образуются при создании образа, примерно так:
- Взяли мы базовый слой — Ubuntu → Это первый (нижний) слой образа.
- Установили туда зависимости для нашего приложения → Это второй слой образа.
- Скопировали туда свое приложение → Это третий слой образа.
- Запустили контейнер, приложение начало работать → В этот момент появляется самый верхний слой, но это слой контейнера а не образа.
Слои образа — неизменяемые (Read only). А слой контейнера изменяемый (Writable). Получается что образ Docker чем-то напоминает торт со множеством слоёв.
Для того чтобы всё это работало Docker использует специальную файловую систему — overlay2, которая встроена в ядро Linux и позволяет «склеивать» слои в одну файловую систему. Overlay2 — это Union File System (UnionFS). Это такой тип файловых систем, которые умеют объединять несколько каталогов (называемых слоями) в одну виртуальную файловую систему. Работает примерно так: если в верхнем слое есть файл с тем же именем, что и в нижнем — он перекрывает нижний. А изменения происходят только в самом верхнем (Writable) слое.
В Docker это используется для:
- Эффективного хранения образов. Если 10 контейнеров используют один и тот же базовый образ, то они используют один и тот же слой, а не копируют его 10 раз. Что уменьшает занимаемую память на диске.
- Быстрого создания контейнеров. При запуске контейнера Docker просто добавляет сверху записываемый слой, а все остальные слои используются только для чтения.
Давайте теперь на практике посмотрим из чего состоит образ ubuntu/nginx. С помощью команды docker pull скачаем его из Docker Hub:
$ docker pull ubuntu/nginx Using default tag: latest latest: Pulling from ubuntu/nginx 47e12efaf9fd: Pull complete 7acb27c4e542: Pull complete 78bf20dd0269: Pull complete 5a0b2d7fc968: Pull complete Digest: sha256:b50aa594cd940fa459144efa129dc21466f7d56d015993914f41f3eb983c893b Status: Downloaded newer image for ubuntu/nginx:latest docker.io/ubuntu/nginx:latest
- В отличии от
docker runэта команда просто скачивает образ без запуска контейнера.
А с помощью команды docker image inspect <образ> посмотрим на слои образа (в выводе нас интересует только «Layers»):
$ docker image inspect ubuntu/nginx:latest
***
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:e51a2e3545f80f9600b606dd979fd6afe1e040114f410c7465a711c04d1feff2",
"sha256:ff6bf5cef72d90e6df59f1c821c2f2a941af0e62f72ac3a66f6406dc27943097",
"sha256:e3e108e8507f8331018336708d29a816819878010301e7765bdbcdf9cd8a3343",
"sha256:5c1278caba4d7a57ab6dff73b7df666de0ba467125c4306236cfbbb8c89ed4d4"
]
},
***
- Эта команда предназначена для просмотра подробной технической информации об образе Docker.
- Здесь мы видим что у образа
ubuntu/nginx4 слоя. - Пока не важно что это за слои. Скорее всего базовый слой Ubuntu, установка Nginx, настройка конфигов.
Все слои хранятся в каталоге /var/lib/docker/overlay2/. Каждый слой — read-only. Когда запускается контейнер, сверху добавляется ещё 1 слой (writable). Там происходят изменения (создание файлов, запись логов и т.д). А при удалении контейнера этот слой исчезает.
Знакомство с Dockerfile
Итак, что такое образ мы узнали, теперь пора научится создавать образы самим. Делается это с помощью специального текстового файла — Dockerfile.
Каждая строка файла Dockerfile — это инструкция. Некоторые инструкции создают новые слои, а некоторые нет. Вот основные инструкции, из которых состоит Dockerfile:
FROM— Указывает базовый образ (обязательно первая строчка!).RUN— Выполняет команду во время сборки (например, установка пакетов).COPY— Копирует файлы/папки из хоста в образ.ADD— Тоже копирует файлы/папки в образ.CMD— Команда, которая выполнится при запуске контейнера (может быть переопределена).ENTRYPOINT— КакCMD, но команду нельзя полностью переопределить, зато можно добавлять аргументы к указанной команде.EXPOSE— Документирует, что контейнер слушает порт (но не открывает его!).WORKDIR— Устанавливает рабочую директорию для последующих команд.ENV— Устанавливает переменные окружения.
Инструкции COPY и ADD внешне действительно похожи — обе копируют файлы в образ. Однако, между ними есть важные различия.
- Во-первых,
COPY— это более простая и предсказуемая команда: она просто копирует локальные файлы или директории прямо в образ. Благодаря этому, её поведение легко контролировать, и именно её рекомендуют использовать в большинстве случаев. - Во-вторых,
ADDделает всё то же, что иCOPY, но дополнительно обладает расширенными возможностями. В частности, она может:- скачивать файлы по URL (например,
ADD https://example.com/file.tar.gz /app/); - автоматически распаковывать архивы:
.tar,.tar.gz,.zipпри копировании.
- скачивать файлы по URL (например,
На первый взгляд инструкция ADD — более интересная, но на практике она снижает прозрачность и воспроизводимость. Поэтому многие рекомендуют всегда использовать COPY вместо ADD. А если нужно скачать или распаковать файл, то делать это явно с помощью RUN и стандартных утилит (curl, wget, tar).
Файл Dockerfile может содержать комментарии — это строки начинающиеся на символ #.
Создание своего образа Docker
Создадим каталог для теста, например docker, и все файлы будем создавать в нём.
Затем, создадим простой скрипт hello.sh:
#!/bin/bash echo "Привет из Docker! Я запущен в контейнере." echo "Текущая дата: $(date)"
Создадим Dockerfile:
# Базовый образ FROM ubuntu # Копируем наш скрипт в образ COPY hello.sh /hello.sh # Делаем скрипт исполняемым внутри образа RUN chmod +x /hello.sh # Указываем, какую команду выполнять при запуске CMD ["/hello.sh"]
Сборка образа делается с помощью команды docker build:
$ docker build -t hello-docker .
-t— имя образа;.— контекст сборки (текущая папка) где находится Dockerfile.
Создадим контейнер из созданного образа:
$ docker run --name hello hello-docker Привет из Docker! Я запущен в контейнере. Текущая дата: Sat Nov 29 12:07:03 UTC 2025
docker run— создаёт и сразу запускает контейнер из указанного образа;--name hello— назначает имя контейнеру.
А вот так можем переопределить CMD при запуске контейнера:
$ docker run --name hello-ignore hello-docker echo "Я игнорирую CMD из Образа!" Я игнорирую CMD из Образа!
В примере выше мы переопределили команду из CMD указанного в образе. Но что если нужно запретить такое переопределение? Для этого существует инструкция ENTRYPOINT. Но тогда блок: echo "Я игнорирую CMD из Образа!" — будет воспринят как параметры.
Запустим ещё раз контейнеры с помощью docker start:
$ docker start -a hello Привет из Docker! Я запущен в контейнере. Текущая дата: Sat Nov 29 12:08:12 UTC 2025 $ docker start -a hello-ignore Я игнорирую CMD из Образа!
- Опция
-aнужна чтобы подключиться к терминалу контейнера. Без него команды будут выполнены, но на экране вывод не появится. - Кстати,
docker runпо умолчанию прикрепляется к терминалу контейнера, поэтому там не нужны дополнительные опции.
А ещё можно создать контейнер, который будет удаляться сразу после выполнения с помощью опции --rm. Это удобно для запуска подобных скриптов. Кстати, для таких (временных) контейнеров придумывать имя не обязательно.
$ docker run --rm hello-docker Привет из Docker! Я запущен в контейнере. Текущая дата: Sat Nov 29 12:09:03 UTC 2025
- Сразу после выполнения этот контейнер удалится.
Работа с переменными в Docker
Переменные окружения — это способ передавать конфигурацию в приложение без жёсткой прописки в коде. Это важно для:
- Смены режима (dev / prod);
- Подключения к БД;
- API-ключей;
- Настроек логирования и т.д.
Один из способов работы с переменными — записать их прямо в Dockerfile с помощью инструкции ENV. Добавим переменную в Dockerfile:
# Базовый образ FROM ubuntu # Копируем наш скрипт в образ COPY hello.sh /hello.sh # Делаем скрипт исполняемым внутри образа RUN chmod +x /hello.sh # Добавим переменную ENV NAME="DevOps" # Указываем, какую команду выполнять при запуске CMD ["/hello.sh"]
Изменим скрипт hello.sh:
#!/bin/bash echo "Привет, $NAME!" echo "Текущая дата: $(date)"
Пересоберём образ:
$ docker build -t hello-docker .
Создадим и запустим новый контейнер с опцией --rm:
$ docker run --rm hello-docker Привет, DevOps! Текущая дата: Sat Nov 29 12:11:00 UTC 2025
Существует ещё два способа задать переменные в момент создания контейнера (при выполнении команды docker run):
docker run --env-file .env— указываем файл со множеством переменных. Приоритет выше чемENVв Dockerfile.docker run -e KEY=value— указываем конкретную переменную. Имеет наивысший приоритет.
Создадим файл .env:
NAME="Elena"
И запустим контейнер:
$ docker run --rm --env-file .env hello-docker Привет, "Elena"! Текущая дата: Sat Nov 29 12:24:54 UTC 2025
И укажем переменную с помощью -e:
$ docker run --rm -e NAME=Alex hello-docker Привет, Alex! Текущая дата: Sat Nov 29 12:26:32 UTC 2025
То-есть у нас скрипт в контейнере использует переменную, а мы разными путями ему эту переменную передаём.
Создание Python приложения в контейнере
Теперь создадим простое Python-приложение, которое будет читать файл и выводить приветствие.
Напишем app.py:
def main():
try:
with open("message.txt", "r") as f:
content = f.read().strip()
print(f"Сообщение из файла: {content}")
except FileNotFoundError:
print("Файл message.txt не найден!")
if __name__ == "__main__":
main()
Создадим файл, из которого наше приложение будет брать текст message.txt:
Привет, это мой второй Docker!
Изменим Dockerfile. Для базового образа возьмём python:3.11-slim — это небольшой образ с интерпретатор Python внутри. Кроме этого укажем рабочий каталог, в который перекинем наше приложение и текстовый файл.
# Базовый образ FROM python:3.11-slim # Рабочий каталог контейнера WORKDIR /app # Копирование файлов в рабочий каталог контейнера COPY app.py . COPY message.txt . # Запуск процесса CMD ["python", "app.py"]
Соберём образ и запустим контейнер:
$ docker build -t my_app . $ docker run --rm my_app Сообщение из файла: Привет, это мой второй Docker!
Убедимся что файлы внутри контейнера. Для этого запустим контейнер с опциями -it и запустим дополнительный процесс sh:
$ docker run --rm -it my_app sh # pwd /app # ls app.py message.txt # exit
После выхода из контейнера он остановится и удалится (сработает опция --rm).
Веб-приложение и проброс портов
Контейнер работает в изолированной сети, и по умолчанию его порты недоступны с хоста. Чтобы открыть доступ, используется порт-маппинг (проброс портов). Делается это при запуске контейнера (docker run) с помощью опции: -p порт_хоста:порт_контейнера, например -p 8080:5000.
- запросы на порт 8080 хоста будут перенаправлены на 5000 порт контейнера;
- при этом приложение в контейнере должно слушать 5000 порт.
Для создания web-приложения я буду использовать Python-3 с модулем flask.
Создадим файл app.py:
from flask import Flask
# Для работы с переменными
import os
app = Flask(__name__)
# Читаем переменные окружения
greeting = os.getenv("GREETING", "Hello")
name = os.getenv("NAME", "World")
@app.route('/')
def hello():
return f"{greeting}, {name}!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Создадим файл с зависимостями для Python — requirements.txt:
Flask==2.3.3
Отредактируем Dockerfile:
FROM python:3.11-slim WORKDIR /app # копируем файл с зависимостями и устанавливаем их # --no-cache-dir -> чтобы уменьшить размер образа COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . # Задаём значения по умолчанию переменным ENV GREETING="Hello" ENV NAME="Stranger" CMD ["python", "app.py"]
В файле .env, укажем 2-е переменные:
GREETING=Bonjour NAME=Elena
Соберём образ и запустим контейнер с пробросом портов:
$ docker build -t greeting-app . $ docker run --rm --name my-greeting -d -p 8080:5000 greeting-app
Проверим работу:
$ curl http://localhost:8080 Hello, Stranger!
Посмотрим логи:
$ docker logs my-greeting * Serving Flask app 'app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://172.17.0.2:5000 Press CTRL+C to quit 172.17.0.1 - - [30/Nov/2025 17:13:10] "GET / HTTP/1.1" 200 -
Остановим контейнер:
$ docker stop my-greeting
Публикация своего образа в Docker Hub
Теперь настало время научиться публиковать свои образы в Docker Hub. Для начала нужно зарегистрироваться в https://hub.docker.com/. Затем создадим персональный токен для доступа из терминала: Account Settings → Personal Access Token.
Соберём образ ещё раз, указав свой логин из Docker Hub (замени italex88 на свой). А также нужно добавить версию (тег) приложения:
$ docker build -t italex88/greeting-app:1.0 .
italex88— логин;greeting-app— имя приложения;1.0— версия (тег) приложения.
Посмотрим на собранный образ:
$ docker image ls italex88/greeting-app:1.0 REPOSITORY TAG IMAGE ID CREATED SIZE italex88/greeting-app 1.0 f20b35c22d73 11 minutes ago 140MB
Залогинемся в Docker Hub (замени логин на свой, после чего нужно будет ввести свой токен):
$ docker login -u italex88
Выложим подготовленный образ в Docker Hub:
$ docker push italex88/greeting-app:1.0
Найдём свой образ в web-интерфейсе Docker Hub: https://hub.docker.com/repositories/ .
Удалим свой образ локально:
$ docker rmi italex88/greeting-app:1.0
И запустим контейнер с помощью docker run, при этом образ скачается из репозитория:
$ docker run --rm -d -p 8080:5000 --name my-greeting italex88/greeting-app:1.0
Проверим что приложение запустилось и работает:
$ curl http://localhost:8080 Hello, Stranger!
Перезапустим приложение с указанием .env файла:
$ docker stop my-greeting $ docker run --rm -d -p 8080:5000 --name my-greeting --env-file .env italex88/greeting-app:1.0
Проверим:
$ curl http://localhost:8080 Bonjour, Elena!
Итог
Вот мы и научились создавать и выкладывать свои образы в Docker Hub! Надеюсь читать было интересно и не слишком сложно.