This project packages libimobiledevice tooling and netmuxd together with a FastAPI + HTML dashboard to pair iOS devices and trigger incremental (incremental-capable) backups over the network. USB pairing workflow is fully supported on Linux hosts. On macOS (Docker Desktop) direct USB passthrough is not available, so you must pair on the host and then run network backups inside the container.
- Run
netmuxdinside the container (or attach to host usbmuxd). - Pair devices via USB directly from the web API.
- Trigger incremental backups using
idevicebackup2(Apple-style MobileSync format). - Persist lockdown pairing certificates and backup data across container restarts.
- Simple REST API + Server-Sent Events (SSE) log streaming for backup progress.
- Multi-architecture support (amd64, arm64) with automatic fallback to source build for
netmuxd. - Built-in HTML dashboard at
/ui(root/redirects there) for health, devices, pairing, backup jobs, and log streaming.
Dockerfile– Multi-stage build (downloads or buildsnetmuxd; installs dependencies; sets up app).app/– FastAPI application (main.py, requirements).docker-compose.yml– Example orchestration with volumes and USB passthrough.
- Pairing must happen with a physical USB connection (trust dialog appears on device). (macOS containers cannot access USB; pair on the macOS host then copy lockdown plist into the container.)
- After pairing, backups can occur while the device is connected only via Wi-Fi (through
netmuxdnetwork muxing). - Each device’s backup lives under
/data/backups/<UDID>. - Pairing information (lockdown plists) lives under
/data/lockdown(symlinked to/var/lib/lockdowninside container). - Weak OpenSSL config enables legacy SHA1 signatures (
OPENSSL_WEAK_CONFIG).
Note: The Dockerfile supports a build argument LIBIMOBILEDEVICE_PKG to specify the correct libimobiledevice runtime package (e.g. libimobiledevice-1.0-6 on Debian stable). The base image is now python:3.12-slim, so a separate virtual environment is no longer used.
docker build --build-arg LIBIMOBILEDEVICE_PKG=libimobiledevice-1.0-6 -t ios-backup .
You can also pin both the netmuxd tag and the libimobiledevice package simultaneously:
docker build \
--build-arg NETMUXD_VERSION=v0.3.0 \
--build-arg LIBIMOBILEDEVICE_PKG=libimobiledevice-1.0-6 \
-t ios-backup:v0.3.0 .
Basic build (current directory is project root):
docker build -t ios-backup .
Multi-arch build (requires Docker Buildx):
docker buildx build --platform linux/amd64,linux/arm64 -t yourrepo/ios-backup:latest .
Pin a specific netmuxd tag (default is v0.3.0):
docker build --build-arg NETMUXD_VERSION=v0.3.0 -t ios-backup:v0.3.0 .
USB pairing mode (device connected via USB at least once):
docker run -d \
--name ios-backup \
-p 8080:8080 \
-v ios_backups:/data/backups \
-v ios_lockdown:/data/lockdown \
-v ios_config:/data/config \
--device /dev/bus/usb \
--restart unless-stopped \
ios-backup:latest
Network-only backup (already paired on host; mount host usbmuxd socket instead of USB bus):
docker run -d \
--name ios-backup \
-p 8080:8080 \
-v ios_backups:/data/backups \
-v ios_lockdown:/data/lockdown \
-v ios_config:/data/config \
-v /var/run/usbmuxd:/var/run/usbmuxd \
-e APP_MODE=backup-only \
--restart unless-stopped \
ios-backup:latest
docker compose up -d
Edit docker-compose.yml to adjust environment variables, volumes, or device mappings.
| Variable | Purpose | Default |
|---|---|---|
| APP_MODE | full starts netmuxd; backup-only skips it |
full |
| PORT | Web server port | 8080 |
| BACKUP_DIR | Backup root | /data/backups |
| LOCKDOWN_DIR | Lockdown plist directory | /var/lib/lockdown |
| OPENSSL_WEAK_CONFIG | Path to custom OpenSSL config | /data/config/openssl-weak.conf |
| USBMUXD_SOCKET_ADDRESS | Host:port for netmuxd socket | 127.0.0.1:27015 |
| MAX_CONCURRENT_BACKUPS | Parallel backup limit | 2 |
| NETMUXD_BIN | Override netmuxd binary name/path | netmuxd |
| NETMUXD_DISABLE_UNIX | Disable unix domain socket (-–disable-unix) | true |
| NETMUXD_HOST | netmuxd host binding | 127.0.0.1 |
| START_USBMUXD | Autostart usbmuxd inside container before launching the FastAPI app | true |
| USBMUXD_FLAGS | Flags passed to usbmuxd (default "-f" to stay foreground; script backgrounds it) | -f |
| USBMUXD_SOCKET_PATH | Path to usbmuxd UNIX domain socket (shared with netmuxd if needed) | /var/run/usbmuxd |
Base URL: http://<host>:8080
- GET
/health– Service health + device list. - GET
/devices– List detected device UDIDs and pairing status. - GET
/devices/{udid}/info– Detailed device info (fromideviceinfo). - POST
/pair– Pair a device. Body optional{ "udid": "<UDID>" }; if omitted and exactly one device present, it is auto-selected. - POST
/unpair– Remove lockdown plist. Body{ "udid": "<UDID>" }. - POST
/backup– Start backup. Body{ "udid": "<UDID>" }. - GET
/backup/jobs– List all jobs. - GET
/backup/{jobId}/status– Status snapshot (running, success, error). - GET
/backup/{jobId}/logs– Last log lines. - GET
/backup/{jobId}/stream– SSE stream for real-time logs. - GET
/backups/{udid}– Summary (size, file count). - GET
/backups/{udid}/files– List all files (metadata only). - DELETE
/backups/{udid}– Remove a device’s backup folder. - GET
/ui– HTML dashboard (root/redirects here).
The container entrypoint uses a startup script (/usr/local/bin/container-start) which, when START_USBMUXD=true, launches usbmuxd first (passing USBMUXD_FLAGS), then execs the Python application. If you prefer to rely on a host usbmuxd instead (e.g. mounting /var/run/usbmuxd), set START_USBMUXD=false to skip starting it inside the container. Adjust USBMUXD_SOCKET_PATH if your host uses a non-standard location.
- Connect device via USB.
- GET
/devices– confirm UDID present. - POST
/pair– trust prompt appears on device, accept it. - Confirm lockdown file persisted under
/data/lockdown. - Disconnect USB; ensure device and container share network (Wi-Fi / LAN).
- POST
/backupwith UDID.
- Uses
idevicebackup2 backup -n --full <BACKUP_DIR>. - Incremental by design; subsequent runs update existing backup set.
- Server sets
OPENSSL_CONFandUSBMUXD_SOCKET_ADDRESSenvironment variables per job. - Progress estimation is heuristic (log line count) because tool does not expose granular progress metrics.
- Pair each separately (UDID-specific plist).
- Jobs enforce one backup at a time per device; concurrency limit overall via
MAX_CONCURRENT_BACKUPS.
Recommended named volumes:
ios_backups→/data/backupsios_lockdown→/data/lockdownios_config→/data/config- (Optional)
logs→/data/logs
Recreating the container will retain pairing state if volumes are preserved.
- Avoid
--privilegedunless necessary; prefer--device /dev/bus/usb. - Limit network exposure of port 8080 (use reverse proxy, auth layer, or bind to internal interfaces).
- Pairing and backup APIs are unauthenticated by default; add an auth middleware (e.g. API key, JWT) for production.
- Backups may contain sensitive data (encrypted iTunes-style). Secure volume storage and enforce appropriate access controls.
- Device not listed:
- Confirm USB passthrough (check
docker logs ios-backupfor permission errors). - On host, verify
lsusbshows Apple vendor (05ac).
- Confirm USB passthrough (check
- Pair fails:
- Ensure device prompt was accepted; re-run
POST /pair. - Check OpenSSL weak config exists at
OPENSSL_WEAK_CONFIG.
- Ensure device prompt was accepted; re-run
- Backup stalls early:
- Inspect SSE stream (
/backup/{jobId}/stream) for errors. - Confirm network stability; large backups can take 20+ minutes.
- Inspect SSE stream (
- netmuxd crashes:
- Recreate container or inspect logs for version incompatibilities; try pinning another
NETMUXD_VERSION.
- Recreate container or inspect logs for version incompatibilities; try pinning another
- Add restore endpoint (
idevicebackup2 restore) – requires caution (device prompts). - Add scheduling (cron-like) inside container or external orchestrator.
- Integrate Prometheus metrics endpoint.
- Replace heuristic progress with parsed phases if format evolves.
- Provide web frontend (React / Vue) consuming the REST API.
Rebuild with new tag:
docker build --build-arg NETMUXD_VERSION=v0.3.1 -t ios-backup:latest .
If release asset missing for your architecture, source build fallback is automatic (Rust toolchain installed in builder stage).
The repository includes a GitHub Actions workflow (.github/workflows/docker-publish.yml) that will build and push multi-architecture images (amd64, arm64) to GHCR when you push to main or create a version tag (e.g. v0.1.0).
Log in once locally:
echo $GITHUB_TOKEN | docker login ghcr.io -u <your_github_username> --password-stdin(or create a classic PAT with read:packages + write:packages scopes if needed).
If the repo is private and you want the image public, make the repository public OR adjust package visibility in GitHub’s “Packages” settings.
git tag v0.1.0
git push origin v0.1.0The workflow will produce:
ghcr.io/<owner>/ios-network-backup:v0.1.0ghcr.io/<owner>/ios-network-backup:latest(if on main or tagged)ghcr.io/<owner>/ios-network-backup:sha-<short_sha>
To run without building locally:
docker pull ghcr.io/<owner>/ios-network-backup:latest
docker run -d --name ios-backup \
-p 8080:8080 \
-v ios_backups:/data/backups \
-v ios_lockdown:/data/lockdown \
-v ios_config:/data/config \
--device /dev/bus/usb \
ghcr.io/<owner>/ios-network-backup:latestdocker pull ghcr.io/<owner>/ios-network-backup:v0.1.0
docker run -d --name ios-backup ... ghcr.io/<owner>/ios-network-backup:v0.1.0Push a tag like v0.3.1 and the workflow will attempt to use that as NETMUXD_VERSION. If you want a different netmuxd tag than the repo version, manually dispatch the workflow with an input version:
- GitHub UI → Actions → “Publish Multi-Arch Docker Image” → “Run workflow” → set version input (e.g.
v0.3.0).
Local:
docker build -t ios-backup:dev .Remote (pull):
docker run ghcr.io/<owner>/ios-network-backup:latestUse GitHub “Packages” UI or CLI:
gh api \
-X DELETE \
/user/packages/container/ios-network-backup/versions/<version_id>The workflow enables SBOM and provenance (provenance: true, sbom: true). Consumers can inspect:
docker buildx imagetools inspect ghcr.io/<owner>/ios-network-backup:latestReplace <owner> with your GitHub user or org name (lowercase).
For GitHub Actions builds in this repository you do not need a Personal Access Token (PAT). The workflow uses the built-in GITHUB_TOKEN with packages: write permission to authenticate to GHCR automatically. Use a PAT or GitHub App token only when:
- You are pushing images locally (outside Actions) to a private package.
- You need to pull a private image from another system that is not executing inside this repository’s Actions.
Public images (package set to public) can be pulled with no authentication:
docker pull ghcr.io/<owner>/ios-network-backup:latest
If you do need a PAT for local pushes:
- Create a fine‑grained PAT with Package permissions (Read & Write) for this repo.
- Login locally:
echo $PAT | docker login ghcr.io -u <your_username> --password-stdin - Push:
docker push ghcr.io/<owner>/ios-network-backup:<tag>
Deploy keys (SSH keys tied to a single repo) cannot be used for GHCR because they only grant Git access, not registry/package API access.
This project aggregates third-party binaries (netmuxd, libimobiledevice). Review their upstream licenses separately. All original code in this repository is released under The Unlicense (public domain). See LICENSE for details.
Use at your own risk. Backups should be periodically validated. This tool does not guarantee immunity to data corruption or incomplete backup states.
Happy backing up!
Python Environment:
Dependencies are installed directly into the python:3.12-slim base image; no per-project virtual environment is created. To add or update Python packages, edit app/requirements.txt and rebuild the image.