├── .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 --------------------------------------------------------------------------------