aiogram is one of the most popular Python libraries for Telegram bots: asynchronous, organized around routers and filters, with FSM support for multi-step conversations. On a laptop, python bot.py works perfectly until the lid closes. Here we deploy an aiogram 3 ordering bot for a clothing shop in Namangan to a VPS and turn it into an always-on service.
Why a VPS
A polling bot is a long-running Python process. Shared hosting is built mainly for PHP sites and usually limits or kills long-lived background processes, and restarting a bot from cron is not reliable. A small VPS with systemd is the right home; on sayt.uz you can order one from the cabinet.
Preparing the code
Never hard-code the token; read it from the environment. Enable logging, because on a server logs are your only window into errors.
import asyncio, logging, os
from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message: Message):
await message.answer("Hello! Type the product you need.")
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
Pin versions in requirements.txt, for example aiogram>=3.0,<4.0, since versions 2 and 3 differ a lot.
Virtual environment and secrets
sudo apt install -y python3-venv git
sudo adduser --disabled-password bot
sudo -iu bot
git clone https://github.com/misol/shop-bot.git
cd shop-bot
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Put BOT_TOKEN=... in .env, run chmod 600 .env and keep the file in .gitignore.
A systemd service
# /etc/systemd/system/shop-bot.service
[Unit]
Description=Shop Telegram bot (aiogram)
After=network-online.target
Wants=network-online.target
[Service]
User=bot
WorkingDirectory=/home/bot/shop-bot
EnvironmentFile=/home/bot/shop-bot/.env
ExecStart=/home/bot/shop-bot/.venv/bin/python bot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable it with sudo systemctl enable --now shop-bot and watch logs with journalctl -u shop-bot -f. To update: git pull, reinstall requirements if needed, sudo systemctl restart shop-bot. Run only one instance; a forgotten copy on your laptop causes TelegramConflictError.
Keep FSM state across restarts
The default MemoryStorage loses every half-finished order when the service restarts. Use Redis in production, and store the orders themselves in a real database such as PostgreSQL or MySQL.
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://127.0.0.1:6379/0")
dp = Dispatcher(storage=storage)
Switching to a webhook
With more users, or when the bot shares a server with a website, a webhook can be the better fit. aiogram ships an aiohttp integration; Nginx terminates HTTPS and forwards to the internal port, and secret_token rejects forged requests.
from aiohttp import web
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
SECRET = os.environ["WEBHOOK_SECRET"]
async def on_startup(bot: Bot):
await bot.set_webhook("https://bot.example.uz/tg/webhook", secret_token=SECRET)
def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp.startup.register(on_startup)
app = web.Application()
SimpleRequestHandler(dispatcher=dp, bot=bot, secret_token=SECRET).register(app, path="/tg/webhook")
setup_application(app, dp, bot=bot)
web.run_app(app, host="127.0.0.1", port=8081)
Point an Nginx location /tg/webhook at http://127.0.0.1:8081 and get a Let's Encrypt certificate for the subdomain. The systemd unit stays the same; only the entry script changes.