Automated Telegram backup with Docker. Performs incremental backups of messages and media on a configurable schedule.
This project is developed with AI assistance (Claude Code).
See docs/ROADMAP.mdβ for what's planned, and docs/CHANGELOG.mdβ for complete version history.
Have a feature request? Open an issueβ !
Two separate Docker images are available (v4.0+):
| Image | Purpose | Size |
|---|---|---|
drumsergio/telegram-archive | Backup scheduler (requires Telegram credentials) | ~300MB |
drumsergio/telegram-archive-viewer | Web viewer only (no Telegram client) | ~150MB |
π¦ Upgrading from v3.x? See Upgrading from v3.x to v4.0β for migration instructions.
# Clone the repository
git clone https://github.com/GeiserX/Telegram-Archive
cd Telegram-Archive
# Create data directories
mkdir -p data/session data/backups
chmod -R 755 data/
# Configure environment
cp .env.example .env
Edit .env with your credentials:
TELEGRAM_API_ID=12345678 # Your API ID
TELEGRAM_API_HASH=abcdef123456 # Your API Hash
TELEGRAM_PHONE=+1234567890 # Your phone (with country code)
VIEWER_USERNAME=admin # Required for web access
VIEWER_PASSWORD=change-this # Required for web access
Optional: enable a SOCKS5 proxy for all Telegram connections (useful in regions where Telegram is blocked or behind corporate firewalls)
TELEGRAM_PROXY_TYPE=socks5
TELEGRAM_PROXY_ADDR=127.0.0.1
TELEGRAM_PROXY_PORT=1080
TELEGRAM_PROXY_USERNAME=
TELEGRAM_PROXY_PASSWORD=
TELEGRAM_PROXY_RDNS=false
Option A: Using the provided scripts (recommended for fresh installs)
# Run authentication
./init_auth.sh # Linux/Mac
# init_auth.bat # Windows
Option B: Direct Docker command (for existing deployments or re-authentication)
If your session expires or you need to re-authenticate an existing container:
# Generic command - adjust volume paths and credentials
docker run -it --rm \
-e TELEGRAM_API_ID=YOUR_API_ID \
-e TELEGRAM_API_HASH=YOUR_API_HASH \
-e TELEGRAM_PHONE=+YOUR_PHONE_NUMBER \
-e SESSION_NAME=telegram_backup \
-v /path/to/your/session:/data/session \
drumsergio/telegram-archive:8.13.0 \
python -m src auth
Example for docker compose deployment:
# If using docker compose with a session volume
docker run -it --rm \
--env-file .env \
-v ./data:/data \
drumsergio/telegram-archive:8.13.0 \
python -m src auth
# Then restart the backup container
docker compose restart telegram-backup
What happens during authentication:
docker compose up -d
View your backup at http://localhost:8000β
The default compose binds the viewer to 127.0.0.1. Put it behind a reverse proxy only after setting VIEWER_USERNAME and VIEWER_PASSWORD. To deliberately run without auth for a local-only viewer, set ALLOW_ANONYMOUS_VIEWER=true β this grants read-only access only; writes still require the master account.
| Problem | Solution |
|---|---|
Permission denied | Run chmod -R 755 data/ |
init_auth.sh: command not found | Run chmod +x init_auth.sh first |
| Viewer shows no data | Both containers need same database path - see Database Configurationβ |
Failed to authorize | Re-run ./init_auth.sh |
The standalone viewer image (drumsergio/telegram-archive-viewer) lets you browse backups without running the backup scheduler.
# Example: Viewer-only deployment
services:
telegram-viewer:
image: drumsergio/telegram-archive-viewer:8.13.0
ports:
- "127.0.0.1:8000:8000"
environment:
BACKUP_PATH: /data/backups
DATABASE_DIR: /data/db
VIEWER_USERNAME: admin
VIEWER_PASSWORD: your-secure-password
VIEWER_TIMEZONE: Europe/Madrid
volumes:
# SQLite needs write access for WAL files, sessions, audit logs, and thumbnails.
# Use :ro only when the database is PostgreSQL and media is mounted separately.
- /path/to/data:/data
Browse your backups at http://localhost:8000β
All settings are configured via environment variables. Set them in your .env file or as environment: entries in docker-compose.yml. See .env.exampleβ for a ready-to-use template.
ENABLE_LISTENERis a master switch. When set tofalse(the default), allLISTEN_*andMASS_OPERATION_*variables have no effect. You only need to configure those when you setENABLE_LISTENER=true.
The Scope column shows whether each variable applies to the backup scheduler (B), the web viewer (V), or both (B/V).
| Variable | Default | Scope | Description |
|---|---|---|---|
| Telegram Credentials | |||
TELEGRAM_API_ID | required | B | API ID from my.telegram.orgβ |
TELEGRAM_API_HASH | required | B | API Hash from my.telegram.orgβ |
TELEGRAM_PHONE | required | B | Phone number with country code (e.g., +1234567890) |
TG_ACCOUNT_<N>_API_ID | - | B | Multiple accountsβ : API ID of account N. N starts at 1 and must be contiguous. Declaring any TG_ACCOUNT_* variable switches to indexed mode and the three legacy variables above are ignored |
TG_ACCOUNT_<N>_API_HASH | - | B | API Hash of account N |
TG_ACCOUNT_<N>_PHONE_NUMBER | - | B | Phone number of account N with country code. Must be distinct across accounts |
TG_ACCOUNT_<N>_LABEL | default (N=1), account<N> (Nβ₯2) | B | Optional display label for account N |
TG_ACCOUNT_<N>_SESSION_NAME | see description | B | Optional session file name for account N. Account 1 defaults to the legacy SESSION_NAME chain (so an upgraded deployment keeps its session file and never re-logins); accounts 2+ default to telegram_backup_account<N> |
TELEGRAM_PROXY_TYPE | - | B | Optional proxy type for all Telegram clients. Currently supports socks5 |
TELEGRAM_PROXY_ADDR | - | B | SOCKS5 proxy host or IP address |
TELEGRAM_PROXY_PORT | - | B | SOCKS5 proxy port |
TELEGRAM_PROXY_USERNAME | - | B | Optional SOCKS5 username |
TELEGRAM_PROXY_PASSWORD | - | B | Optional SOCKS5 password |
TELEGRAM_PROXY_RDNS | false | B | Use remote DNS resolution through the SOCKS5 proxy |
| Backup Schedule & Storage | |||
SCHEDULE | 0 */6 * * * | B | Cron expression for backup frequency |
BACKUP_PATH | /data/backups | B/V | Base path for backup data and media |
DOWNLOAD_MEDIA | true | B | Download media files (photos, videos, documents) |
DOWNLOAD_MEDIA_TYPES | (empty) | B | Comma-separated whitelist of media types worth downloading: photo, video, video_note, animation, voice, audio, sticker, document, webpage. Empty downloads every type. Filtered media is still recorded with its metadata (name, MIME, size), only the file stays on Telegram. An archive captured before 8.5.0 typed round videos as video, and the retry pass judges a stored row by its stored type, so run reclassify-round-videos once before relying on video_note here |
DOWNLOAD_DOCUMENT_MIME_TYPES | (empty) | B | Narrow the document type to specific MIME types, e.g. application/pdf. Exact match, or the filename extension derived from the configured MIMEs (catches files Telegram labels application/octet-stream but names report.pdf). Each value must be a full type/subtype: a wildcard or a bare extension is rejected at startup rather than matching nothing. The extension fallback comes from the system MIME database, so a type it does not know is matched by its declared MIME only, and says so at startup. Empty keeps every document |
DOWNLOAD_CHAT_DESCRIPTION | false | B | Fetch each chat's description on every run for the viewer's chat info panel: a group or channel's about text, a user's bio, plus the member count of channels and supergroups. One extra API request per chat per run |
MAX_MEDIA_SIZE_MB | 100 | B | Skip media files larger than this (MB) |
MEDIA_MAX_FILENAME_BYTES | 143 | B | Usable filename byte budget for downloaded media. Raise to 255 on plain ext4/xfs; keep 143 for Synology/eCryptfs encrypted shares |
MEDIA_MAX_DOWNLOAD_ATTEMPTS | 5 | B | Stop retrying a file's download after this many failed attempts. Re-requesting the download resets the counter |
MEDIA_FLOOD_SLEEP_THRESHOLD | 60 | B | Mid-download FloodWaits up to this many seconds are absorbed in place so the transfer resumes instead of restarting from byte 0 (issue #232). 0 restores the old raise-immediately behavior. Absorbed pauses count toward DOWNLOAD_TIMEOUT_SECONDS |
DIALOG_FLOOD_SLEEP_THRESHOLD | 60 | B | FloodWaits up to this many seconds during get_dialogs()'s internal pagination are absorbed in place so the listing resumes on the same page instead of the whole call restarting from page 1 (issue #295 β an account with enough dialogs to reliably trip a page's FloodWait could otherwise never complete an initial non-whitelist backup, no matter the retry count or schedule spacing). 0 restores the old raise-immediately behavior |
DOWNLOAD_TIMEOUT_SECONDS | 3600 | B | Give up on a single media download after this many seconds. 0 disables the timeout |
MEDIA_REFRESH_MAX_ATTEMPTS | 3 | B | How many times a media item whose file reference expired is re-fetched and retried before it is left for the next scheduled run |
MEDIA_REFRESH_TIMEOUT_SECONDS | 120 | B | Upper bound on one message-refresh round trip, so a wedged connection cannot stall the run |
PARALLEL_DOWNLOAD_ENABLED | false | B | Fetch large files over several connections to lift the single-stream speed cap (see below) |
PARALLEL_DOWNLOAD_MIN_SIZE_MB | 20 | B | Only files at least this large use the parallel path (min 1) |
PARALLEL_DOWNLOAD_CONNECTIONS | 4 | B | Concurrent connections per file (clamped 2β8) |
PARALLEL_DOWNLOAD_PART_SIZE_KB | 512 | B | Chunk size per request; one of 4/8/16/32/64/128/256/512 |
BATCH_SIZE | 100 | B | Messages processed per database batch |
CHECKPOINT_INTERVAL | 1 | B | Save backup progress every N batch inserts (lower = safer resume after crash) |
DATABASE_TIMEOUT | 60.0 | B/V | Database operation timeout in seconds |
SESSION_NAME | telegram_backup | B | Telethon session file name |
SESSION_DIR | /data/session | B | Directory holding the session file. Defaults to a session/ directory alongside BACKUP_PATH |
DEDUPLICATE_MEDIA | true | B | Symlink identical media files across chats to save disk space |
SYNC_DELETIONS_EDITS | false | B | Batch-check ALL messages for edits/deletions each run (expensive!) |
VERIFY_MEDIA | false | B | Re-download missing or corrupted media files |
FILL_GAPS | false | B | After each scheduled backup, look for runs of missing message IDs and fetch them |
GAP_THRESHOLD | 50 | B | How many consecutive missing message IDs count as a gap worth filling |
STATS_CALCULATION_HOUR | 3 | B | Hour (0-23) to recalculate backup statistics daily |
PRIORITY_CHAT_IDS | - | B | Comma-separated chat IDs to process first in all operations |
SKIP_MEDIA_CHAT_IDS | - | B | Skip media downloads for specific chats (messages still backed up with text) |
SKIP_MEDIA_DELETE_EXISTING | true | B | Delete existing media files and DB records for chats in skip list to reclaim storage |
DOWNLOAD_YOUTUBE_VIDEOS | false | B | Archive the video file Telegram attaches to a YouTube link preview. Off by default; the message, link and thumbnail are archived either way |
YOUTUBE_VIDEOS_DELETE_EXISTING | false | B | Also delete YouTube link-preview videos already downloaded (needs DOWNLOAD_YOUTUBE_VIDEOS=false). Cannot be undone |
SKIP_TOPIC_IDS | - | B | Skip specific topics in forum supergroups (format: chat_id:topic_id,...) |
LOG_LEVEL | INFO | B/V | Logging verbosity: DEBUG, INFO, WARNING/WARN, ERROR |
LOG_CHAT_TITLES | false | B | Name the chat on the two per-chat progress lines: [27/27] Backing up: "My Group". Opt-in. Chat ids are never logged either way, a one-to-one chat is named by kind only (private chat) and never by the person, and titles are sanitised so a chosen title cannot forge a log line |
| Flood & Retry Tuning | |||
MAX_FLOOD_RETRIES | 5 | B | How many times a Telegram call is retried after a FloodWait before it gives up |
MAX_FLOOD_WAIT_SECONDS | 3600 | B | A FloodWait longer than this is not waited out β the call fails instead |
BACKOFF_MIN_SECONDS | 2.0 | B | First delay of the exponential backoff used for transient connection errors |
BACKOFF_MAX_SECONDS | 300.0 | B | Ceiling for that backoff delay |
FLOOD_WAIT_LOG_THRESHOLD | 10 | B | FloodWaits shorter than this are routine and logged at DEBUG instead of WARNING. 0 logs every one |
| Chat Filtering | See Chat Filteringβ below | ||
CHAT_IDS | - | B | Whitelist mode: backup ONLY these chats (ignores all other filters) |
WHITELIST_RESOLVE_DIALOG_LIMIT | 1000 | B | When a CHAT_IDS entry cannot be resolved (typically a DM on a fresh session), scan up to this many dialogs once to warm the entity cache β it then resolves permanently (issue #234). 0 disables |
CHAT_TYPES | private,groups,channels | B | Type-based mode: comma-separated chat types to backup |
GLOBAL_EXCLUDE_CHAT_IDS | - | B | Exclude specific chats (any type) |
GLOBAL_INCLUDE_CHAT_IDS | - | B | Force-include specific chats (any type) |
EXCLUDE_CHAT_IDS | - | B | Legacy alias for GLOBAL_EXCLUDE_CHAT_IDS, read only when that variable is unset or empty |
INCLUDE_CHAT_IDS | - | B | Legacy alias for GLOBAL_INCLUDE_CHAT_IDS, read only when that variable is unset or empty |
PRIVATE_EXCLUDE_CHAT_IDS | - | B | Exclude specific private chats |
PRIVATE_INCLUDE_CHAT_IDS | - | B | Force-include specific private chats |
GROUPS_EXCLUDE_CHAT_IDS | - | B | Exclude specific groups |
GROUPS_INCLUDE_CHAT_IDS | - | B | Force-include specific groups |
CHANNELS_EXCLUDE_CHAT_IDS | - | B | Exclude specific channels |
CHANNELS_INCLUDE_CHAT_IDS | - | B | Force-include specific channels |
GLOBAL_INCLUDE_FOLDER_IDS | - | B | Force-include every chat in this Telegram folder, across all types |
PRIVATE_INCLUDE_FOLDER_IDS | - | B | Force-include this folder's private chats |
GROUPS_INCLUDE_FOLDER_IDS | - | B | Force-include this folder's groups |
CHANNELS_INCLUDE_FOLDER_IDS | - | B | Force-include this folder's channels. See Folder-based includeβ |
FOLLOW_CHAT_MIGRATIONS | false | B | Automatically adopt the new supergroup id when a tracked basic group is upgraded to a supergroup, so capture continues without editing include lists. When off, the sweep only warns. See Group β supergroup migrationsβ |
| Real-time Listener | See Real-time Listenerβ below | ||
ENABLE_LISTENER | false | B | Master switch β enables all LISTEN_* features below |
LISTEN_EDITS | true | B | Apply text edits in real-time |
LISTEN_DELETIONS | false | B | Process deletion events from Telegram. Opt-in only |
DELETION_MODE | hard | B | When deletions are processed: hard removes archived messages (legacy), soft keeps messages and marks them deleted |
LISTEN_NEW_MESSAGES | true | B | Save new messages in real-time between scheduled backups |
LISTEN_NEW_MESSAGES_MEDIA | false | B | Also download media immediately (vs. next scheduled backup) |
LISTEN_CHAT_ACTIONS | true | B | Track chat photo, title, and member changes |
LISTEN_REACTIONS | false | B | Capture reactions in real-time (opt-in). Best-effort and aggregate-only (per-emoji counts); the scheduled backup reconciles reactions regardless |
REACTION_DEBOUNCE_SECONDS | 1.5 | B | Coalesce a burst of reaction updates on the same message into one write |
REACTION_RESWEEP_DAYS | 0 | B | Re-check the last N days of messages per chat on every scheduled sweep to recover your own reactions (0 disables). See Reactions made by your own accountβ |
REACTION_RESWEEP_MAX_PER_CHAT | 500 | B | Cap on messages re-checked per chat per sweep (β5 API calls/chat/sweep at the default) |
REACTION_RESWEEP_BATCH_DELAY_SECONDS | 2 | B | Minimum spacing between the re-sweep's API requests, across chats (0 disables). Smooths bursts; on a FloodWait the re-sweep pauses and resumes within the same run once the wait expires, deferring to the next sweep only if the wait outlives the run or floods repeat |
MASS_OPERATION_THRESHOLD | 10 | B | Max operations per chat before rate limiting triggers |
MASS_OPERATION_WINDOW_SECONDS | 30 | B | Sliding window for counting operations (seconds) |
MASS_OPERATION_BUFFER_DELAY | 2.0 | B | Deprecated compatibility setting; operations are rate-limited, not buffered |
| Event Webhook | |||
EVENT_WEBHOOK_ENABLED | false | B | Master switch β fire an HTTP request when the listener applies an edit/deletion. Opt-in only. See Event Webhookβ below |
EVENT_WEBHOOK_URL | β | B | Target URL (http:// or https://). Required when enabled; treated as a secret and never logged |
EVENT_WEBHOOK_METHOD | POST | B | POST or PUT |
EVENT_WEBHOOK_HEADERS | {} | B | JSON object of extra headers (auth tokens etc.). Its Content-Type drives auto-escaping; defaults to application/json; charset=utf-8 |
EVENT_WEBHOOK_EVENTS | both | B | Comma list: message_edited, message_deleted |
EVENT_WEBHOOK_CHAT_IDS | β | B | Comma-separated marked chat ids to fire for; empty = all chats the listener processes |
EVENT_WEBHOOK_BODY_TEMPLATE | JSON body | B | Custom body with {placeholder} / {placeholder|filter} substitution; empty = default JSON body |
| Database | See Database Configurationβ below | ||
DATABASE_URL | - | B/V | Full database URL (highest priority, overrides all below) |
DB_TYPE | sqlite | B/V | Database engine: sqlite or postgresql |
DB_PATH | $BACKUP_PATH/telegram_backup.db | B/V | Path to SQLite database file |
DATABASE_PATH | - | B/V | Full path to SQLite file (v2 compatible alias for DB_PATH) |
DATABASE_DIR | - | B/V | Directory containing telegram_backup.db (v2 compatible) |
POSTGRES_HOST | localhost | B/V | PostgreSQL host |
POSTGRES_PORT | 5432 | B/V | PostgreSQL port |
POSTGRES_USER | telegram | B/V | PostgreSQL username |
POSTGRES_PASSWORD | - | B/V | PostgreSQL password (required when using PostgreSQL) |
POSTGRES_DB | telegram_backup | B/V | PostgreSQL database name |
DB_ECHO | false | B/V | Log every SQL statement. Debugging only β extremely verbose |
| Viewer & Authentication | |||
VIEWER_USERNAME | - | V | Master web viewer username |
VIEWER_PASSWORD | - | V | Master web viewer password |
ALLOW_ANONYMOUS_VIEWER | false | V | Explicitly allow unauthenticated local viewer mode. Grants read-only access β browsing/search work, but settings, viewer/token management, and deletions still require the master account |
AUTH_SESSION_DAYS | 30 | V | Days before re-authentication is required |
AUTH_PROXY_HEADER | - | V | Header carrying the authenticated username from a trusted reverse proxy (Authelia, Authentik, Keycloak), e.g. Remote-User. See warning below |
AUTH_PROXY_ADMIN_USERS | - | V | Comma-separated usernames from AUTH_PROXY_HEADER that get the admin (master) role |
AUTH_PROXY_DEFAULT_ACCESS | none | V | Default chat access for auto-created proxy users: none or all |
DISPLAY_CHAT_IDS | - | V | Restrict viewer to specific chats (comma-separated IDs) |
TRUST_PROXY_HEADERS | false | V | Trust X-Forwarded-For / X-Real-IP only when your reverse proxy overwrites them |
INTERNAL_PUSH_SECRET | - | B/V | Shared secret for SQLite backup-to-viewer realtime push over Docker/private networks |
VIEWER_HOST | localhost | B | Viewer host for SQLite realtime push from backup/listener. The localhost default only fits a same-host (bare-metal) setup; the shipped compose overrides it to the telegram-viewer service |
| `VIEWER |
Content type
Image
Digest
sha256:047204c38β¦
Size
279.2 MB
Last updated
about 23 hours ago
docker pull drumsergio/telegram-archive-viewer