One-way backup of iCloud Drive and iCloud Notes to a WebDAV server such as Nextcloud. Built to run as a Kubernetes CronJob next to icloudpd, whose trusted Apple session it borrows, so there is no second two-factor login to maintain.
iCloud stays the source of truth. The tool never writes to iCloud.
assets/<note>/, linked relatively) into a folder named after
its Notes folder. The file name carries the first eight characters of the
note id, so equal titles do not collide. Changed notes are exported again,
notes moved to "Recently Deleted" are removed. Locked notes cannot be read
and are left as an earlier run stored them.X-OC-Mtime).A SQLite file records what was uploaded, so later runs only transfer changes.
MAX_DELETE_RATIO of the tracked files
or notes (beyond five, which are always allowed). A nearly empty listing
from iCloud therefore cannot wipe the backup. Raise the ratio for a single
run when a large deletion is intended.When the server's virus scanner refuses a Drive file (Nextcloud's
files_antivirus answers 415 with Virus <signature> ...), the file stays
in iCloud and is not backed up, but this does not count as an error. The run
mails one report listing every new finding with its signature. Once the mail
is out, the file is recorded and later runs skip it without downloading it
again, until it changes in iCloud or disappears from there.
If the mail cannot be sent, or MAIL_TO is not set, the run exits with
code 1 and tries again next time, so a finding is never silently dropped.
All settings come from environment variables.
| Variable | Default | Meaning |
|---|---|---|
APPLE_ID | required | Apple ID whose session is borrowed |
SOURCE_SESSION_DIR | /session | icloudpd's cookie directory, mounted read-only |
WORK_DIR | /tmp/icloud-backup | Session copy and downloads; needs room for the largest file |
STATE_FILE | /state/state.sqlite | Upload state, on a persistent volume |
WEBDAV_URL | required | WebDAV root, for Nextcloud https://<host>/remote.php/dav |
WEBDAV_USER | required | Account on the WebDAV server |
WEBDAV_PASSWORD_FILE | File holding the password (preferred) | |
WEBDAV_PASSWORD | Password, if no file is given | |
DRIVE_TARGET | iCloud Drive | Target folder for Drive |
NOTES_TARGET | iCloud Notes | Target folder for Notes |
DRIVE_ENABLED, NOTES_ENABLED | true | Switch an area off |
DRY_RUN | false | Log what would happen, write nothing |
MAX_DELETE_RATIO | 0.25 | Largest share of tracked items one run may delete |
CHUNK_SIZE_MIB | 64 | Files larger than this are uploaded in chunks of this size |
LOG_LEVEL | info | debug, info, warning, error |
MAIL_TO | Recipient of virus reports; without it a finding fails the run | |
SMTP_HOST | required with MAIL_TO | Mail server, implicit TLS |
SMTP_PORT | 465 | Submissions port |
SMTP_USERNAME | required with MAIL_TO | Login, also the sender |
SMTP_PASSWORD_FILE | File holding the SMTP password (preferred) | |
SMTP_PASSWORD | SMTP password, if no file is given | |
MAIL_FROM | SMTP_USERNAME | Sender address |
For Nextcloud, use an app password (Settings, Security) rather than the account password.
| Code | Meaning |
|---|---|
| 0 | Run completed without errors |
| 1 | Run completed, but some items failed, a safety net stopped deletions, or a virus finding could not be reported |
| 2 | No usable Apple session: renew it in icloudpd |
| 3 | Invalid configuration |
Logs are JSON lines on stderr. They never contain session tokens: for
exceptions the class name goes into error, and only failed WebDAV
requests add a detail with method, URL, status and the reason Nextcloud
gave, such as a virus found by files_antivirus or a forbidden file name.
The image runs as uid 1000 and starts icloud-backup. Tests and type checks
run during the image build (stage test). It needs read access to
icloudpd's cookie directory, which icloudpd also creates as uid 1000.
docker build -t icloud-backup .
python3.13 -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/pytest
.venv/bin/mypy
Releases follow semantic versioning and are tagged automatically from
Conventional Commits on main (semver-bump.yaml); a tag builds and
publishes the image.
.htaccess, are not recorded as uploaded, so every run tries
them again and exits with code 1.MIT, see LICENSE.
Content type
Image
Digest
sha256:ec092e438…
Size
55.2 MB
Last updated
2 days ago
docker pull ricariel/icloud-backup