Создание собственных образов Docker

🕒 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/nginx 4 слоя.
  • Пока не важно что это за слои. Скорее всего базовый слой 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 при копировании.

На первый взгляд инструкция 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! Надеюсь читать было интересно и не слишком сложно.

Мы используем cookie-файлы для наилучшего представления нашего сайта. Продолжая использовать этот сайт, вы соглашаетесь с использованием cookie-файлов.
Принять
Отказаться
Политика конфиденциальности