Backing up my Android phone with Termux and BorgBackup

Fabio Natali, 14 July 2026

Intro

If you, like me - willingly or not - are an Android user in search of a file backup solution, this post might be for you. I'll walk you through how I use Termux and BorgBackup (and a few other tools) to set up a bespoke phone backup workflow.

tl;dr: my Termux+Borg configuration is available in this repository. It includes instructions to bootstrap a Termux environment from scratch, including setting up a Borg-based backup cron job. It's tailored to my needs (it includes Emacs!), but you may be able to cherry pick bits and pieces that are relevant to you.

Alternative backup strategies

At least three options come to mind when it comes to backing up your phone data.

Mainstream commercial services

You can use a mainstream file-synchronisation and cloud-backup service, such as Google Drive, Google Photos, Dropbox, and the like. These services tend to be easy to use and, all considered, cost-effective. However, they are typically proprietary and not end-to-end encrypted. By using them, you end up relinquishing some degree of control over your data.

Popular self-hostable alternatives

You can use one of the popular open-source self-hostable solutions such as Nextcloud, Syncthing, or Immich. These are all great projects with a good track record. However, none of them seemed to quite fit my needs.

Nextcloud

In my case, setting up, securing, and maintaining a whole Nextcloud server only for backing up my phone's data seemed overkill. Every new self-hosted service inevitably widens the attack surface of your IT setup and becomes a security liability. I want my setup to be as minimalist as possible.

Syncthing

Syncthing is another project with a great reputation. However, when it comes to Android, the story is somewhat patchy. The official Android app was discontinued some time ago. Some community members stepped up to revive the Android project, but I wasn't very keen to trust a newly-formed group of maintainers that I knew little about.

Immich

Immich seemed to be focused on photos and videos, while I was looking for a more general data backup solution.

A bespoke solution

The third and ultimate solution I could think of was a bespoke combination of Termux and BorgBackup. It seemed the most promising option for my case, so that's what I went for.

In the rest of this post, I go through the setup in more detail.

Requirements

This setup requires:

  • Termux installed on your phone
  • BorgBackup, and various other packages, installed in Termux
  • access to a secure server via SSH

Optionally:

Step-by-step process

If you want to tag along, these are the steps I followed to set things up.

Termux setup

Install Termux from F-Droid or Google Play. Then, within Termux:

pkg install \
    borgbackup \
    curl \
    jq \
    openssh \
    termux-api \
    termux-tools

SSH setup

Create an SSH key pair:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""

Copy the public part of the key to your server's ~/.ssh/authorized_keys so that you can log in without being prompted for a password. Let's assume that your server is server.example.com and that you can log into it with ssh phone@server.example.com.

Backup initialisation

BorgBackup, or simply Borg, is a great Python tool for deduplicated (i.e. space-efficient) encrypted backups. Some familiarity with Borg is taken for granted here. Refer to the project's documentation if you're new to it.

Initialise the Borg repository:

borg init --encryption=repokey ssh://phone@server.example.com/path/to/repo

If you trust the server, you may want to use an empty passphrase, which makes it easier to use Borg non-interactively.

Congratulations, you should now be able to back up your files with:

borg create ssh://phone@server.example.com/path/to/repo::first-backup \
    /storage/emulated/0/data-0 \
    /storage/emulated/0/data-1 \
    ...

Did it work? Yay, congratulations!

The command is quite a mouthful, though, isn't it? Custom Bash script to the rescue then - read on.

A Borg wrapper

