To run a Telegram bot with Docker you put the bot code in an image, pass the token as an environment variable, and start the container with --restart unless-stopped so it comes back after a crash or a reboot. This guide builds a small long-polling bot, runs it in the background, and shows you how to read the logs when Telegram returns an error.
The example uses Python, but the container and restart parts are the same for any language.
Before you start
You need:
- A Linux VPS with root access and Docker installed. On Ubuntu 22.04/24.04,
docker --versionanddocker compose versionshould both print something. - A bot token from @BotFather. Keep it out of your shell history and out of the image.
- A working directory such as
/opt/tgbot.
Warning: Anyone with the token controls the bot. Never commit it to Git or bake it into a Dockerfile with ENV. Pass it at runtime instead.
Create the directory and check Docker is running:
mkdir -p /opt/tgbot && cd /opt/tgbot
systemctl is-active dockerIf that prints inactive, run systemctl enable --now docker.
Write the bot and the Dockerfile
Create /opt/tgbot/bot.py. This bot uses long polling, so it needs no public port and no webhook:
import os
import time
import requests
TOKEN = os.environ["BOT_TOKEN"]
API = f"https://api.telegram.org/bot{TOKEN}"
def send(chat_id, text):
requests.post(f"{API}/sendMessage", json={"chat_id": chat_id, "text": text}, timeout=30)
def main():
offset = 0
while True:
try:
r = requests.get(
f"{API}/getUpdates",
params={"timeout": 30, "offset": offset},
timeout=40,
)
for update in r.json().get("result", []):
offset = update["update_id"] + 1
message = update.get("message") or {}
chat_id = message.get("chat", {}).get("id")
text = (message.get("text") or "").strip()
if chat_id and text:
send(chat_id, f"You said: {text}")
except Exception as exc:
print("poll error:", exc, flush=True)
time.sleep(5)
if __name__ == "__main__":
main()The try/except matters. A network blip should not kill the process, because a container that is still running but no longer polling will not be restarted by Docker.
Now create the Dockerfile:
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir requests
COPY bot.py .
CMD ["python", "-u", "bot.py"]The -u flag keeps stdout unbuffered so docker logs shows output immediately.
Finally, create the environment file and lock it down:
cat > /opt/tgbot/.env <<'EOF'
BOT_TOKEN=123456789:replace-with-your-real-token
EOF
chmod 600 /opt/tgbot/.envBuild the image
Build with a tag you can roll back to later:
cd /opt/tgbot
docker build -t mybot:1 .Expect a few layers to download on the first run. When it finishes you will see naming to docker.io/library/mybot:1. Confirm it exists:
docker images mybotRun the Telegram bot with Docker and auto-restart
Start the container in the background with a restart policy:
docker run -d \
--name mybot \
--restart unless-stopped \
--env-file /opt/tgbot/.env \
mybot:1unless-stopped means Docker restarts the container after a crash and after a host reboot, but leaves it stopped if you stopped it yourself. That is the right default for a bot. If you want it to start even after you stopped it manually, use --restart always instead.
Check the state:
docker ps --filter name=mybot
docker inspect -f '{{.State.Status}} restarts={{.RestartCount}}' mybotYou should see running and a restart count of 0.
Tip: Docker only restarts a container when the main process exits. If your bot hangs without exiting, add a healthcheck or a watchdog inside the app.
Check it works
Open Telegram, find your bot, and send it any message. It should reply with You said: ….
Then watch the logs live:
docker logs -f --tail 100 mybotYou should see no output on a healthy long-polling bot, because it only prints when something goes wrong. Press Ctrl+C to stop following; that does not stop the container.
Test the restart policy properly. Kill the main process and watch Docker bring it back:
docker exec mybot pkill -f bot.py
sleep 5
docker ps --filter name=mybot
docker inspect -f '{{.RestartCount}}' mybotThe container should be running again and the restart count should have increased. To confirm it survives a reboot, restart the VPS and check docker ps once it is back.
Troubleshooting
- Container exits immediately with
KeyError: 'BOT_TOKEN'— the environment file was not passed or the path is wrong. Check withdocker inspect mybot | grep -A5 Env, and make sure you used--env-file /opt/tgbot/.envwith the absolute path. - Logs show
409 Conflict: terminated by other getUpdates request— two processes are polling the same token. Stop the other one, for example an old container (docker rm -f mybot-old) or a systemd service you left running on the host. 401 Unauthorizedin the logs — the token is wrong or was revoked in BotFather. Generate a new one and update.env, then recreate the container.- Bot stops answering but the container is still
running— the polling loop died without exiting the process. Look at the last lines ofdocker logs mybot, fix the unhandled exception, and rebuild. A restart policy cannot help here. - Container does not come back after a reboot — either the policy is
no(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' mybot) or the Docker service is disabled. Runsystemctl enable --now docker.
Next steps
- Docker restart policies: keep containers running after a reboot explains how
unless-stopped,alwaysandon-failurediffer. - Docker run flags you will actually use covers
-d,-e,-vand--restartin one place. - Centralise Docker logs with Loki and Promtail if you want to search logs from several containers at once.
