Контейнеризация Python-приложения с Docker: от создания до деплоя

Деплой – это процесс выгрузки и запуска ПО на рабочем сервере или в облачной среде, при этом деплой приложения зачастую может быть достаточно затрудненным: необходимо установить все нужные зависимости, не получив конфликта с другим ПО на сервере. Именно эту проблему и решает контейнеризация – например, можно взять Docker-контейнер Python, “упаковать” все зависимости приложения в одну сущность и, когда необходим деплой на сервер, запустить ее в изолированном окружении. Таким образом, средства контейнеризации позволяют упаковывать приложения и их зависимости в изолированные среды.

В данной статье мы разберем технологию контейнеризации на практике – расскажем, как происходит процесс создания контейнера на примере приложения Python, однако шаги будут в целом общими и для других языков. Также немного коснемся того, как передать образ Docker на другую машину.

Как формируются образы

В основе контейнеров лежат образы – стандартизированный набор всех файлов, библиотек, исполняемых файлов и конфигураций для их запуска. У них есть две важные особенности:

  1. Образы неизменны – после создания образа его невозможно изменить. Можно только создать новый образ или применить изменения поверх него.
  2. Образы как лук – у образов есть слои. Каждый слой представляет собой набор изменений системы – добавления, удаления или изменения файлов.

Эти особенности позволяют расширять или дополнять существующие образы контейнеров Docker. К примеру, в случае приложения Python разумно начать с образа нужной версии Python и добавить слои, добавляющие код вашего приложения и устанавливающие необходимые зависимости.

Как создать и запустить Dockerfile

Dockerfile представляет собой некое подобие рецепта, в нем указывается базовый образ Docker, а также добавляемые нами слои – то есть шаги, которые нужно выполнить для запуска приложения. Рассмотрим на примере простого приложения на Python – речь о боте из документации aiogram:

import asyncio
import logging
import sys
from os import getenv

from aiogram import Bot, Dispatcher, html
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.types import Message

# Bot token can be obtained via https://t.me/BotFather
TOKEN = getenv("BOT_TOKEN")

# All handlers should be attached to the Router (or Dispatcher)

dp = Dispatcher()


@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
    """
    This handler receives messages with `/start` command
    """
    # Most event objects have aliases for API methods that can be called in events' context
    # For example if you want to answer to incoming message you can use `message.answer(...)` alias
    # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
    # method automatically or call API method directly via
    # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
    await message.answer(f"Hello, {html.bold(message.from_user.full_name)}!")


@dp.message()
async def echo_handler(message: Message) -> None:
    """
    Handler will forward receive a message back to the sender

    By default, message handler will handle all message types (like a text, photo, sticker etc.)
    """
    try:
        # Send a copy of the received message
        await message.send_copy(chat_id=message.chat.id)
    except TypeError:
        # But not all the types is supported to be copied so need to handle it
        await message.answer("Nice try!")


async def main() -> None:
    # Initialize Bot instance with default bot properties which will be passed to all API calls
    bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))

    # And the run events dispatching
    await dp.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())

Также нам потребуется файл зависимостей нашего приложения requirements.txt:

aiofiles==24.1.0
aiogram==3.20.0.post0
aiohappyeyeballs==2.6.1
aiohttp==3.11.18
aiosignal==1.3.2
annotated-types==0.7.0
attrs==25.3.0
certifi==2025.6.15
frozenlist==1.7.0
idna==3.10
magic-filter==1.0.12
multidict==6.4.4
propcache==0.3.2
pydantic==2.11.7
pydantic-core==2.33.2
typing-extensions==4.14.0
typing-inspection==0.4.1
yarl==1.20.1

Итак, для начала необходимо выбрать базовый образ – в нашем случае удобнее всего будет начать с образа с уже установленным Python. Для подбора образа воспользуемся hub.docker.com, введя в поле поиска “Python”:

docker hub

При выборе Python в результатах откроется страница со списком доступных официальных образов от Docker:

docker python

Образы Alpine отличаются малым размером, для нашего приложения используется версия Python 3.13 – воспользуемся образом 3.13-alpine.

Обратите внимание!
Необязательно использовать Docker Hub для загрузки образов контейнеров – можно искать и в сторонних репозиториях. Однако не стоит загружать образы контейнеров из неизвестных источников.

Отлично, образ выбрали – пришло время начать составлять Dockerfile. Для указания базового образа существует директива FROM:

FROM python:3.13-alpine

Дальше потребуется задать рабочую директорию, в нее будут копироваться наши файлы и в ней же – выполняться дальнейшие команды. Воспользуемся директивой WORKDIR, указав путь внутри контейнера:

WORKDIR /app

Затем потребуется установить зависимости нашего приложения – для этого скопируем файл requirements.txt в контейнер командой COPY, а директивой RUN создадим виртуальное окружение, активируем его и вызовем pip для запуска установки:

COPY requirements.txt ./
RUN python -m venv .venv \
&& source .venv/bin/activate \
&& pip install --no-cache-dir -r requirements.txt

Отлично, зависимости готовы, теперь нужно скопировать код нашего проекта. В данном случае это просто файл main.py:

