A small, containerized tool that periodically exports media (photos & videos) from a hosted Immich instance and backs it up into per-user Nextcloud libraries using WebDAV.
Immich remains the primary photo app; Nextcloud acts as a per-user off-site backup of raw media.
For each configured user the container will:
- Export the user's photos and videos from Immich via
immich-go(full or incremental, depending on env vars). - Store the export locally under
/data/<user>/YYYY/MM/DD/<filename>. - Sync (or copy) the local export into the same user's Nextcloud account at
Photos/immich-backup/YYYY/MM/DD/<filename>viarcloneover WebDAV.
The container runs once and exits — scheduling is left to the host (cron, systemd timer, etc.).
Implementation note: the script writes a per-user TOML config file and calls immich-go with --config /tmp/immich-go-<user>.toml (see spec/SPEC.md for an example). Template tokens used by the config are {{DateYear}}, {{DateMonth}}, {{DateDay}} and {{OriginalFileName}}.
Normalization & deduplication
Before uploading, the script normalizes immich-go exports produced as temporary files and JSON sidecars. For each temporary export (data file names beginning with ~) the script:
- reads the accompanying
.JSONsidecar to obtain the original filename anddateTaken; - moves the data file into the correct date-based folder (
/data/<user>/YYYY/MM/DD/) and renames it to the original filename; the sidecar is moved to<filename>.JSONalongside the file; - if a target file already exists the script compares the two files (using
cmpwhen available, otherwise a file-size fallback) and, if identical, removes the temporary export and its sidecar to avoid duplicates; - if not identical, the script generates a unique filename by appending a numeric suffix to avoid collisions.
This normalization step runs before rclone so the remote receives correctly named files in the date-based layout and duplicate uploads are minimized.
| Requirement | Notes |
|---|---|
| Immich instance | With API keys created for every user you want to back up. |
| Nextcloud instance | With user accounts and an app password per user. |
| Docker + Docker Compose v2 | docker compose (not the legacy docker-compose). |
| Persistent storage | A host directory or NAS mount for /data (exported media). |
- Log in to your Immich instance as the target user.
- Go to Account Settings → API Keys.
- Click New API Key, give it a description, and copy the token.
- Repeat for every user you want to back up.
Your Nextcloud username is the one shown in the WebDAV URL. In Nextcloud:
- Go to Settings → (bottom of the left sidebar) and look for the WebDAV address.
- It will look like:
https://cloud.example.com/remote.php/dav/files/alice/—aliceis the username.
- Go to Settings → Security → Devices & sessions.
- Enter a name (e.g. immich-backup) and click Create new app password.
- Copy the generated password — it is only shown once.
Important: Always use app passwords, never your main Nextcloud password.
All configuration is done through environment variables. Copy .env.template to .env and fill in your values:
cp .env.template .env
# Edit .env with your favourite editorNever commit your
.envfile to version control! Add it to.gitignore.
| Variable | Description | Example |
|---|---|---|
IMMICH_SERVER |
Base URL of Immich (no trailing slash) | https://immich.example.com |
NC_BASE_URL |
Base URL of Nextcloud (no trailing slash) | https://cloud.example.com |
USER_LIST |
Space-separated logical user IDs | alice bob |
User identifiers must be lowercase [a-z0-9_-] and are used literally to build per-user variable names.
| Variable | Description |
|---|---|
IMMICH_API_KEY_<user> |
Immich API key for this user |
NC_USER_<user> |
Nextcloud login name (used in WebDAV path + auth) |
NC_PASS_<user> |
Nextcloud app password for this user |
| Variable | Default | Description |
|---|---|---|
RCLONE_MODE |
sync |
sync mirrors local→remote (including deletions); copy is append-only |
RCLONE_BWLIMIT |
8M |
Bandwidth limit for rclone |
RCLONE_TRANSFERS |
4 |
Number of parallel file transfers |
RCLONE_CHECKERS |
4 |
Number of parallel checkers |
IMMICH_FROM_DATE_RANGE |
(disabled) | Pass-through value for immich-go from-date-range; takes precedence over IMMICH_INCREMENTAL_DAYS |
IMMICH_INCREMENTAL_DAYS |
0 |
Incremental window in days (>0 enables incremental export, 0 or empty keeps full export) |
PRUNE_AFTER_DAYS |
(disabled) | Delete local exports older than N days (0 or empty = disabled) |
DELETE_LOCAL_AFTER_RUN |
false |
Delete all local exports for a user after a successful sync (true/1/yes to enable) |
RETRY_COUNT |
0 |
Number of retries for transient immich-go/rclone failures |
RETRY_DELAY_SECONDS |
10 |
Seconds to wait between retries |
LOG_LEVEL |
info |
Logging verbosity: debug, info, warn, error |
JSON_LOG |
false |
Output logs as JSON lines for machine parsing |
TEST_MODE |
false |
Dry-run: skips immich-go export, runs rclone --dry-run |
Additional notes:
IMMICH_GO_VERSIONis available as a Docker build-arg and the project currently uses0.31.0by default in theDockerfile. Pin and verify versions when building images.- Template tokens used by the runtime TOML config:
{{DateYear}},{{DateMonth}},{{DateDay}},{{OriginalFileName}}.
| Container path | Purpose |
|---|---|
/data |
Exported media; should be backed by persistent host storage |
docker compose run --rm immich2nextcloud-backupdocker compose build
docker compose run --rm immich2nextcloud-backupdocker compose build \
--build-arg IMMICH_GO_VERSION=0.31.0 \
--build-arg RCLONE_VERSION=1.69.1TEST_MODE=true docker compose run --rm immich2nextcloud-backup# Last 1 day
IMMICH_INCREMENTAL_DAYS=1 docker compose run --rm immich2nextcloud-backup
# Explicit range (takes precedence over IMMICH_INCREMENTAL_DAYS)
IMMICH_FROM_DATE_RANGE="2026-02-20,2026-02-21" docker compose run --rm immich2nextcloud-backupAdd a cron job on the host, for example nightly at 02:00:
0 2 * * * cd /path/to/immich2nextcloud-backup && docker compose run --rm immich2nextcloud-backup >> /var/log/immich-backup.log 2>&1Create a service unit (/etc/systemd/system/immich-backup.service):
[Unit]
Description=Immich to Nextcloud backup
[Service]
Type=oneshot
WorkingDirectory=/path/to/immich2nextcloud-backup
ExecStart=/usr/bin/docker compose run --rm immich2nextcloud-backupAnd a timer unit (/etc/systemd/system/immich-backup.timer):
[Unit]
Description=Run Immich backup daily
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.targetEnable with:
sudo systemctl daemon-reload
sudo systemctl enable --now immich-backup.timer| Code | Meaning |
|---|---|
0 |
Success — all users processed without failures |
1 |
Partial failure — one or more users failed during export or sync |
2 |
Fatal configuration error — missing required global env vars |
- Media-only backups. Albums, people, faces, and other Immich metadata are not backed up — only raw photo and video files.
- Default is full export every run. If no incremental env vars are set, each invocation performs a full export from Immich. You can enable incremental mode with
IMMICH_FROM_DATE_RANGEorIMMICH_INCREMENTAL_DAYS. - No restore tooling. Recovery is manual — your media files are simply present in Nextcloud as regular files.
- amd64 only. The Docker image downloads amd64 binaries for
rcloneandimmich-go.
- Never commit API keys, passwords, or other secrets to version control.
- Store credentials in an uncommitted
.envfile (already in.gitignore) or use Docker secrets. - The script creates temporary
rcloneconfig files containing obscured passwords and securely removes them after use (shredwhen available, otherwiserm). - Secrets are never printed to logs.
Exporting user media and storing app passwords is a privacy-sensitive operation. Make sure this is done under appropriate policies and that the host running this container is adequately protected.