Создание собственных образов Docker
Хранилище образов Docker Hub
Я уже писал статью про основы Docker: Первое знакомство с Docker — установка и базовые понятия . В ней я упомянул про хранилище образов — Docker Hub . Именно там, по умолчанию, 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!
Если понравилась статья, подпишись на мой канал в VK или Telegram .