COPY main.py ./

Создадим пользователя для запуска приложения и выберем его, чтобы контейнер не запускался от пользователя root. Тут помогут RUN и USER соответственно:

RUN adduser app --disabled-password --no-create-home --gecos ""
USER app
Обратите внимание!
При использовании команд важно учитывать особенности используемого базового дистрибутива – например, в Alpine для добавления пользователя используется команда adduser, в то время как в других дистрибутивах – useradd.

И наконец, зададим команду для старта приложения при запуске контейнера директивой CMD:

CMD ["/app/.venv/bin/python", "./main.py"]

Итоговый Dockerfile выглядит следующим образом:

FROM python:3.13-alpine
WORKDIR /app
COPY requirements.txt ./
RUN python -m venv .venv \
&& source .venv/bin/activate \
&& pip install --no-cache-dir -r requirements.txt
COPY main.py ./
RUN adduser app --disabled-password --no-create-home --gecos ""
USER app
CMD ["/app/.venv/bin/python", "./main.py"]

Остается лишь собрать контейнер, задать для него тег и проверить его работу. Для сборки используем команду docker buildx build. Опционально ключом -t можно сразу добавить название образа и его тег:

docker buildx build -t telegram_sample:0.1.0 .
[+] Building 12.4s (11/11) FINISHED                                                                                                                                                           docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                                    0.1s
 => => transferring dockerfile: 341B                                                                                                                                                                    0.0s
 => [internal] load metadata for docker.io/library/python:3.13-alpine                                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                                                                       0.1s
 => => transferring context: 2B                                                                                                                                                                         0.0s
 => [1/6] FROM docker.io/library/python:3.13-alpine                                                                                                                                                     0.0s
 => [internal] load build context                                                                                                                                                                       0.1s
 => => transferring context: 65B                                                                                                                                                                        0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                                                                                           0.0s
 => [3/6] COPY requirements.txt ./                                                                                                                                                                      0.2s
 => [4/6] RUN python -m venv .venv && source .venv/bin/activate && pip install --no-cache-dir -r requirements.txt                                                                                       9.9s
 => [5/6] COPY main.py ./                                                                                                                                                                               0.2s
 => [6/6] RUN adduser app --disabled-password --no-create-home --gecos ""                                                                                                                               0.4s
 => exporting to image                                                                                                                                                                                  1.0s
 => => exporting layers                                                                                                                                                                                 1.0s
 => => writing image sha256:87a516b211272c9e5e33ac31275fdae31b08890872f149b71a1ed72277c11911                                                                                                            0.0s
 => => naming to docker.io/library/telegram_sample:0.1.0

Проверим работу образа, запустив контейнер и передав токен бота в переменной окружения:

docker run -e BOT_TOKEN='REDACTED' -d -it telegram_sample
ab94d6e790f55f9f00148fe268fc841c220b73358bc4bb79294cf7eba69b2619

Контейнер успешно запустился:

docker ps
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS     NAMES
ab94d6e790f5   telegram_sample   "/app/.venv/bin/pyth…"   8 seconds ago   Up 8 seconds             eager_easley

Отправим сообщение боту – в случае успеха бот должен отправить аналогичное сообщение в ответ:

telegram bot

Экспорт образа

У нас есть рабочий образ, но на данный момент он только на машине, на которой происходила сборка. Как нам передать его на конечный сервер? В данном случае есть несколько способов.

Сохранение образа в архив

Первый способ – сохранить образ как архив tar.gz командой docker save, после чего загрузить его на конечный сервер и импортировать командой docker load.

Для сохранения команда на примере образа telegram_sample:0.1.0 будет выглядеть так:

docker save telegram_sample:0.1.0 | gzip > telegram_sample.tar.gz

Для импорта полученного архива на нужной машине используется команда docker load:

docker load < telegram_sample.tar.gz

Передача образа по SSH

Второй способ комбинирует команды save и load для передачи образа по SSH, минуя сохранение в файл. Для передачи образа telegram_sample:0.1.0 по SSH на сервер host с пользователем user команда будет выглядеть так:

docker save telegram_sample:0.1.0 | gzip | ssh user@host docker load

Также можно воспользоваться альтернативным вариантом команды:

docker save telegram_sample:0.1.0 | gzip | DOCKER_HOST=ssh://user@host docker load

Загрузка образа в реестр контейнеров

Третий способ – использование репозитория в реестре контейнеров наподобие Docker Hub. На его примере мы и рассмотрим процесс. В первую очередь потребуется зарегистрироваться либо авторизоваться на сайте hub.docker.com, после чего создать репозиторий:

create a repository

Введите название репозитория, а также выберите тип – приватный или публичный – и подтвердите создание. Название репозитория должно совпадать с названием образа:

repository private

Авторизуйтесь в командной строке, используя команду docker login:

docker login

USING WEB-BASED LOGIN

i Info → To sign in with credentials on the command line, use 'docker login -u <username>'


Your one-time device confirmation code is: GVFT-TDSS
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate

Waiting for authentication in the browser…

