You wrote a Telegram bot with aiogram, tested it locally, and it works. Now it has to live somewhere that doesn't sleep when you close the laptop. A small VPS is the natural home — but there's a gap between python bot.py in an SSH session and a bot that actually stays up through crashes, reboots, and your connection dropping.
This is the production version: a virtualenv, the token kept out of your code, a systemd service that resurrects the bot on its own, and a clear-eyed answer to the question everyone eventually asks — polling or webhook?
Why not just run it on your laptop
A bot needs a steady outbound connection to Telegram. Your laptop sleeps, reboots for updates, and hops between networks — every one of those drops the bot, and users hit a wall of silence. A VPS holds that connection around the clock. That's the whole reason to move it off your machine, and it's the same reason a Discord bot belongs on a server too.
If you just want the fastest possible "get it online" path with a minimal bot, the host-a-Telegram-bot walkthrough covers that. This piece goes a level deeper: aiogram, safe token handling, and knowing when to scale.
The bot, in a virtualenv
SSH in and keep the bot isolated in its own venv — don't install packages system-wide, it makes upgrades and cleanup a mess later:
sudo apt update && sudo apt install -y python3-venv
mkdir ~/tgbot && cd ~/tgbot
python3 -m venv venv && source venv/bin/activate
pip install -U aiogram
A minimal aiogram 3 bot that reads its token from the environment, not from a hard-coded string:
# bot.py
import asyncio, logging, os
from aiogram import Bot, Dispatcher
from aiogram.types import Message
from aiogram.filters import CommandStart
logging.basicConfig(level=logging.INFO)
dp = Dispatcher()
@dp.message(CommandStart())
async def start(m: Message):
await m.answer("Alive and running on a VPS.")
async def main():
bot = Bot(os.environ["BOT_TOKEN"])
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Keep the token out of your code
Never paste the BotFather token into bot.py — one push to a public repo and it's leaked. Put it in a root-readable env file instead:
sudo tee /etc/tgbot.env >/dev/null <<'EOF'
BOT_TOKEN=123456:your-token-from-botfather
EOF
sudo chmod 600 /etc/tgbot.env
A leaked token is one /revoke in BotFather away from being fixed — but a leaked anything else on a shared machine is worse. A dedicated VPS for the bot is honestly a safer home for the token than your daily laptop: if it leaks, you rotate one token, not your whole setup.
The part that keeps it alive: systemd
This is what separates a bot that runs from a bot that stays running. Create the service:
# /etc/systemd/system/tgbot.service
[Unit]
Description=Telegram bot (aiogram)
After=network-online.target
Wants=network-online.target
[Service]
User=botuser
WorkingDirectory=/home/botuser/tgbot
EnvironmentFile=/etc/tgbot.env
ExecStart=/home/botuser/tgbot/venv/bin/python bot.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Run it as a non-root user (botuser above), not root — if the bot is ever compromised, you want it boxed in. Then:
sudo systemctl daemon-reload
sudo systemctl enable --now tgbot
Restart=always with RestartSec=3 means a crash — a bad update from Telegram, an unhandled exception, an OOM — brings the bot back in three seconds instead of leaving it dead until you notice. Watch it live:
journalctl -u tgbot -f
That's your whole logging setup. No log files to rotate, no extra tooling — journald already has it.
Updating without downtime drama
When you change the code or bump aiogram:
cd ~/tgbot && source venv/bin/activate
pip install -U aiogram # if upgrading the library
sudo systemctl restart tgbot
journalctl -u tgbot -n 30 --no-pager # confirm it came back clean
The restart blips the bot for a second or two. For a polling bot that's invisible to users — Telegram queues updates and delivers them once the bot reconnects.
Polling vs webhook — the honest version
This is the decision people overthink. Here's the plain version:
Polling (start_polling, what the code above uses) has the bot ask Telegram "anything new?" on a long-lived connection. It needs nothing but outbound internet — no domain, no TLS, no open inbound ports. It runs fine behind NAT. For the overwhelming majority of bots, this is correct and you should stop here.
Webhook has Telegram push updates to you, which means you must expose a public HTTPS endpoint. That requires a domain and a reachable inbound port — so either a dedicated public IP or a reverse proxy in front. More setup, more things that break. The payoff is lower latency and less overhead at large scale — thousands of concurrent users, heavy update volume.
The practical rule: start with polling on a NAT plan. Move to a webhook only when you've actually outgrown polling — and if you do, that's when a dedicated IP earns its place, because you need that inbound HTTPS endpoint.
What it costs to run
A polling bot is light. It idles on Telegram's long-poll and reacts to messages, so the box mostly waits:
- $3 Nano (1 vCPU / 1 GB) — fine for most bots, even fairly chatty ones.
- $5 Micro (2 vCPU / 2 GB) — when the bot keeps a database (SQLite/Postgres), handles media, or serves a lot of users.
- Higher only if the bot itself does real work — image processing, a local model, heavy per-message logic.
Signup is email-only and you pay in USDC or USDT on Base or Ethereum — no card, no ID. A card works too, but the on-ramp has a ~$27 minimum, so for a $3 plan it's smoother to top up a small balance once and let renewals draw from it. It's CPU-only, one datacenter in Germany — a non-issue for a bot, worth knowing if you needed a GPU or a specific region.
The honest bottom line
A Telegram bot is one of the cheapest things you can self-host: a $3 box, a systemd unit, and polling gets you a bot that's online through crashes and reboots without you babysitting it. Reach for a webhook and a bigger plan only when scale actually forces it — not because a tutorial told you webhooks are "better." Get the small box, keep the token in an env file, let systemd handle the uptime, and — before anything else — run the new-VPS security checklist so the box itself is locked down.