├── .gitignore
├── bot
├── Dockerfile
├── requirements.txt
└── main.py
├── nginx
├── first_start
│ └── default.conf.template
└── templates
│ └── default.conf.template
├── .env
├── docker-compose.yaml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .env.test
2 | .env.prod
3 | .DS_Store
4 | echo/
--------------------------------------------------------------------------------
/bot/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9-alpine
2 | WORKDIR /bot
3 | COPY requirements.txt requirements.txt
4 | RUN pip install --upgrade pip && pip install -r requirements.txt && chmod 755 .
5 | COPY . .
6 | ENV TZ Europe/Moscow
7 | CMD ["python3", "-u", "main.py"]
--------------------------------------------------------------------------------
/bot/requirements.txt:
--------------------------------------------------------------------------------
1 | aiogram==2.25.1
2 | aiohttp==3.8.4
3 | aiosignal==1.3.1
4 | async-timeout==4.0.2
5 | attrs==23.1.0
6 | Babel==2.9.1
7 | certifi==2023.5.7
8 | charset-normalizer==3.1.0
9 | frozenlist==1.3.3
10 | idna==3.4
11 | magic-filter==1.0.9
12 | multidict==6.0.4
13 | pytz==2023.3
14 | yarl==1.9.2
--------------------------------------------------------------------------------
/nginx/first_start/default.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 |
5 | server_name ${NGINX_HOST} www.${NGINX_HOST};
6 | server_tokens off;
7 |
8 | location /.well-known/acme-challenge/ {
9 | root /var/www/certbot;
10 | }
11 |
12 | location / {
13 | return 301 https://${NGINX_HOST}$request_uri;
14 | }
15 | }
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Telegram token, obtained from @botfather
2 | # Токен вашего бота, полученный у @botfather
3 | TELEGRAM_TOKEN = 'your_telegram_token'
4 | # Your domain, attached to VPS (let's encrypt most likely won't sign any free/automatic domain from you provider)
5 | # Ваш домен, привязанный к VPS (let's encrypt скорее всего не создаст ssl сертификат для бесплатного домена, который автоматом создал ваш провайдер)
6 | NGINX_HOST = 'your_domain.com'
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | bot:
5 | build: ./bot
6 | restart: always
7 | environment:
8 | - TELEGRAM_TOKEN
9 | - NGINX_HOST
10 | ports:
11 | - 3001:3001
12 | nginx:
13 | image: nginx:1.23-alpine
14 | ports:
15 | - 80:80
16 | - 443:443
17 | restart: always
18 | environment:
19 | - NGINX_HOST
20 | volumes:
21 | - ./nginx/first_start/:/etc/nginx/templates/:ro
22 | # - ./nginx/templates/:/etc/nginx/templates/:ro
23 | - ./certbot/www:/var/www/certbot/:ro
24 | - ./certbot/conf/:/etc/nginx/ssl/:ro
25 | certbot:
26 | image: certbot/certbot:v2.5.0
27 | volumes:
28 | - ./certbot/www/:/var/www/certbot/:rw
29 | - ./certbot/conf/:/etc/letsencrypt/:rw
--------------------------------------------------------------------------------
/nginx/templates/default.conf.template:
--------------------------------------------------------------------------------
1 | upstream bot{
2 | server bot:3001;
3 | }
4 |
5 | server {
6 | listen 80;
7 | listen [::]:80;
8 |
9 | server_name ${NGINX_HOST} www.${NGINX_HOST};
10 | server_tokens off;
11 |
12 | location /.well-known/acme-challenge/ {
13 | root /var/www/certbot;
14 | }
15 |
16 | location / {
17 | return 301 https://${NGINX_HOST}$request_uri;
18 | }
19 | }
20 |
21 | server {
22 | listen 443 default_server ssl http2;
23 | listen [::]:443 ssl http2;
24 |
25 | server_name ${NGINX_HOST};
26 |
27 | ssl_certificate /etc/nginx/ssl/live/${NGINX_HOST}/fullchain.pem;
28 | ssl_certificate_key /etc/nginx/ssl/live/${NGINX_HOST}/privkey.pem;
29 |
30 | location /webhook {
31 | proxy_pass http://bot;
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/bot/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from aiogram import Bot, Dispatcher, types, executor
3 | from aiogram.utils.executor import start_webhook
4 |
5 | # global variable to switch between polling|webhook for debug/local start
6 | IS_WEBHOOK = 1
7 |
8 | TELEGRAM_TOKEN = os.environ.get('TELEGRAM_TOKEN')
9 | NGINX_HOST = os.environ.get('NGINX_HOST')
10 |
11 | # webhook settings
12 | WEBHOOK_HOST = f'https://{NGINX_HOST}'
13 | WEBHOOK_PATH = '/webhook'
14 | WEBHOOK_URL = f'{WEBHOOK_HOST}{WEBHOOK_PATH}'
15 |
16 | # webserver settings
17 | WEBAPP_HOST = '0.0.0.0'
18 | WEBAPP_PORT = 3001
19 |
20 | # bot initialization
21 | bot = Bot(token=TELEGRAM_TOKEN)
22 | dp = Dispatcher(bot)
23 |
24 | # webhook startup & shutdown
25 | async def on_startup(dp):
26 | await bot.set_webhook(WEBHOOK_URL)
27 | print(f'Telegram servers now send updates to {WEBHOOK_URL}. Bot is online')
28 |
29 | async def on_shutdown(dp):
30 | await bot.delete_webhook()
31 |
32 | # echo handler function
33 | async def start_command_handler(message: types.Message):
34 | await message.reply('Hey there! I am a simple echo-bot, ready to deploy you projects.')
35 |
36 | # bot startup
37 | if __name__ == '__main__':
38 | dp.register_message_handler(start_command_handler, content_types=['text'])
39 | if IS_WEBHOOK == 1:
40 | start_webhook(
41 | dispatcher=dp,
42 | webhook_path=WEBHOOK_PATH,
43 | on_startup=on_startup,
44 | on_shutdown=on_shutdown,
45 | skip_updates=True,
46 | host=WEBAPP_HOST,
47 | port=WEBAPP_PORT,)
48 | else:
49 | executor.start_polling(dp, skip_updates=True)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python Telegram бот на webhook в docker на любом VPS/VDS за 3 минуты (nginx, ssl, compose)
2 |
3 | ## Основная информация
4 |
5 | На просторах гитхаба и особенно в .ру сегменте нет цельного базового решения для деплоя python telegram бота(ов) на виртуалку под докером и на вебхуках и ssl сертификатом от Let's Encrypt. Решил исправить ситуацию и сделать универсальный образ, готовый к развертыванию на VPS. Если запустите этот образ, то его можно легко дополнять микросервисами, в том числе django, postgres, redis и тд.
6 |
7 | Бот в комплекте основан на библиотеке aiogram - настройка его webhook'а описана тут https://docs.aiogram.dev/en/latest/examples/webhook_example.html. Это простой эхо-бот болванка, готовый для добавления вашей логики.
8 | У aiogram уже есть собственный веб-сервер, но нам интересен микросервисный подход и подключение других сервисов в docker compose, поэтому используем nginx.
9 |
10 | Описания @botfather и самого бота не будет. Если вы дошли до деплоя, то уже и так в курсе.
11 |
12 | ## Подготовка
13 |
14 | Чтобы все заработало, нам понадобится самый простой VPS, привязанный к нему домен, ssh доступ и пара минут.
15 |
16 | **Подготовка**
17 |
18 | 0. Выбираем понравившегося провайдера, заказываем простой виртуальный сервер на ubuntu (любой, 18-20 версий), 1 gb RAM и 10 gb SSD будет достаточно (хватит еще на пару микросервисов). Запоминаем его IP.
19 | 1. Скорее всего, провайдер предоставит бесплатный домен в своей подсети, но имейте в виду - Lets's Encrypt скорее всего не выпустит SSL сертификат, тк у него есть почасовая квота, которая по распространенным доменам расходуется моментально (например, хостинг timeweb и их бесплатный ***.tw1.ru)
20 | 2. Покупаем домен и привязываем его к IP нашего VPS. Не забываем проверить, что домен попал в общедоступные DNS-сервера (это все на автомате должен сделать доменный провайдер). Обычно это видно в личном кабинете провайдера в соответствующем разделе. А - запись для ipv4, AAAA - запись для ipv6.
21 |
22 | 2.1 Получили домен "your_domain.com"
23 |
24 | 2.2 Проверим в терминале (macos/linux) привязку домена к ip:
25 | ```
26 | nslookup your_domain.com
27 | ```
28 | В ответ должны получить:
29 | ```
30 | Non-authoritative answer:
31 | Name: your_domain.com
32 | Address: ip вашего VPS
33 | ```
34 |
35 | 3. Подготавливаем VPS:
36 |
37 | 3.1. Заходим на VPS по ssh, устаналиваем docker (compose также установится в комплекте) по гайду: https://docs.docker.com/engine/install/ubuntu/. Самый простой вариант - добавить репозиторий и скачать последнюю версию. Если ошибок нет - отлично, двигаемся дальше.
38 |
39 | 3.2. Клонируем этот репозиторий полностью:
40 | ```
41 | git clone https://github.com/ssharkexe/telegram-nginx-docker-webhook.git
42 | ```
43 |
44 | 3.3. Переходим в папку с проектом и прописываем переменные окружения в .env файл:
45 | ```
46 | cd telegram-nginx-docker-webhook
47 | sudo nano .env
48 | ```
49 | указываем в кавычках токен вашего бота (TELEGRAM_TOKEN) и домен (NGINX_HOST), сохраняем.
50 |
51 |
52 | 4. Дальше необходимо запустить отдельно nginx на 80 порту без SSL, чтобы выпустить для него SSL сертификат, но возникает коллизия: в составе всего проекта nginx не запустится, тк будет искать SSL сертификаты в указанных нами папках, а их там до обращения к let's encrypt нет. Есть несколько разных решений, я использую самое очевидное и с минимальной правкой кода:
53 |
54 | 4.1 Nginx с версии 1.19 поддерживает загрузку переменных окружения напрямую в свой конфигурационный файл, поэтому отдельно конфиг nginx править мы не будем (мы уже прописали в переменную окружения NGINX_HOST наш домен). Передача переменных происходит так: nginx при запуске забирает файл ***.conf.template (по сути - тот же nginx.conf, только с переменными окружения вида ${VARIABLE}), подменяет в нем все переменные на их значения и сохраняет этот файл в свою рабочую директорию nginx/conf.d/. Об этом указано на официальной странице nginx docker образа: https://hub.docker.com/_/nginx
55 |
56 | 4.2 В docker-compose.yaml в разделе nginx есть следующее:
57 | ```
58 | volumes:
59 | - ./nginx/first_start/:/etc/nginx/templates/:ro
60 | # - ./nginx/templates/:/etc/nginx/templates/:ro
61 | ```
62 | В комплекте у нас есть 2 конфига: сокращенный, только на 80 порт и без ssl, и полный для постоянной работы, на 80, 443 портах и ссылками на сертификаты. По умолчанию при первом запуске nginx подхватит конфиг из папки ./nginx/first_start/, поэтому:
63 | ВАЖНО: после выпуска сертификата нужно удалить / закомментировать первую строку и раскомментировать вторую, где указана папка ./nginx/templates/
64 |
65 | 4.3 Настройки выполнены, собираем образ и запускаем nginx
66 | ```
67 | docker compose --env-file .env build
68 | docker compose run --rm -d -p 80:80 nginx
69 | ```
70 | Важно, что при запуске отдельных сервисов compose не считывает мэппинг портов из docker-compose.yaml, поэтому в команде выше мы указали 80:80 вручную.
71 |
72 | 4.4 Отлично, nginx запущен, можно тут же в консоли выполнить:
73 | ```
74 | curl http://your_domain.com
75 | ```
76 | Если появилась ошибка 301 - все отлично! Идем дальше.
77 |
78 | ## Финальная часть. Получаем SSL сертификаты от Let's Encrypt
79 |
80 | Nginx запущен, ждет обращения по адресу /.well-known/acme-challenge/. Пробуем смоделировать выпуск сертификата (не забываем заменить your_domain.com на ваш домен):
81 | ```
82 | docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ --dry-run -d your_domain.com
83 | ```
84 | Вводим почту и соглашаемся на выпуск. Если в консоли появилось:
85 | ```
86 | The dry run was successful.
87 | ```
88 | Все отлично, можно выпускать сертификат. Выполняем аналогичную команду, только без --dry-run:
89 | ```
90 | docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d your_domain.com
91 | ```
92 |
93 | Поздравляю, сертификат выпущен (на 3 месяца)! Осталось завершить контейнер с nginx:
94 | ```
95 | docker compose kill
96 | docker compose down
97 | ```
98 | Скорректировать docker-compose.yaml в соответствии с п4.2:
99 | ```
100 | volumes:
101 | # - ./nginx/first_start/:/etc/nginx/templates/:ro
102 | - ./nginx/templates/:/etc/nginx/templates/:ro
103 | ```
104 | И запустить весь проект, предварительно его пересоздав:
105 | ```
106 | docker compose --env-file .env build
107 | docker compose up
108 | ```
109 | В консоли появится "Telegram servers now send updates to https://your_domain.com. Bot is online". Вы великолепны!
110 |
111 | Обновление сертификата через 3 месяца командой (если nginx уже запущен, первую команду пропускаем):
112 | ```
113 | docker compose up
114 | docker compose run --rm certbot renew
115 | ```
116 |
117 | ## Заметки
118 | Если вылезут баги, прошу оформить Issue.
119 | * В директории /bot есть Dockerfile, в котором прописан образ python 3.9 alpine и параметры запуска скрипта (+ бонус, установка timezone MSK)
120 | * Бот можно запустить и в режиме polling для отладки, в main.py установить IS_WEBHOOK = 0
--------------------------------------------------------------------------------