Перейдите по ссылке https://login.docker.com/activate и введите код подтверждения, указанный в выводе вашей команды.

login docker

Подтвердите корректность кода, сверившись с выводом терминала:

docker device confirmation

В случае успешного входа появится сообщение об успешной авторизации:

docker authorization

В терминале также будет сообщение об успешном входе:

docker login

USING WEB-BASED LOGIN

i Info → To sign in with credentials on the command line, use 'docker login -u <username>'


Your one-time device confirmation code is: PCLP-LTCP
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate

Waiting for authentication in the browser…

WARNING! Your credentials are stored unencrypted in '/home/pinklife/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/

Login Succeeded

Пройдя авторизацию, вы сможете загрузить образ в репозиторий командой:

docker push имя_пользователя/название_репозитория:тег

Например:

docker push plawork/telegram_sample:0.1.0
The push refers to repository [docker.io/plawork/telegram_sample]
41ab6c4bcb57: Pushed
9e9e770ab52e: Pushed
51ad86ed9582: Pushed
c6758331ff6c: Pushed
560547f983eb: Pushed
6a9854a10c2e: Mounted from library/python
840d7d095707: Mounted from library/python
40b0a95a818e: Mounted from library/python
fd2758d7a50e: Mounted from library/python
0.1.0: digest: sha256:54c5cc2ce309b604feecb9c17259ccf41ce1571bb03064166a76d985e308c7ee size: 2197

В этом случае может появиться ошибка формата:

An image does not exist locally with the tag: plawork/telegram_sample

Тогда необходимо дополнительно протегировать образ, используя имя пользователя. Сделать это можно командой docker image tag, например, для пользователя plawork и образа с названием telegram_sample это будет выглядеть так:

docker image tag telegram_sample:0.1.0 plawork/telegram_sample:0.1.0

После загрузки образ появится в списке тегов:

docker tags

Чтобы загрузить его на конечной машине, используйте команду docker pull, например:

docker pull plawork/telegram_sample:0.1.0

Если вы создавали репозиторий приватным, для загрузки также потребуется авторизоваться командой docker login.

Деплой готового образа

Отлично, у нас есть готовый образ, осталось разместить его на сервере. Для этого воспользуемся решением Docker – оно готово к размещению контейнеров сразу после установки. Для создания сервера перейдите по ссылке готового решения и нажмите “Создать сервер” либо в разделе “Облако” выберите создание сервера, после чего переключитесь на готовое решение Docker. 

Beget приложения

Выберите подходящую вам конфигурацию, а также при желании добавьте для удобства публичный ключ SSH – с ним можно осуществлять подключение к серверу без ввода пароля. О том, как создать пару ключей, вы можете прочитать в нашей статье. По завершении настройки подтвердите создание сервера.

Далее потребуется передать образ на созданный сервер одним из описанных в прошлом разделе способов. Воспользуемся вторым способом:

docker save telegram_sample:0.1.0 | gzip | DOCKER_HOST=ssh://root@ip_сервера docker load
fd2758d7a50e: Loading layer [==================================================>]  8.594MB/8.594MB
40b0a95a818e: Loading layer [==================================================>]  1.689MB/1.689MB
840d7d095707: Loading layer [==================================================>]  37.29MB/37.29MB
6a9854a10c2e: Loading layer [==================================================>]   5.12kB/5.12kB
560547f983eb: Loading layer [==================================================>]  1.536kB/1.536kB
c6758331ff6c: Loading layer [==================================================>]   2.56kB/2.56kB
51ad86ed9582: Loading layer [==================================================>]  42.81MB/42.81MB
9e9e770ab52e: Loading layer [==================================================>]  4.096kB/4.096kB
41ab6c4bcb57: Loading layer [==================================================>]  9.216kB/9.216kB
Loaded image: telegram_sample:0.1.0

Образ передан на конечный сервер – осталось запустить контейнер. Подключаемся к серверу по SSH:

ssh root@ip_сервера

Запускаем контейнер, указав нужные для работы приложения переменные окружения и политику перезапуска – в данном случае всегда перезапускать, если он не был остановлен вручную.

docker run -e BOT_TOKEN='REDACTED' -d -it --restart unless-stopped telegram_sample:0.1.0
a577d18ba9e1cdd0ecf4f2dcba426bf8f3cdd24ab4b8a31a659b4d23a9b6c477

Проверим работу бота – он также должен отправить наше сообщение обратно:

tg bot test

Бот работает как ожидалось, значит, он успешно задеплоен, а наша статья о контейнеризации Python-приложения с Docker подходит к концу.

Заключение

В данной статье мы кратко рассказали, как создать контейнер с собственным приложением на Python, передать его на другую машину для дальнейшего запуска и задеплоить получившийся контейнер на нашем готовом решении Docker.

Если возникнут вопросы, напишите нам, пожалуйста, тикет из панели управления аккаунта (раздел “Помощь и поддержка”), а если вы захотите обсудить эту статью, системы контейнеризации или нашу облачную платформу с коллегами по цеху и сотрудниками Beget – ждем вас в нашем сообществе в Telegram.

0
1446