17 β Production va deploy¶
β¬ οΈ Oldingi: 16 β Testlash va xatolarni boshqarish Β· π README Β· Keyingi: 18 β Yakuniy kapston: to'liq bot β‘οΈ
Bu bobda: botingizni o'z kompyuteringizdan chiqarib, doimiy ishlaydigan holatga keltirasiz. O'rganamiz: nega
python bot.pyni terminalda qoldirib bo'lmaydi; VPS (virtual server) nima va kod u yerga qanday yetadi;.envva sirlarni xavfsiz saqlash (BOT_TOKENhech qachon kodga yoki Git'ga tushmaydi); production'da polling va webhook orasidagi tanlov va to'g'ri sozlash; graceful shutdown (tartibli to'xtash) β handler yarmida uzilib qolmaslik uchunstartup/shutdownhook'lar; botni doimiy ushlab turuvchi systemd va supervisor xizmatlari; Docker vaDockerfile(konteynerda paketlash); log va monitoring (xatolikni admin'ga yetkazish); avtomatik qayta ishga tushirish (Restart=always); va aiogram'ning production best-practice'lari (defaultparse_mode,allowed_updates, retry, bitta instans qoidasi). CI/CD (avtomatik deploy) uchun GitHub Actions bobi ga bog'lanamiz.Halol eslatma: bu bobdagi aiogram kod qismlari β
startup/shutdownlifecycle hook'lar, webhook aiohttp ilovasini qurish va route ro'yxatga olish,.envtoken o'qish,resolve_used_update_types()bilanallowed_updates, defaultparse_mode, handler routing β offline, tokensiz ishga tushirib tekshirildi (natijalar matnda). Lekin VPS, Docker, systemd, nginx buyruqlari illustrativ: ular real Linux server, internet va BotFather tokeni talab qiladi β bu yerda Windows muhitida ishga tushirilmaydi. Konfiguratsiya fayllari (Dockerfile, unit fayl, .env) sintaksis jihatdan to'g'ri va ko'chirib ishlatishga tayyor, lekin "deploy bo'ldi / xabar yetib bordi" degan soxta natija yozilmagan.
1. Nega "terminalda qoldirish" deploy emas¶
15-bobgacha botingizni shunday ishga tushirib keldingiz:
Terminal ochiq turganda bot ishlaydi. Lekin:
- terminalni yopsangiz β bot o'ladi;
- kompyuteringizni o'chirsangiz yoki uxlatib qo'ysangiz β bot o'ladi;
- internet uzilsa, bot to'xtaydi va o'zi qayta yoqilmaydi;
- xato (exception) bo'lib jarayon yiqilsa β boshqa hech kim uni qayta ishga tushirmaydi.
Production (ishlab chiqarish) deganda biz quyidagini nazarda tutamiz: bot doimiy, internetga ulangan serverda turadi, o'zi avtomatik ishga tushadi, yiqilsa o'zini qayta yoqadi, va siz uxlab yotganingizda ham foydalanuvchilarga xizmat qiladi.
Diagrammada production botning to'liq manzarasi: VPS (doim yoqilgan Linux server) ichida systemd botni doimiy ushlab turadi; bot Docker konteynerida ishlaydi; yon servislar (Redis, DB) va loglar ham shu yerda. Telegram bilan aloqa esa o'sha tanish getUpdates/sendMessage.
Bu bob Python'ni va aiogram asoslarini biladi deb faraz qiladi. Agar
async/await,venv,pipnotanish bo'lsa, avval Python asoslari ga qarang. VPS/Linux buyruqlari bu kitobning mavzusi emas, shu sababli ular illustrativ β siz ularni o'z server provayderingiz hujjatiga qarab moslaysiz.
2. VPS nima va kod u yerga qanday yetadi¶
VPS (Virtual Private Server) β bu internetda doim yoqilgan, sizga ijaraga berilgan kichik Linux kompyuter. Oddiy provayderlar: Hetzner, DigitalOcean, Vultr, Contabo, Timeweb va h.k. Eng arzon tarif (1 yadro, 1-2 GB RAM) ko'p botlar uchun yetarli.
Kod serverga ikki asosiy yo'l bilan yetadi:
- Git orqali (tavsiya etiladi). Loyihangiz GitHub'da, serverda
git cloneqilasiz, yangilanish kelgandagit pull. Bu eng toza yo'l β versiya nazorati, tarix, qaytarish hammasi bor. Git asoslari uchun Git/GitHub kitobi. - Fayl ko'chirish (
scp/rsync). Kichik narsalar uchun, lekin tarixsiz va xatoga moyil.
Serverga kirish odatda SSH orqali (illustrativ):
# bu buyruqlar SERVERDA (Linux) bajariladi β illustrativ, real VPS talab qiladi
ssh root@123.45.67.89
# Python va git o'rnatish (Ubuntu/Debian misoli)
apt update && apt install -y python3 python3-venv python3-pip git
# loyihani klonlash
git clone https://github.com/sizning-username/mening-botim.git
cd mening-botim
# virtual muhit va paketlar
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
requirements.txt β loyihangiz bog'liqliklari. Versiyalarni qotirib yozing (reproducible bo'lsin):
aiogram==3.28.2
aiohttp==3.13.5
python-dotenv==1.0.1
# FSM uchun Redis kerak bo'lsa:
# aiogram[redis]==3.28.2
# DB uchun:
# aiosqlite==0.20.0
# SQLAlchemy==2.0.36
Eslatma β
aiogram[redis]. Production'da FSM holatini Redis'da saqlamoqchi bo'lsangiz,redispaketi alohida o'rnatiladi:pip install "aiogram[redis]". Biz buni offline tekshirdik βaiogram.fsm.storage.redismoduli aiogram bilan keladi, lekinrediskutubxonasi qo'shimcha ekstra sifatida o'rnatiladi (5-bo'limda batafsil).
3. Sirlar: .env va BOT_TOKEN (kodga yozilmaydi!)¶
Eng muhim xavfsizlik qoidasi: bot tokeni β bu sizning botingizning paroli. Uni qo'lga kiritgan har kim sizning bot nomingizdan xabar yuborishi, foydalanuvchilarni aldashi mumkin. Shuning uchun token hech qachon:
- kodga to'g'ridan yozilmaydi (
Bot("123456:AAH...")β β yomon); - Git'ga commit qilinmaydi;
- log'ga yoki xato xabariga chiqarilmaydi.
To'g'ri yo'l β muhit o'zgaruvchisi (environment variable). Lokal va serverda .env faylida saqlanadi:
# .env (bu fayl .gitignore ichida bo'lishi SHART)
BOT_TOKEN=123456789:AAExampleTokenFromBotFather
ADMIN_ID=987654321
REDIS_URL=redis://localhost:6379/0
.gitignore ga albatta qo'shing:
Endi kodda tokenni .env dan o'qiymiz. python-dotenv faylni o'qib os.environ ga yuklaydi:
import os
from dotenv import load_dotenv
load_dotenv() # .env faylni o'qib muhitga yuklaydi
def get_token() -> str:
token = os.environ.get("BOT_TOKEN")
if not token:
# token yo'q bo'lsa β darhol va aniq xato (jim ishlamaslik kerak)
raise RuntimeError("BOT_TOKEN topilmadi! .env faylini tekshiring.")
return token
Biz bu naqshni offline ishga tushirib tekshirdik: token bo'lganda get_token() uni qaytaradi, bo'lmaganda esa aniq RuntimeError ko'taradi (jim None bilan davom etmaydi). Mana tasdiqlangan natija:
allowed_updates = ['callback_query', 'message']
OK t3: token .env-dan o'qildi, allowed_updates avtomatik aniqlandi
Toza tuzilma β pydantic-settings. Katta loyihada
os.environ.get(...)larni hamma joyga tarqatish o'rniga, sozlamalarni bitta tipli klassda yig'ish qulay. Bu naqsh modullar bobida (DI/konfiguratsiya) ko'rsatiladi; bu yerda asosiy g'oya muhim: sir = muhit o'zgaruvchisi, hech qachon kod emas.
# pydantic-settings bilan (illustrativ struktura β fikr bir xil)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
bot_token: str
admin_id: int
redis_url: str = "redis://localhost:6379/0"
model_config = SettingsConfigDict(env_file=".env")
settings = Settings() # .env dan avtomatik o'qiydi, tip va majburiylikni tekshiradi
4. Production'da polling va webhook¶
5-bo'limgacha siz polling ishlatib keldingiz: dp.start_polling(bot). U Telegram'dan "yangi xabar bormi?" deb doim so'rab turadi. Bu rivojlash uchun zo'r. Production'da esa ikki yo'l bor.
Polling β oddiy va ko'p hollarda yetarli¶
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
async def main():
bot = Bot(
token=get_token(),
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
dp = Dispatcher(storage=MemoryStorage())
# ... dp.include_router(...) ...
# eski getUpdates'larni tashlab, faqat yangi kelganlarini olamiz:
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
drop_pending_updates=True β bot uzoq o'chiq turgan bo'lsa, to'planib qolgan eski xabarlarni qayta ishlamaslik uchun ularni tashlab yuboradi. delete_webhook esa avval webhook qo'yilgan bo'lsa, uni olib tashlaydi (aks holda polling ishlamaydi).
Bitta token = bitta polling instans. Telegram bir token uchun bir vaqtning o'zida faqat bitta
getUpdatesjarayoniga ruxsat beradi. Agar siz xato bilan ikkita instans ishga tushirsangiz, ular bir-birining update'ini "o'g'irlaydi" vaTelegramConflictErrorchiqadi. Deploy paytida eski instansni to'xtatib, keyin yangisini yoqing (8-bo'lim, systemd buni avtomatik qiladi).
run_polling ham bor β u start_polling ustiga signal'larni (SIGTERM/SIGINT) ham boshqaradi:
# qulay yuqori darajali variant β signal va shutdown'ni o'zi ushlaydi
dp.run_polling(bot, allowed_updates=dp.resolve_used_update_types())
run_polling β asyncio.run(...) ni o'zi chaqiradi, shuning uchun uni async def ichidan emas, balki to'g'ridan top-level'dan chaqiriladi.
allowed_updates β keraksiz trafikni kesish¶
Telegram standart holatda barcha update turlarini yubormaydi (masalan, chat_member). Botingiz aniq qaysi turlarni ishlatishini aytib, keraksizlarini kessangiz, trafik va yuk kamayadi. aiogram buni handler'laringizdan avtomatik aniqlaydi:
used = dp.resolve_used_update_types()
# masalan: ['callback_query', 'message']
await dp.start_polling(bot, allowed_updates=used)
Biz buni offline tasdiqladik: @router.message(...) va @router.callback_query() handler'lari bo'lgan dispatcher uchun resolve_used_update_types() aynan ['callback_query', 'message'] ni qaytardi.
Webhook β yuqori yuk uchun¶
Webhook'da Telegram o'zi botning HTTPS URL'iga update'ni POST qiladi. Bu tezroq va ko'p instansga oson scaling beradi, lekin public HTTPS URL (domen + sertifikat) talab qiladi. aiogram aiohttp serveri bilan keladi:
from aiohttp import web
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
BASE_URL = "https://bot.misol.uz" # sizning public domeningiz
WEBHOOK_PATH = "/webhook/abc-secret-path" # tasodifiy, taxmin qilib bo'lmas yo'l
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"] # .env dan
async def on_startup(bot: Bot) -> None:
# JONLI: Telegram'ga "shu URL'ga update yubor" deb aytamiz (illustrativ β public URL kerak)
await bot.set_webhook(
f"{BASE_URL}{WEBHOOK_PATH}",
secret_token=WEBHOOK_SECRET,
allowed_updates=dp.resolve_used_update_types(),
drop_pending_updates=True,
)
def main():
bot = Bot(get_token(), default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage())
# ... dp.include_router(...) ...
dp.startup.register(on_startup)
app = web.Application()
handler = SimpleRequestHandler(dispatcher=dp, bot=bot, secret_token=WEBHOOK_SECRET)
handler.register(app, path=WEBHOOK_PATH)
setup_application(app, dp, bot=bot)
# JONLI: real port ochadi va Telegram so'rovlarini kutadi (illustrativ β server + domen kerak)
web.run_app(app, host="0.0.0.0", port=8080)
if __name__ == "__main__":
main()
secret_token β majburiy xavfsizlik chorasi. Telegram har bir so'rovda uni X-Telegram-Bot-Api-Secret-Token sarlavhasida yuboradi; SimpleRequestHandler uni tekshiradi va mos kelmasa so'rovni rad etadi. Bu sizning URL'ingizni topib olgan begona odam soxta update yuborolmasligini ta'minlaydi.
Biz webhook qismini offline tekshirdik (real run_app chaqirmasdan β u port ochib, public URL kutadi): SimpleRequestHandler va setup_application to'g'ri import bo'ldi, aiohttp ilovasiga route haqiqatan qo'shildi. Tasdiqlangan natija:
routes: ['/webhook/abc-secret-path']
OK t2: SimpleRequestHandler/setup_application import va register ishladi
Webhook'ni nginx orqasiga qo'ying. Production'da aiohttp serverini to'g'ridan internetga ochmang. Oldida nginx (reverse proxy) turadi: u HTTPS sertifikatini (Let's Encrypt) ushlaydi va so'rovni ichki
127.0.0.1:8080ga uzatadi. Bu illustrativ nginx bloki:
# illustrativ β real domen va sertifikat talab qiladi
server {
listen 443 ssl;
server_name bot.misol.uz;
ssl_certificate /etc/letsencrypt/live/bot.misol.uz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.misol.uz/privkey.pem;
location /webhook/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Qaysi birini tanlash? Boshlovchi va o'rta botlar uchun polling β oddiy, ishonchli, URL/domen kerakmas. Webhook'ga faqat juda yuqori yuk yoki ko'p-instansli scaling kerak bo'lganda o'ting.
5. FSM holatini production'da saqlash: Redis¶
8-bobda FSM (holatlar mashinasi) MemoryStorage bilan ishladi. Production'da bu xavfli: bot qayta yoqilsa, xotira tozalanadi β foydalanuvchilar to'ldirayotgan formalar, holatlar yo'qoladi. Yechim β holatni tashqi omborga (Redis) saqlash:
# pip install "aiogram[redis]" (redis paketi alohida ekstra)
from aiogram import Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url(os.environ["REDIS_URL"]) # masalan redis://localhost:6379/0
dp = Dispatcher(storage=storage)
Endi bot yiqilib qayta yonsa ham, foydalanuvchi qaysi holatda turgani Redis'da saqlanib qoladi. MemoryStorage faqat rivojlash va testlar uchun.
Halol eslatma. Biz offline test muhitida
redisserveriga ulanmadik (u alohida xizmat talab qiladi). Lekin tekshirdik:aiogram.fsm.storage.redismoduli aiogram'ning o'zida mavjud (importlib.util.find_specbilan), va uredispaketini talab qiladi (pip install "aiogram[redis]").RedisStorage.from_url(...)β aiogram hujjatida ko'rsatilgan idiomatik konstruktor. Real ulanish Redis serveri ishlab turishini talab qiladi, shuning uchun bu qismni "illustrativ β Redis xizmati kerak" deb belgilaymiz.
6. Graceful shutdown β tartibli to'xtash¶
Deploy paytida yoki server qayta yuklanganda bot to'xtatiladi. Agar uni shartta o'ldirsangiz, o'sha onda ishlayotgan handler yarmida uzilib qoladi, DB ulanishlari toza yopilmaydi. Graceful shutdown β botni tartibli to'xtatish: yangi update qabul qilishni to'xtatib, ishlayotganlarini tugatib, ulanishlarni yopib, keyin chiqish.
aiogram buning uchun lifecycle hook'lar beradi: startup (bot yonganda) va shutdown (to'xtaganda). Bu yerda biz DB/Redis ochib-yopamiz, webhook qo'yamiz/olamiz, admin'ga "bot yondi/to'xtadi" deb xabar beramiz:
from aiogram import Bot, Dispatcher
async def on_startup(bot: Bot) -> None:
# bot ishga tushganda bir marta: DB ulanishini ochish, admin'ga xabar
print("Bot ishga tushdi")
async def on_shutdown(bot: Bot) -> None:
# bot to'xtaganda bir marta: ulanishlarni yopish, oxirgi log
print("Bot to'xtadi, ulanishlar yopildi")
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
Dekorator shakli ham bor β bir xil ishlaydi:
Biz bu lifecycle tartibini offline tekshirdik. emit_startup/feed_update/emit_shutdown ketma-ketligida hook'lar va handler aynan to'g'ri tartibda ishladi:
[('startup', None), ('handler', 42), ('shutdown', None)]
OK t1: startup -> handler -> shutdown tartibi to'g'ri
dp.start_polling(bot) va web.run_app(...) ikkalasi ham bu hook'larni avtomatik chaqiradi: start_polling SIGTERM/SIGINT (Ctrl+C, systemctl stop) ni ushlaydi, polling'ni to'xtatadi, shutdown hook'larni ishga soladi va bot.session.close() ni o'zi qiladi. Sizning vazifangiz β ulanishlaringizni shutdown hook'da yopish.
SIGKILL(-9) β graceful emas. Agar jarayonnikill -9bilan o'ldirsangiz, OS uni darhol uradi va hech qanday hook ishlamaydi (diagrammadagi qizil quti). Shuning uchunsystemctl stop(uSIGTERMyuboradi) ishlatiladi,kill -9emas.
To'liq graceful polling skeleti (DB bilan):
import asyncio
import aiosqlite
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
async def main():
bot = Bot(get_token(), default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage())
# dp.include_router(...)
db = await aiosqlite.connect("bot.db")
async def on_startup(bot: Bot):
print("ulanishlar tayyor")
async def on_shutdown(bot: Bot):
await db.close() # DB'ni toza yopamiz
print("DB yopildi")
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
await bot.delete_webhook(drop_pending_updates=True)
try:
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
finally:
await bot.session.close() # har holatda sessiyani yopamiz
if __name__ == "__main__":
asyncio.run(main())
7. Log va monitoring¶
Production'da print yetarli emas. Sizga strukturali log kerak: vaqt, daraja (INFO/WARNING/ERROR), qaysi modul. Python'ning standart logging moduli buni beradi:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
)
# aiogram ham shu logger orqali yozadi
log = logging.getLogger("bot")
log.info("Bot ishga tushdi")
log.warning("Foydalanuvchi %s cheklovga tushdi", user_id)
Biz handler ichida log.info(...) ishlatib, routing va loglashni offline pytest bilan tekshirdik β 2 ta test o'tdi:
Kutilmagan xatoni admin'ga yetkazish¶
Eng muhim monitoring: bot ichida kutilmagan xato bo'lganda jim yiqilib qolmasin, balki sizga (admin'ga) xabar bersin. aiogram'da buni xatolik handler'i (@dp.errors) bilan qilamiz:
from aiogram import Dispatcher
from aiogram.types import ErrorEvent
ADMIN_ID = int(os.environ["ADMIN_ID"])
@dp.errors()
async def xatoni_ushla(event: ErrorEvent, bot: Bot):
# log'ga to'liq stek-iz yozamiz
logging.exception("Handler'da xato: %s", event.exception)
# JONLI: admin'ga qisqacha xabar (illustrativ β token+internet kerak)
await bot.send_message(ADMIN_ID, f"Botda xato: {type(event.exception).__name__}")
return True # xato "ushlandi" deb belgilanadi, bot yiqilmaydi
Yuqoridagi
bot.send_message(...)jonli botda admin'ga xato xabarini yuboradi. Offline (tokensiz) testda bu satr Telegram'ga so'rov yuborishga uringani uchun ishlamaydi β biz@dp.errorshandler'ining ro'yxatga olinishini valogging.exceptionqismini tekshirdik; xabar yuborish esa "illustrativ β token+internet kerak".
Katta loyihalarda log'larni Sentry kabi xizmatga jo'natish ham keng tarqalgan (xatolarni guruhlash, stek-iz, ogohlantirish). Bu illustrativ β tashqi xizmat va kalit talab qiladi:
# illustrativ β sentry hisobi va DSN kaliti kerak
import sentry_sdk
sentry_sdk.init(dsn=os.environ["SENTRY_DSN"], traces_sample_rate=0.0)
8. Botni doimiy ushlash: systemd¶
VPS'da botni doim ishlatib turish va yiqilsa avtomatik qayta yoqishning standart yo'li β systemd (Linux'ning xizmat menejeri). Siz bitta "unit fayl" yozasiz, systemd qolganini qiladi.
/etc/systemd/system/mening-botim.service (illustrativ β Linux server talab qiladi):
[Unit]
Description=Mening Telegram botim
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/mening-botim
EnvironmentFile=/home/botuser/mening-botim/.env
ExecStart=/home/botuser/mening-botim/.venv/bin/python -m bot
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Eng muhim qatorlar:
Restart=alwaysβ bot qanday sababdan yiqilsa ham, systemd uni qayta yoqadi.RestartSec=5β qayta yoqishdan oldin 5 soniya kutadi (cheksiz tez sikldan saqlaydi).EnvironmentFile=...β.envni avtomatik muhitga yuklaydi (bu yerdaload_dotenv()ham kerak emas, lekin zarar qilmaydi).After=network-online.targetβ internet ulangach yoqiladi.
Boshqarish buyruqlari (illustrativ β Linux server talab qiladi):
sudo systemctl daemon-reload # unit faylni qayta o'qish
sudo systemctl enable mening-botim # server qayta yuklanganda avtoyoqish
sudo systemctl start mening-botim # hozir yoqish
sudo systemctl status mening-botim # holatni ko'rish
sudo systemctl restart mening-botim # deploy'dan keyin qayta yoqish
sudo systemctl stop mening-botim # SIGTERM yuboradi -> graceful shutdown
sudo journalctl -u mening-botim -f # loglarni jonli kuzatish
systemctl stop aynan SIGTERM yuboradi β shuning uchun 6-bo'limdagi graceful shutdown ishlaydi. Deploy oqimi odatda shunday (illustrativ):
cd /home/botuser/mening-botim
git pull
.venv/bin/pip install -r requirements.txt
sudo systemctl restart mening-botim # eski instans graceful to'xtaydi, yangisi yonadi
supervisor β muqobil. Ba'zi serverlarda systemd o'rniga supervisor ishlatiladi (ayniqsa Docker'siz, eski tizimlarda). Konsepsiya bir xil: konfiguratsiya faylida buyruq,
autorestart=true, log yo'li ko'rsatiladi. Illustrativsupervisorkonfiguratsiyasi:
; /etc/supervisor/conf.d/mening-botim.conf (illustrativ)
[program:mening-botim]
command=/home/botuser/mening-botim/.venv/bin/python -m bot
directory=/home/botuser/mening-botim
autostart=true
autorestart=true
stopsignal=TERM
stdout_logfile=/var/log/mening-botim.log
stopsignal=TERM β supervisor ham graceful uchun SIGTERM yuboradi.
9. Docker β konteynerda paketlash¶
Docker kodingizni, Python versiyasini, barcha paketlarni va sozlamalarni bitta konteyner ichiga joylaydi. "Mening kompyuterimda ishlayapti, serverda esa yo'q" muammosi yo'qoladi β konteyner hamma joyda bir xil ishlaydi.
Dockerfile (illustrativ β Docker o'rnatilgan server talab qiladi):
# Yengil rasmiy Python image (versiya qotirilgan)
FROM python:3.14-slim
# logni darhol chiqarish uchun (buffer'siz) + .pyc yozmaslik
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Avval faqat requirements'ni nusxalaymiz β Docker cache'dan foydalanish uchun
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Endi qolgan kodni nusxalaymiz
COPY . .
# Botni modul sifatida ishga tushiramiz (signal'lar to'g'ri yetib borishi uchun exec-shakl)
CMD ["python", "-m", "bot"]
Muhim nuqtalar:
python:3.14-slimβ kichik, lekin to'liq Python image. Versiyani qotiramiz (latestemas).- requirements'ni alohida nusxalash β kod o'zgarganda paketlar qayta o'rnatilmaydi (Docker layer cache). Bu build'ni tezlashtiradi.
CMD ["python", "-m", "bot"](JSON/exec shakl) β bu shakldaSIGTERMto'g'ridan Python'ga yetadi, ya'ni graceful shutdown ishlaydi. Shell shakli (CMD python -m bot) signal'ni yutib qo'yishi mumkin.
.dockerignore (keraksiz fayllarni image'ga kiritmaslik):
.envni image'ga QO'SHMANG. Tokenni image ichiga "pishirib" qo'ymang (image boshqalarga tarqalishi mumkin). O'rniga konteyner ishga tushganda muhit o'zgaruvchisi sifatida bering (docker run --env-file .env ...yokidocker composedaenv_file).
Build va run (illustrativ β Docker talab qiladi):
docker build -t mening-botim .
docker run -d --name botim --restart=always --env-file .env mening-botim
docker logs -f botim # loglarni kuzatish
docker stop botim # SIGTERM -> graceful shutdown (10s kutadi, keyin SIGKILL)
--restart=always β Docker darajasidagi avto-qayta-yoqish (systemd'ning Restart=always ekvivalenti). docker stop 10 soniya graceful kutadi, shuning uchun shutdown hook'laringiz ishlaydi.
docker-compose β bot + Redis + DB birga¶
Bir nechta xizmatni (bot, Redis, DB) birga boshqarish uchun docker-compose.yml qulay (illustrativ):
# docker-compose.yml (illustrativ β Docker talab qiladi)
services:
bot:
build: .
restart: always
env_file: .env
depends_on:
- redis
redis:
image: redis:7-alpine
restart: always
volumes:
- redis-data:/data
volumes:
redis-data:
docker compose up -d β hammasini bir buyruq bilan yoqadi; depends_on bot Redis'dan keyin yonishini ta'minlaydi.
10. Avtomatik deploy (CI/CD) va best-practice'lar¶
Har safar qo'lda SSH bilan kirib git pull qilish o'rniga, CI/CD (Continuous Deployment) ni sozlash mumkin: GitHub'ga git push qilasiz β testlar ishlaydi, keyin avtomatik serverga deploy bo'ladi. Bu GitHub Actions bilan qilinadi.
CI/CD'ni to'liq o'rganish uchun GitHub Actions bobi ga qarang. Bot uchun oddiy workflow g'oyasi (illustrativ β GitHub repo va server kerak):
# .github/workflows/deploy.yml (illustrativ)
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1) testlarni ishga tushirish (16-bob)
# 2) SSH orqali serverga ulanib: git pull + systemctl restart
Sirlar (token, server kaliti) GitHub'ning Secrets bo'limida saqlanadi β hech qachon workflow fayliga yozilmaydi.
Production best-practice'lar yig'masi¶
aiogram botni production'ga chiqarishda eslab qoling:
- Token =
.env/sir. Hech qachon kodga, Git'ga, log'ga..envβ.gitignoreda. - Default
parse_modeni bir marta o'rnating βBot(default=DefaultBotProperties(parse_mode=ParseMode.HTML)). Biz buni tekshirdik:bot.default.parse_mode == ParseMode.HTML. allowed_updates=dp.resolve_used_update_types()β keraksiz update'larni kesing.- Bitta token = bitta polling instans. Deploy'da eski instansni avval to'xtating (systemd
restartbuni o'zi qiladi). - Polling boshida
delete_webhook(drop_pending_updates=True)β eski webhook va to'planib qolgan xabarlarni tozalang. - Graceful shutdown β
shutdownhook'da DB/Redis yoping;SIGTERMishlating (kill -9emas). Restart=always(systemd yoki Docker) β bot yiqilsa o'zi qaytadi.- FSM holatini Redis'da saqlang (production),
MemoryStoragefaqat rivojlash uchun. @dp.errors()bilan kutilmagan xatoni ushlang va admin'ga/log'ga yetkazing.- Versiyalarni qotiring (
requirements.txtda==), Docker image versiyasini ham. - Network xatosida aiogram o'zi qayta urinadi β
start_pollingulanish uzilsa qayta ulanadi; siz qo'ldawhile Trueloop yozishingiz shart emas.
aiogram'ni Node.js (grammY/Telegraf) bilan solishtirmoqchi bo'lsangiz, Node.js bobi dagi deploy yondashuvi shunga o'xshash: konteyner + process menejer (PM2) + sirlar muhit o'zgaruvchisida. Asosiy konsepsiyalar til-agnostik.
Mashqlar¶
Eslatma: quyidagi mashqlarning kod qismlari offline (tokensiz) ishlaydi β
feed_update, lifecycle hook,.envo'qish, konfiguratsiya. VPS/Docker/systemd qismlari illustrativ β ularni faqat tahlil qiling yoki sintaksisini tekshiring, real ishga tushirish server talab qiladi.
Oson¶
.envfaylidanBOT_TOKENni o'qiydiganget_token()funksiyasini yozing: token bo'lsa qaytarsin, bo'lmasa aniqRuntimeErrorko'tarsin.os.environbilan ikkala holatni tekshiring (tokensiz).requirements.txtfaylini yozing: aiogram 3.28.2, aiohttp 3.13.5, python-dotenv ni versiyalari qotirilgan holda. Nega==ishlatamiz, bir-ikki gap bilan tushuntiring..gitignorega production bot uchun qaysi fayllar/papkalar tushishi kerakligini ro'yxat qiling va har birining sababini yozing.- Dispatcher'ga
startupvashutdownhook ulang (registerbilan). Offlineemit_startup/emit_shutdownchaqirib, ikkalasi ham ishlaganini tekshiring (ro'yxatga voqea qo'shib). - Bir handler bo'lgan dispatcher uchun
dp.resolve_used_update_types()nimani qaytarishini bashorat qiling, keyin offline tekshirib o'zingizni sinang. - Quyidagi
Dockerfileqatorida xato bor:CMD python -m bot. Nega bu graceful shutdown uchun yomon va to'g'ri shakli qanday?
O'rta¶
- Polling botning to'liq
main()skeletini yozing:Bot(default=DefaultBotProperties(parse_mode=ParseMode.HTML)),delete_webhook(drop_pending_updates=True),start_polling(allowed_updates=...), vafinallydabot.session.close(). Offline defaultparse_modeo'rnatilganiniassertbilan tekshiring. @dp.errors()xatolik handler'ini yozing: ulogging.exception(...)bilan log yozsin vaTrueqaytarsin. Handler ro'yxatga olinganini offline tekshiring (realsend_messagesiz).- systemd unit fayl yozing va quyidagi talablarni bajaring: yiqilsa qayta yonadi, 10 soniya kutib qayta yonadi,
.envni avtomatik yuklaydi, internet ulangach yonadi. Har qatorni izohlang. - Webhook uchun aiohttp ilovasini quring:
SimpleRequestHandlernisecret_tokenbilan ro'yxatga oling,setup_applicationchaqiring. Offline route haqiqatan qo'shilganini tekshiring (web.run_appchaqirmasdan). MemoryStoragevaRedisStorageorasidagi farqni production nuqtai nazaridan tushuntiring: bot qayta yonganda nima bo'ladi? Qaysi birini qachon tanlaysiz?docker-compose.ymlyozing:botvaredisxizmatlari, ikkalasi hamrestart: always, bot Redis'dan keyin yonsin, Redis ma'lumotivolumeda saqlansin.
Qiyin¶
- To'liq graceful polling botini quring:
aiosqliteulanishinistartupda ochib,shutdownda yoping. Offline:emit_startup-> mock/startupdate'nifeed_update->emit_shutdownketma-ketligini ishga tushirib, ulanish ochildi va yopildi ekaniniassertbilan isbotlang (DB yopilgach so'rov xato berishi orqali). - "Bitta token = bitta polling instans" qoidasini hujjat bilan tushuntiring: ikkita instans yonsa qanday xato (
TelegramConflictError) chiqadi, deploy paytida buni qanday oldini olasiz (eski instansni avval to'xtatish)? systemdrestartbuni qanday hal qiladi? (Tahliliy mashq β kod ishga tushirish shart emas, lekin aiogram istisno klassini import qilib mavjudligini tekshiring.)
Yechimlar
1. Token o'qish β bor va yo'q holatlari (offline tekshirilgan):
import os
def get_token() -> str:
token = os.environ.get("BOT_TOKEN")
if not token:
raise RuntimeError("BOT_TOKEN topilmadi! .env faylini tekshiring.")
return token
# tekshiruv
os.environ["BOT_TOKEN"] = "123456:AAH-FakeTest_abc"
assert get_token() == "123456:AAH-FakeTest_abc"
del os.environ["BOT_TOKEN"]
try:
get_token()
assert False, "xato kutilgandi"
except RuntimeError as e:
print("to'g'ri xato:", e) # -> BOT_TOKEN topilmadi! ...
os.environ.get(...) None qaytarsa, jim davom etmaymiz β aniq xato ko'taramiz. Bu "konfiguratsiya yo'q" muammosini ishga tushishda darhol ushlaydi.
2. Versiyalar qotirilgan requirements.txt:
== ishlatamiz, chunki reproducible (qayta-tiklanuvchan) build kerak: bugun lokal kompyuterda ishlagan kod ertaga serverda aynan o'sha paket versiyalari bilan ishlasin. >= ishlatsangiz, kelajakda chiqqan yangi (mos kelmaydigan) versiya o'rnatilib, bot kutilmaganda buzilishi mumkin β ayniqsa aiogram 2.x -> 3.x kabi katta o'zgarishlarda halokatli.
3. Production .gitignore:
.env # sirlar (token, parollar) β hech qachon Git'ga emas
.venv/ # virtual muhit β har serverda qaytadan quriladi, og'ir va platformaga bog'liq
__pycache__/ # Python kesh fayllari β kerakmas, avtomatik qayta yaratiladi
*.pyc # kompilyatsiya qilingan bayt-kod β kerakmas
*.db # lokal SQLite baza β ma'lumot, kodga aralashmasligi kerak
*.log # log fayllari β server tomonda yig'iladi, repo'ni shishiradi
Eng muhimi .env β token tasodifan ham GitHub'ga tushmasligi uchun.
4. Lifecycle hook (offline tekshirilgan):
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
audit = []
async def on_startup(bot: Bot):
audit.append("startup")
async def on_shutdown(bot: Bot):
audit.append("shutdown")
async def main():
bot = Bot("123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
await dp.emit_startup(bot=bot)
await dp.emit_shutdown(bot=bot)
await bot.session.close()
print(audit) # -> ['startup', 'shutdown']
assert audit == ["startup", "shutdown"]
asyncio.run(main())
5. Bashorat: faqat @router.message(...) bo'lsa ['message']; agar callback_query handler ham bo'lsa ['callback_query', 'message']. Tekshirish:
from aiogram import Dispatcher, Router
from aiogram.filters import Command
from aiogram.types import Message
r = Router()
@r.message(Command("ping"))
async def ping(m: Message): ...
dp = Dispatcher()
dp.include_router(r)
print(dp.resolve_used_update_types()) # -> ['message']
assert dp.resolve_used_update_types() == ["message"]
Handler turini qo'shgan sayin ro'yxat o'sadi β bu aynan allowed_updates ga beriladigan qiymat.
6. CMD python -m bot β shell shakli. Bu Docker'da /bin/sh -c "python -m bot" ga aylanadi, ya'ni PID 1 β sh, Python esa uning bolasi. docker stop SIGTERM ni PID 1 (sh) ga yuboradi, lekin sh uni Python'ga uzatmasligi mumkin β natijada graceful shutdown ishlamaydi va 10 soniyadan keyin SIGKILL bilan shartta o'ladi. To'g'ri shakli β exec (JSON) shakli:
Bu shaklda Python to'g'ridan PID 1 bo'ladi, SIGTERM unga yetadi, start_polling uni ushlab graceful to'xtaydi.
7. Polling main() skeleti (offline parse_mode tekshirildi):
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
async def build():
bot = Bot("123456:AAH-FakeTest_abc",
default=DefaultBotProperties(parse_mode=ParseMode.HTML))
assert bot.default.parse_mode == ParseMode.HTML
dp = Dispatcher(storage=MemoryStorage())
await bot.session.close()
print("OK: default parse_mode =", bot.default.parse_mode)
return bot, dp
asyncio.run(build())
# To'liq skelet (jonli β token+internet kerak, illustrativ):
async def main():
bot = Bot(get_token(), default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher(storage=MemoryStorage())
# dp.include_router(...)
await bot.delete_webhook(drop_pending_updates=True)
try:
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
finally:
await bot.session.close()
build() qismi offline o'tdi: bot.default.parse_mode haqiqatan ParseMode.HTML. main() ning start_polling qismi jonli Telegram talab qiladi (illustrativ).
8. Xatolik handler (ro'yxatga olinishi offline tekshirildi):
import logging
from aiogram import Bot, Dispatcher
from aiogram.types import ErrorEvent
dp = Dispatcher()
@dp.errors()
async def xatoni_ushla(event: ErrorEvent, bot: Bot):
logging.exception("Handler'da xato: %s", event.exception)
# JONLI (illustrativ): await bot.send_message(ADMIN_ID, "...")
return True
# handler ro'yxatga olinganini tekshiramiz (real xato yuborishsiz):
assert len(dp.errors.handlers) == 1
print("OK: xatolik handler ro'yxatga olindi")
return True β xato "ushlandi" deb belgilanadi va bot ishlashda davom etadi (yiqilmaydi). send_message jonli yuborish β illustrativ.
9. systemd unit fayl (illustrativ):
[Unit]
Description=Mening Telegram botim
After=network-online.target # internet tayyor bo'lgach yoqiladi
Wants=network-online.target
[Service]
Type=simple
User=botuser # root'da emas, alohida foydalanuvchida (xavfsizroq)
WorkingDirectory=/home/botuser/mening-botim
EnvironmentFile=/home/botuser/mening-botim/.env # .env ni avtomatik yuklaydi
ExecStart=/home/botuser/mening-botim/.venv/bin/python -m bot
Restart=always # yiqilsa har doim qayta yonadi
RestartSec=10 # qayta yonishdan oldin 10 soniya kutadi
[Install]
WantedBy=multi-user.target
Restart=always + RestartSec=10 β talab qilingan "yiqilsa 10 soniyada qayta yonish". EnvironmentFile β .env ni yuklash. After/Wants=network-online.target β internet ulangach yonish.
10. Webhook aiohttp ilovasi (route qo'shilishi offline tekshirildi):
from aiohttp import web
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
WEBHOOK_PATH = "/webhook/abc-secret-path"
WEBHOOK_SECRET = "my-super-secret-token"
bot = Bot("123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
app = web.Application()
SimpleRequestHandler(dispatcher=dp, bot=bot, secret_token=WEBHOOK_SECRET).register(app, path=WEBHOOK_PATH)
setup_application(app, dp, bot=bot)
paths = [getattr(r.resource, "canonical", None) for r in app.router.routes()]
assert WEBHOOK_PATH in paths
print("OK: route qo'shildi ->", paths)
# web.run_app(...) CHAQIRMAYMIZ β u real port va public URL talab qiladi (illustrativ)
secret_token Telegram'dan kelgan X-Telegram-Bot-Api-Secret-Token sarlavhasini tekshirib, soxta so'rovlarni rad etadi.
11. MemoryStorage holatni jarayon xotirasida saqlaydi β bot qayta yonganda (deploy, yiqilish, server restart) hammasi yo'qoladi: foydalanuvchilar to'ldirayotgan formalar, FSM holatlari nolga tushadi. RedisStorage esa holatni tashqi Redis serverida saqlaydi β bot qayta yonsa ham foydalanuvchi qaysi qadamda turgani saqlanib qoladi. Tanlov: rivojlash/test va holat yo'qolishi muhim bo'lmagan oddiy botlar uchun MemoryStorage; foydalanuvchi uzun forma/dialog to'ldiradigan, yoki bir nechta instansli production bot uchun RedisStorage. pip install "aiogram[redis]" kerak; RedisStorage.from_url(REDIS_URL) bilan ulanasiz.
12. docker-compose (illustrativ):
services:
bot:
build: .
restart: always
env_file: .env
depends_on:
- redis # bot Redis'dan keyin yonadi
redis:
image: redis:7-alpine
restart: always
volumes:
- redis-data:/data # ma'lumot konteyner o'chsa ham saqlanadi
volumes:
redis-data:
depends_on ishga tushish tartibini beradi; volume Redis ma'lumotini konteyner qayta yaratilsa ham saqlaydi.
13. Graceful DB ochish/yopish, yopilganini isbotlash (offline tekshirilgan):
import asyncio
from datetime import datetime
import aiosqlite
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message, Update, Chat, User
router = Router()
holat = {}
@router.message(CommandStart())
async def start(message: Message):
holat["handler_ishladi"] = True
async def main():
bot = Bot("123456:AAH-FakeTest_abc")
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
db = await aiosqlite.connect(":memory:")
await db.execute("CREATE TABLE t(x INTEGER)")
await db.commit()
holat["db"] = db
async def on_startup(bot: Bot):
holat["startup"] = True
async def on_shutdown(bot: Bot):
await db.close()
holat["shutdown"] = True
dp.startup.register(on_startup)
dp.shutdown.register(on_shutdown)
await dp.emit_startup(bot=bot)
msg = Message(message_id=1, date=datetime.now(),
chat=Chat(id=1, type="private"),
from_user=User(id=1, is_bot=False, first_name="T"), text="/start")
await dp.feed_update(bot, Update(update_id=1, message=msg))
await dp.emit_shutdown(bot=bot)
await bot.session.close()
assert holat.get("startup") and holat.get("handler_ishladi") and holat.get("shutdown")
# DB yopilganini isbotlaymiz: yopilgan ulanishda so'rov xato beradi
try:
await db.execute("SELECT 1")
assert False, "DB ochiq qolgan!"
except Exception as e:
print("DB yopilgan (kutilgan xato):", type(e).__name__)
print("OK: startup -> handler -> shutdown, DB yopildi")
asyncio.run(main())
Bu yerda shutdown hook db.close() ni chaqiradi; keyin yopilgan ulanishda SELECT 1 xato berishi DB haqiqatan yopilganini isbotlaydi.
14. "Bitta token = bitta polling instans": Telegram bir token uchun bir vaqtda faqat bitta getUpdates so'rov-oqimiga ruxsat beradi. Ikkita instans yonsa, ular bir-birining update'ini tortib oladi va Telegram 409 Conflict qaytaradi β aiogram buni TelegramConflictError istisno sifatida ko'taradi:
from aiogram.exceptions import TelegramConflictError
assert issubclass(TelegramConflictError, Exception)
print("TelegramConflictError mavjud:", TelegramConflictError.__name__)
(Bu import offline o'tadi β istisno klassi aiogram'da bor.) Deploy paytida oldini olish: yangi instansni yoqishdan oldin eskisini to'xtating. systemd systemctl restart buni avtomatik qiladi β u avval eski jarayonga SIGTERM yuborib, u graceful to'xtaguncha (yoki TimeoutStopSec tugaguncha) kutadi, keyin yangisini yoqadi. Shu sababli ikkita instans hech qachon bir vaqtda polling qilmaydi. Docker'da ham docker stop (eski) -> docker run (yangi) yoki docker compose up -d (recreate) shu tartibni saqlaydi. Webhook'da bu muammo yo'q (Telegram bitta URL'ga yuboradi), lekin u yerda ham eski webhook'ni yangisiga almashtirish atomik bo'ladi.
β¬ οΈ Oldingi: 16 β Testlash va xatolarni boshqarish Β· π README Β· Keyingi: 18 β Yakuniy kapston: to'liq bot β‘οΈ