Skip to content

REST API Control Surface

The image ships an optional HTTP API that lets you trigger backups, inspect state, download or delete backup files, initiate restores, and change a whitelisted subset of runtime settings — all over plain HTTP with a bearer token. When disabled (the default) nothing changes: go-cron is PID 1 exactly as before.


Enabling the API

Set REST_API_ENABLE=TRUE and supply a token. When enabled, the bundled backupgram-api binary becomes PID 1 and supervises go-cron; the cron schedule and the 8080 healthcheck endpoint are unchanged.

services:
  backup:
    image: ganiyevuz/backupgram:17
    environment:
      POSTGRES_HOST: postgres
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      SCHEDULE: "@daily"
      TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}"
      TELEGRAM_CHAT_ID: "${TELEGRAM_CHAT_ID}"
      # --- REST API ---
      REST_API_ENABLE: "TRUE"
      REST_API_TOKEN: "${REST_API_TOKEN}"   # required; see also REST_API_TOKEN_FILE
    ports:
      - "127.0.0.1:8081:8081"              # bind to loopback; use a reverse proxy for remote access
    volumes:
      - backups:/backups

See examples/docker-compose.rest-api.yml for a ready-to-run compose file.

Variable Default Description
REST_API_ENABLE FALSE Set TRUE to enable.
REST_API_PORT 8081 Listening port (separate from the 8080 healthcheck).
REST_API_TOKEN "" Admin bearer token. Required when the API is enabled.
REST_API_TOKEN_FILE "" Docker-secret path for the token (takes precedence over REST_API_TOKEN).

Authentication

The API key is a single admin bearer token you generate yourself and set on the container — there is no key-issuing endpoint. Every request (except GET /healthz) must include:

Authorization: Bearer <token>

A missing or wrong token returns 401 Unauthorized. The token is compared in constant time (crypto/subtle), and the server is fail-closed: when REST_API_ENABLE=TRUE it refuses to start unless REST_API_TOKEN (or a readable REST_API_TOKEN_FILE) is set, so it never runs unauthenticated.

GET /healthz is intentionally open so load-balancers and orchestrators can probe liveness without a credential.

Generate a token

Use any high-entropy value (≥ 32 bytes of randomness):

openssl rand -base64 48
# or
openssl rand -hex 32
# or
python3 -c "import secrets; print(secrets.token_urlsafe(48))"

Set it via REST_API_TOKEN, or — recommended for production — point REST_API_TOKEN_FILE at a Docker secret (it takes precedence over the plain env var):

environment:
  REST_API_ENABLE: "TRUE"
  REST_API_TOKEN_FILE: /run/secrets/backupgram_token
secrets:
  - backupgram_token

Validate / use a token

Send it as the bearer header; only the correct token gets through:

TOKEN="…your token…"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8081/healthz                       # 200 (no auth needed)
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8081/status                        # 401 (missing)
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer wrong" http://localhost:8081/status   # 401
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" http://localhost:8081/status  # 200

In django-backupgram, paste the same token into the BackupServer token field; it is stored encrypted at rest and sent server-side — never exposed to the browser.

Rotate a token

Generate a new value, update REST_API_TOKEN / the secret, recreate the container, and update the token wherever clients store it (e.g. the BackupServer record). There is no token database, expiry, or revocation list — rotation is simply replacing the value.


TLS / reverse-proxy warning

The API has no built-in TLS. It speaks plain HTTP. Do not expose port 8081 to the public internet without fronting it with a TLS-terminating reverse proxy (nginx, Caddy, Traefik, etc.).

The example compose file binds the port to 127.0.0.1 (127.0.0.1:8081:8081) so it is only reachable on the host loopback by default. If you need remote access, place a proxy in front and let the proxy handle TLS — never bind directly to 0.0.0.0 in a production deployment.


Endpoints

Method Path Auth Description
GET /healthz none Liveness probe → {"status":"ok"}
GET /status bearer Config summary + last backup result
GET /backups bearer Inventory: [{slot, name, size, mtime}, …]
GET /backups/{slot}/{name} bearer Download a dump file
DELETE /backups/{slot}/{name}?confirm=true bearer Delete a backup (confirm=true required)
POST /backup bearer Trigger a backup → 202 {"job_id":"…"}
POST /restore bearer Restore a backup → 202 {"job_id":"…"}
GET /jobs bearer List all async job states
GET /jobs/{id} bearer State of one job (queued/running/succeeded/failed)
GET /config bearer Effective runtime config (secrets masked)
PATCH /config bearer Update whitelisted keys; all-or-nothing
DELETE /config/{key} bearer Clear one override (revert to base env)

POST /restore body

{
  "file": "last/mydb-20260416-143000.sql.gz",
  "target_db": "mydb",
  "confirm": true
}

Or, for a Telegram-sourced restore:

{
  "telegram_message_id": 4521,
  "target_db": "mydb_restored",
  "confirm": true
}

