Add touch telegram_session.session to setup instructions and clarify that the session file must exist (even empty) before starting the container.
Telegram Backup Downloader v5
Downloads text, photos, videos, and documents from configured Telegram channels. Message text is stored in messages.txt and JSONL metadata; media is sorted into per-channel folders. SQLite tracks completed work, failed downloads, and sync progress.
Platform support: Docker images are built for
linux/amd64andlinux/arm64only. Legacy 32-bit ARM (arm/v7) is not supported because thecryptgTelethon extension has no prebuilt wheel for it. This covers servers, desktops, NAS devices, Raspberry Pi 4/5, and Apple Silicon via emulation.
Prerequisites
- Python 3.12 or Docker
- A Telegram API ID and API hash from my.telegram.org
- A Telegram account that can access every configured channel
Local Quick Start
- Create a virtual environment and install dependencies:
python -m venv .venvthen.venv\Scripts\pip install -r requirements.txton Windows, or.venv/bin/pip install -r requirements.txton Linux/macOS. - Copy
config.example.jsontoconfig.jsonand enter your Telegram values. - Run
python downloadv5.py. The first run requests Telegram login verification if no session file exists.
Backups are written to channels/; logs are written to logs/app.log. Stop gracefully with Ctrl+C.
Docker
Option A: Docker Compose (recommended)
The example docker-compose.yml uses the pre-built image from Docker Hub and supports linux/amd64 and linux/arm64.
Before running, create the folders and config file so the container can write to them. The compose file mounts local paths for config, session, data, channels, and logs:
mkdir -p data channels logs
touch telegram_session.session
cp config.example.json config.json
# edit config.json with your Telegram values
Important: The
telegram_session.sessionfile must exist (even empty) before the container starts. If the container can't find the session file, it will fail to start. Create an empty file as shown above.
On Linux, if the container runs as a non-root user, make sure the current user has read+write access to these folders (the example compose runs as root).
Then start the stack:
docker compose up -d
docker compose logs -f telegram-backup
First-time login: After the initial
docker compose up -d, the container will start but won't be logged in yet. You need to authenticate interactively first:# Stop the detached container docker compose down # Run interactively — you'll be prompted for phone number + verification code # (and password if you have 2FA enabled) docker compose up # After successful login, press Ctrl+C to stop docker compose down # Now start detached — session is saved docker compose up -dThe session file is written to
./telegram_session.sessionon the host, so subsequent starts won't ask for verification again (unless it expires or you delete the file).
This is the docker-compose.yml:
version: "3.9"
services:
telegram-backup:
image: hoelee/telegram-backup-downloader:latest
container_name: telegram-backup
restart: unless-stopped
user: root
volumes:
- ./config.json:/app/config.json
- ./telegram_session.session:/app/telegram_session.session
- ./data:/app/data
- ./channels:/app/channels
- ./logs:/app/logs
ports:
- "8080:8080"
environment:
- TZ=Asia/Kuala_Lumpur
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=3)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
Set status_port to 8080 in config.json to use the health check and expose the status API.
Option B: docker run
mkdir -p data channels logs
touch telegram_session.session
cp config.example.json config.json
docker run -d \
--name telegram-backup \
--restart unless-stopped \
-v $(pwd)/config.json:/app/config.json \
-v $(pwd)/telegram_session.session:/app/telegram_session.session \
-v $(pwd)/data:/app/data \
-v $(pwd)/channels:/app/channels \
-v $(pwd)/logs:/app/logs \
-p 8080:8080 \
-e TZ=Asia/Kuala_Lumpur \
hoelee/telegram-backup-downloader:latest
Important: The
telegram_session.sessionfile must exist (even empty) before the container starts. Create an empty file as shown above.
Follow logs with docker logs -f telegram-backup.
First-time login: After the initial
docker run -d, the container will start but won't be logged in yet. You need to authenticate interactively first:# Stop and remove the detached container docker stop telegram-backup && docker rm telegram-backup # Run interactively — you'll be prompted for phone number + verification code # (and password if you have 2FA enabled) docker run -it --rm \ -v $(pwd)/config.json:/app/config.json \ -v $(pwd)/telegram_session.session:/app/telegram_session.session \ -v $(pwd)/data:/app/data \ -v $(pwd)/channels:/app/channels \ -v $(pwd)/logs:/app/logs \ -p 8080:8080 \ -e TZ=Asia/Kuala_Lumpur \ hoelee/telegram-backup-downloader:latest # After successful login, press Ctrl+C to exit # Now run detached — session is saved to ./telegram_session.sessionThe session file is written to
./telegram_session.sessionon the host, so subsequent starts won't ask for verification again (unless it expires or you delete the file).
Option C: Build from source
cp config.example.json config.json
docker compose -f docker-compose.yml up -d --build
docker compose logs -f telegram-backup
Configuration
| Key | Required | Default | Description |
|---|---|---|---|
api_id |
Yes | - | Numeric Telegram API ID. |
api_hash |
Yes | - | Telegram API hash. |
phone_number |
Yes | - | Account phone number in international format. |
session_name |
Yes | - | Telethon session basename. |
channels |
Yes | - | Channel usernames or numeric peer IDs. |
parallel_downloads |
No | 3 |
Number of media workers. |
download_timeout_seconds |
No | 600 |
Per-download timeout. |
download_retry_count |
No | 3 |
Attempts made in one processing pass. |
max_lifetime_retries |
No | 20 |
Total failed passes before an item is dropped; 0 disables the cap. |
queue_max_size |
No | 5000 |
Normal download queue capacity. |
min_disk_space_gb |
No | 6 |
Pause media downloads below this free space. |
channel_auto_disable_after |
No | 5 |
Consecutive resolution failures before disabling a channel; 0 disables this. |
media_record_ttl_days |
No | 90 |
Retention for completed media DB records; 0 disables pruning. |
retry_drop_log |
No | true |
Write discarded downloads to logs/dropped_downloads.jsonl. |
resync_interval_minutes |
No | 60 |
Periodic backfill interval; 0 disables it. |
status_port |
No | 0 |
HTTP port; 0 disables the server. |
db_path |
No | telegram_state.db |
SQLite state database path. |
channel_overrides |
No | {} |
Per-channel controls. Only turnon is supported. Use turnon: false to skip a channel without removing it from channels. Example: {"-100321012345":{"turnon":false}}. Change is detected by config watcher — no restart needed. |
channel_last_message_id_overrides |
No | {} |
One-time per-channel sync cursor overrides. With updateonce: true, sets the channel's SQLite last_message_id to the specified value, then automatically changes updateonce to false in config.json. Use numeric Telegram peer IDs that are included in channels. |
manual_downloads |
No | {} |
Message IDs to prioritize, keyed by channel ID. Useful to force-retry specific messages. Example: {"-1001":[42,43,44]}. Change is detected by config watcher — no restart needed. |
config.json must be mounted read-write. The program writes back to it directly — for example, when channel_last_message_id_overrides runs with updateonce: true, the downloader updates the database and then flips updateonce to false in the same file. A read-only mount (:ro) would break this and any other live config update.
config.json is watched every 10 seconds. Changes to channels, overrides, cursor overrides, and manual downloads are applied without restarting. Do not set status_port, db_path, worker count, or API credentials expecting a live process to rebind/recreate those resources; restart after changing them.
One-Time Sync Cursor Override
Use channel_last_message_id_overrides to resume a channel from a known message ID or re-download messages after a cursor correction. The channel must be present in channels. When the running downloader detects updateonce: true, it updates the database cursor and writes updateonce: false only after the database change succeeds.
"channel_last_message_id_overrides": {
"-1004295572354": {
"updateonce": true,
"last_message_id": 21321
}
}
The next sync starts from a small overlap before that cursor, so existing message and media records prevent duplicate output.
HTTP API
The API listens on 0.0.0.0:<status_port> with no authentication. Only expose it on a trusted network.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Returns {"status":"ok"}. |
| GET | /status |
Connection, queue, workers, disk, database, failure, and channel status. |
| GET | /logs?lines=100 |
Last 1-5000 lines of logs/app.log. |
| POST | /reload |
Reloads config.json. |
| POST | /db/cleanup |
Prunes expired records and vacuums SQLite. |
| POST | /channel/{id}/enable |
Enables a channel override and resolves channels. |
| POST | /channel/{id}/disable |
Disables a channel override. |
Troubleshooting
- Login problems: delete only the session file if you intentionally need to authenticate again, then restart.
- No downloads: verify the account belongs to or can view the channel, and confirm the channel override is enabled.
- Downloads paused: inspect
/statusor the log for disk-space warnings. Downloads resume automatically after space is recovered. - Repeated failed media: inspect
logs/app.log; items reaching the lifetime cap are recorded inlogs/dropped_downloads.jsonlwhen enabled. - Port unavailable: set
status_portto another free port, update compose port mapping if needed, and restart. - Database location in Docker: set
db_pathtodata/telegram_state.dbif you want the database in the mounteddatadirectory.