I use a backup script to make invocations a bit easier - see the end of this blog post for the full listing. I have the script saved as ~/.local/bin/backup (which is in my shell's PATH).

Save the script configuration in ~/.config/backup.json:

{
  "source_directories": [
    "/storage/emulated/0/data-0",
    "/storage/emulated/0/data-1",
    ...
  ],
  "repository": "ssh://phone@server.example.com/path/to/repo"
}

Optionally, include exclude_patterns (for paths that you do not want to back up) and healthcheck_url (for health-check services to ping when the script completes successfully). Refer to the script source for further details.

Now test the script by running backup or backup my-config.json. This should create a new backup on the server, with the folders data-0, data-1, etc.

Scheduled backups

If all has worked well so far, you may want to schedule the backup to run automatically at regular intervals. The termux-api package we installed earlier provides the termux-job-scheduler command, which is the Termux equivalent of a cron job:

termux-job-scheduler \
    --battery-not-low false \
    --network unmetered \
    --period-ms 900000 \
    --persisted true \
    --script ~/.local/bin/backup

In my experience, this doesn't seem to drain the battery noticeably, but your mileage may vary. If so, tweak the job options accordingly (e.g. --battery-not-low and --period-ms). You can see the list of current jobs with termux-job-scheduler --pending.

Hardening

From a functional point of view, the setup is essentially complete. We have managed to achieve what we set out to do: back up an Android phone to a remote server. However, given my security inclination, there are a couple of aspects of this setup I'm not completely happy about:

  • SSH is open to the world on the server
  • the phone has full shell access to the server

Here's how I addressed these points.

WireGuard

WireGuard is a lightweight VPN protocol that creates encrypted point-to-point tunnels with minimal configuration and great performance. My phone-to-server connection runs over a WireGuard tunnel: the server only exposes SSH on the WireGuard interface, meaning it is not reachable from the public internet. The WireGuard Android app makes it straightforward to keep the tunnel active on the phone.

SSH configuration

Earlier, we copied the phone's public key to the server's ~/.ssh/authorized_keys to allow password-less logins. We can restrict the phone user's SSH access so that it can only run Borg by prepending the following to the public key entry:

command="borg serve --restrict-to-path /home/phone/backup",restrict ssh-...

You may want to check the OpenSSH documentation for further details, e.g. here.

Further restrictions

Things can be made even more restrictive - for example, by setting the Borg repository to append-only mode. This would prevent an attacker from getting hold of your phone and remotely deleting all your backups - although it's not really part of my threat model.

Final considerations and full backup script

I've been running this setup for a few days, and it's proved reliable so far. Backups typically complete in a few seconds (thanks to Borg's deduplication), and I've successfully restored files from the repository on more than one occasion. The main limitation is that it only covers files accessible to Termux - app data that isn't stored on shared storage won't be backed up.

My Termux dotfiles are available here, including the backup script and the bootstrap workflow for the whole Termux environment.

I hope this may be useful to some of you - please send me any comment or question via the Fediverse or email (my contact details are on my home page).

The full backup script is reproduced below.

#!/data/data/com.termux/files/usr/bin/bash

# A minimalist BorgBackup script for Termux on Android. Backs up directories to
# a remote Borg repository, prunes old archives, and pings a healthcheck URL.
#
# Usage:
#
# backup [config.json]
#
# Reads source directories, repository path, and other settings from a JSON
# config file (default: ~/.config/backup.json). E.g.:
#
# {
#   "source_directories": [
#     "/storage/emulated/0/Pictures"
#   ],
#   "repository": "ssh://phone@example.com/home/phone/backup",
#   "healthcheck_url": "https://healthcheck.example.com/foobar"
# }
#
# Before first use, initialise the remote repository, e.g.:
#
# borg init --encryption=authenticated ssh://user@host/path/to/repository
#
# To run automatically, the script can be added to the Termux scheduler, e.g.:
#
# termux-job-scheduler \
#     --battery-not-low false \
#     --network unmetered \
#     --period-ms $((1000 * 60 * 30)) \
#     --persisted true \
#     --script PATH-TO-SCRIPT
#
# Termux dependencies:
#
# - bash
# - borgbackup
# - curl
# - jq
# - openssh
# - termux-tools (termux-wake-lock and termux-wake-unlock)
# - termux-api (termux-job-scheduler)

set -o errexit
set -o nounset
set -o pipefail

# ------------------------------------------------------------------------------
# Help

if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
    cat <<EOF
Usage: $(basename "$0") [CONFIG_FILE]

A minimalist BorgBackup script for Termux on Android. Backs up directories to a
remote Borg repository, prunes old archives, and pings a healthcheck URL. Only
runs when the device is charging and connected to WiFi.

Arguments:
  CONFIG_FILE  Path to JSON config (default: ~/.config/backup.json)
EOF
    exit 0
fi

# ------------------------------------------------------------------------------
# Load config

CONFIG="${1:-$HOME/.config/backup.json}"

cfg() { jq -r "$1" "$CONFIG"; }

ARCHIVE_FORMAT="{utcnow:%Y-%m-%dT%H:%M:%S}"
REPOSITORY="$(cfg '.repository')"
HEALTHCHECK_URL="$(cfg '.healthcheck_url // empty')"

mapfile -t BACKUP_PATHS < <(cfg '.source_directories[]')
mapfile -t EXCLUDE_PATTERNS < <(cfg '.exclude_patterns // [] | .[]')

# ------------------------------------------------------------------------------
# Functions

acquire_lock() {
    exec 9>~/.borgbackup.flock
    if ! flock -n 9; then
        echo "Another backup is already running"
        exit 0
    fi
}

init() {
    termux-wake-lock
}

cleanup() {
    termux-wake-unlock
}

backup() {
    local exclude_args=()
    for pattern in "${EXCLUDE_PATTERNS[@]}"; do
        exclude_args+=("--exclude" "$pattern")
    done

    borg create \
         --filter=AME \
         --list \
         --progress \
         --stats \
         "${exclude_args[@]}" \
         "${REPOSITORY}::${ARCHIVE_FORMAT}" \
         "${BACKUP_PATHS[@]}"
}

prune() {
    borg prune \
         --list \
         --keep-weekly=-1 \
         --keep-within=7d \
         --stats \
         "$REPOSITORY"
}

ping_healthcheck() {
    if [[ -n "$HEALTHCHECK_URL" ]]; then
        curl --max-time 10 --retry 5 "$HEALTHCHECK_URL"
    fi
}

# ------------------------------------------------------------------------------
# Main

trap "cleanup" EXIT
acquire_lock
init
backup
prune
ping_healthcheck || true  # backup succeeded, don't fail on this

Revision c2ca984.