Rules: - Exactly one of file or telegram_message_id must be set. - target_db must match ^[A-Za-z_][A-Za-z0-9_]{0,62}$. - confirm: true is required (guards against accidental restores).


Async jobs

POST /backup and POST /restore return 202 Accepted immediately with a job id. Poll GET /jobs/{id} until the state is terminal:

State Meaning
queued Accepted, not yet started
running In progress
succeeded Completed without error
failed Completed with an error (check state, error, and log_tail)
{
  "id": "a1b2c3d4e5f6a7b8",
  "type": "backup",
  "state": "succeeded",
  "queued_at": 1780735738,
  "started_at": 1780735738,
  "finished_at": 1780735739,
  "exit_code": 0,
  "log_tail": ["...up to last 50 lines of combined output..."],
  "error": ""
}

GET /jobs returns the full list (most recent first). Job history is kept in-memory and cleared on container restart.


Runtime config

GET /config returns the effective value of every mutable key. Non-secret keys appear as {"value":"...","source":"base|override"}. Secret (write-only) keys are masked — the value field is omitted entirely and only set and source are returned: {"set":true,"source":"base"} if configured, {"set":false,"source":"base"} if not set.

PATCH /config accepts a JSON object of {KEY: value} pairs. The update is atomic (all-or-nothing): if any key is blocked the entire request is rejected with 403. A SCHEDULE change takes effect immediately by restarting go-cron with the new expression. Overrides are written to ${BACKUP_DIR}/.api-overrides.env and survive container restarts (as long as the volume is mounted).

DELETE /config/{key} removes one override, reverting that key to whatever the base environment provides.

Whitelisted mutable keys

Key Masked in GET?
SCHEDULE no
BACKUP_KEEP_MINS no
BACKUP_KEEP_DAYS no
BACKUP_KEEP_WEEKS no
BACKUP_KEEP_MONTHS no
POSTGRES_DB no
POSTGRES_DB_AUTODISCOVER no
POSTGRES_DB_EXCLUDE no
POSTGRES_EXTRA_OPTS no
POSTGRES_EXCLUDE_TABLES no
TELEGRAM_CHAT_ID no
TELEGRAM_THREAD_ID no
TELEGRAM_NOTIFY_ON no
TELEGRAM_API_URL no
BACKUP_MIN_DISK_SPACE no
BACKUP_MAX_AGE_HOURS no
WEBHOOK_EXTRA_ARGS no
TELEGRAM_BOT_TOKEN yes (write-only)
TELEGRAM_API_ID yes (write-only)
TELEGRAM_API_HASH yes (write-only)
WEBHOOK_URL yes (write-only)

Blocked keys (always 403)

Connection credentials (POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, all *_FILE variants), BACKUP_DIR, BACKUP_ENCRYPTION_KEY, all REST_API_* keys, POSTGRES_CLUSTER, and any startup-only variable are blocked. Attempting to PATCH them returns 403 Forbidden.

*_FILE precedence caveat

A Docker-secret file (*_FILE) always wins over an API override of the same credential. For example, if TELEGRAM_BOT_TOKEN_FILE is mounted, a PATCH to TELEGRAM_BOT_TOKEN is accepted and persisted, but the running value is still read from the secret file. Avoid managing the same credential by both paths.


curl examples

Liveness

curl http://localhost:8081/healthz
# {"status":"ok"}

Status

curl -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/status

Trigger a backup and poll until done

# Start a backup
JOB=$(curl -s -X POST \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/backup | jq -r .job_id)

# Poll
until [ "$(curl -s -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/jobs/$JOB | jq -r .state)" != "running" ]; do
  sleep 2
done

curl -s -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/jobs/$JOB | jq .

List backups and download one

# List
curl -s -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/backups | jq .

# Download daily/mydb-20260416.sql.gz
curl -OJ -H "Authorization: Bearer $REST_API_TOKEN" \
  "http://localhost:8081/backups/daily/mydb-20260416.sql.gz"

Delete a backup

curl -X DELETE \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  "http://localhost:8081/backups/last/mydb-20260416-143000.sql.gz?confirm=true"

Restore from a stored file

curl -s -X POST \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file":"last/mydb-20260416-143000.sql.gz","target_db":"mydb","confirm":true}' \
  http://localhost:8081/restore | jq .

Restore from a Telegram message id

curl -s -X POST \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"telegram_message_id":4521,"target_db":"mydb_restored","confirm":true}' \
  http://localhost:8081/restore | jq .

Read, update, and clear config

# Read effective config
curl -s -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/config | jq .

# Change schedule and retention
curl -s -X PATCH \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"SCHEDULE":"0 3 * * *","BACKUP_KEEP_DAYS":"14"}' \
  http://localhost:8081/config | jq .

# Revert SCHEDULE override (go back to the base env value)
curl -X DELETE \
  -H "Authorization: Bearer $REST_API_TOKEN" \
  http://localhost:8081/config/SCHEDULE