Деплой – это процесс выгрузки и запуска ПО на рабочем сервере или в облачной среде, при этом деплой приложения зачастую может быть достаточно затрудненным: необходимо установить все нужные зависимости, не получив конфликта с другим ПО на сервере. Именно эту проблему и решает контейнеризация – например, можно взять Docker-контейнер Python, “упаковать” все зависимости приложения в одну сущность и, когда необходим деплой на сервер, запустить ее в изолированном окружении. Таким образом, средства контейнеризации позволяют упаковывать приложения и их зависимости в изолированные среды.
В данной статье мы разберем технологию контейнеризации на практике – расскажем, как происходит процесс создания контейнера на примере приложения Python, однако шаги будут в целом общими и для других языков. Также немного коснемся того, как передать образ Docker на другую машину.
Как формируются образы
В основе контейнеров лежат образы – стандартизированный набор всех файлов, библиотек, исполняемых файлов и конфигураций для их запуска. У них есть две важные особенности:
- Образы неизменны – после создания образа его невозможно изменить. Можно только создать новый образ или применить изменения поверх него.
- Образы как лук – у образов есть слои. Каждый слой представляет собой набор изменений системы – добавления, удаления или изменения файлов.
Эти особенности позволяют расширять или дополнять существующие образы контейнеров 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”:

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

Образы Alpine отличаются малым размером, для нашего приложения используется версия Python 3.13 – воспользуемся образом 3.13-alpine.
Отлично, образ выбрали – пришло время начать составлять 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 appadduser, в то время как в других дистрибутивах – 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Отправим сообщение боту – в случае успеха бот должен отправить аналогичное сообщение в ответ:

Экспорт образа
У нас есть рабочий образ, но на данный момент он только на машине, на которой происходила сборка. Как нам передать его на конечный сервер? В данном случае есть несколько способов.
Сохранение образа в архив
Первый способ – сохранить образ как архив 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, после чего создать репозиторий:

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

Авторизуйтесь в командной строке, используя команду 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 и введите код подтверждения, указанный в выводе вашей команды.

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

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

В терминале также будет сообщение об успешном входе:
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 pull, например:
docker pull plawork/telegram_sample:0.1.0Если вы создавали репозиторий приватным, для загрузки также потребуется авторизоваться командой docker login.
Деплой готового образа
Отлично, у нас есть готовый образ, осталось разместить его на сервере. Для этого воспользуемся решением Docker – оно готово к размещению контейнеров сразу после установки. Для создания сервера перейдите по ссылке готового решения и нажмите “Создать сервер” либо в разделе “Облако” выберите создание сервера, после чего переключитесь на готовое решение Docker.

Выберите подходящую вам конфигурацию, а также при желании добавьте для удобства публичный ключ 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Проверим работу бота – он также должен отправить наше сообщение обратно:

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