This page covers database backup and restore procedures, filesystem backup strategy, maintenance mode, storage folder organization, and routine maintenance jobs for Immich deployments.
For deployment configuration, see page 5.4 (Configuration Options). For database schema details, see page 7.3 (Database Schema).
A complete Immich backup must cover two distinct data stores:
UPLOAD_LOCATION.Immich stores file paths in the database and does not scan the upload folder on startup. The database must be backed up independently of the files.
DatabaseBackupService (server/src/services/database-backup.service.ts) manages automated PostgreSQL dumps using pg_dumpall, compressed with gzip and stored as .sql.gz files in StorageFolder.Backups.
Settings are managed at Administration > Settings > Backup in the web UI.
| Config Key | Default | Description |
|---|---|---|
backup.database.enabled | true | Enable automated backups |
backup.database.cronExpression | '0 2 * * *' | Schedule (2:00 AM daily) |
backup.database.keepLastAmount | 14 | Number of backups to retain |
On the ConfigInit event, DatabaseBackupService.onConfigInit() attempts to acquire DatabaseLock.BackupDatabase via databaseRepository.tryLock(). Only the instance holding this lock creates the cron job, preventing duplicate backups in multi-instance deployments. onConfigUpdate() updates the cron schedule whenever backup settings change.
Sources: server/src/services/database-backup.service.ts55-88
Diagram: DatabaseBackupService job pipeline
Sources: server/src/services/database-backup.service.ts90-104
Backup files follow this naming pattern, stored inside UPLOAD_LOCATION/backups/:
immich-db-backup-{YYYYMMDDTHHmmss}-v{immichVersion}-pg{postgresVersion}.sql.gz
Example:
immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz
Helper functions in server/src/utils/database-backups.ts validate and parse filenames:
| Function | Purpose |
|---|---|
isValidDatabaseBackupName() | Accepts any .sql.gz or .sql file |
isValidDatabaseRoutineBackupName() | Matches the automated naming pattern (new and old style) |
isFailedDatabaseBackupName() | Matches .sql.gz.tmp partial files |
findDatabaseBackupVersion() | Extracts the Immich version string from a filename |
The old backup style (immich-db-backup-{unixTimestamp}.sql.gz) is also recognized during cleanup.
Sources: server/src/utils/database-backups.ts1-24 server/src/services/database-backup.service.ts105-115
After each successful backup, cleanupDatabaseBackups() reads the backup directory, separates routine backups from uploaded backups, sorts routine backups newest-first, and deletes any beyond keepLastAmount. All .sql.gz.tmp partial files are always deleted unconditionally.
Sources: server/src/services/database-backup.service.ts116-160
The resulting backup appears in UPLOAD_LOCATION/backups/ and counts toward the retention limit.
Immich provides three restore paths:
| Method | When to Use |
|---|---|
| Web UI — Settings | Existing installation with access to the admin panel |
| Web UI — Onboarding wizard | Fresh installation importing data from a previous instance |
| Command line | Automated recovery, scripting, or advanced scenarios |
The restore process:
Sources: docs/docs/administration/backup-and-restore.md39-59
Used when setting up Immich on a new host and importing data from a previous instance.
UPLOAD_LOCATION subdirectories (backups, encoded-video, library, profile, thumbs, upload) into the new UPLOAD_LOCATION.docker compose up -dStorageFolder..sql.gz file.Sources: docs/docs/administration/backup-and-restore.md61-100
A .sql.gz backup file can be uploaded directly through the web UI:
.sql.gz fileuploaded- prefixUpload is handled by DatabaseBackupService.uploadBackup(), which stores the file in StorageFolder.Backups.
Sources: server/src/services/database-backup.service.ts161-200
For automation or advanced recovery. The search_path fix is required for compatibility with Immich's PostgreSQL schema setup.
Manual backup (Linux):
Restore (Linux):
If the Immich server has already run against the new containers before the restore, set
DB_SKIP_MIGRATIONS=truebefore starting services. Remove it after the restore completes.
Sources: docs/docs/administration/backup-and-restore.md134-209
The filename encodes the Immich version and PostgreSQL major.minor version. The UI displays compatibility indicators via findDatabaseBackupVersion():
| Indicator | Meaning |
|---|---|
| ✅ | Backup version matches the current Immich version |
| ⚠️ (yellow) | Backup from a different Immich version — migrations may run automatically |
| ⚠️ (red) | Version could not be determined from the filename |
Sources: server/src/utils/database-backups.ts16-18
Maintenance mode is a special server state used during database restore operations. When active, the server runs MaintenanceWorkerService and MaintenanceWorkerController instead of the normal application stack.
Diagram: Maintenance mode service and controller structure
Sources: server/src/maintenance/maintenance-worker.service.ts40-88 server/src/maintenance/maintenance-worker.controller.ts40-45 server/src/types.ts474-476
MaintenanceWorkerService.init() runs when the maintenance worker starts:
SystemMetadataKey.MaintenanceMode from the database to recover the one-time JWT secret and any pending action.StorageCore.setMediaLocation() using detectMediaLocation(), which probes candidate paths (/data, /usr/src/app/upload) to find the active media location.action is stored in the metadata (e.g., RestoreDatabase), calls runAction() to execute it.Sources: server/src/maintenance/maintenance-worker.service.ts67-88
The MaintenanceWorkerController exposes the following routes (all require the maintenance JWT cookie):
| Route | Handler | Purpose |
|---|---|---|
GET /server/config | getServerConfig() | Returns { maintenanceMode: true } |
GET /server/version | getServerVersion() | Current server version |
GET /admin/database-backups | listDatabaseBackups() | List available backup files |
GET /admin/database-backups/:filename | downloadDatabaseBackup() | Download a backup file |
DELETE /admin/database-backups | deleteDatabaseBackup() | Delete one or more backups |
POST /admin/database-backups/upload | uploadDatabaseBackup() | Upload a .sql.gz file |
GET /admin/maintenance/status | maintenanceStatus() | Current action and progress |
GET /admin/maintenance/detect-install | detectPriorInstall() | Storage integrity checks |
POST /admin/maintenance/login | login() | Authenticate with the maintenance secret |
POST /admin/maintenance/restore | restore() | Trigger a database restore |
Sources: server/src/maintenance/maintenance-worker.controller.ts40-160
During the onboarding restore flow, detectPriorInstall() checks each StorageFolder for readability and reports file counts. The result is reflected in SystemFlags.mountChecks (Record<StorageFolder, boolean>).
Sources: server/src/types.ts473 server/src/maintenance/maintenance-worker.service.ts157-175
Immich organizes files under UPLOAD_LOCATION into subfolders corresponding to the StorageFolder enum:
Diagram: UPLOAD_LOCATION directory tree mapped to StorageFolder enum values
| Folder | StorageFolder | Contents | Critical? | Regenerable? |
|---|---|---|---|---|
upload/ | Upload | Original files (storage template off) | ✅ | No |
library/ | Library | Original files (storage template on) | ✅ | No |
profile/ | Profile | User avatar images | ✅ | No |
backups/ | Backups | Automated database dumps | ✅ | No |
thumbs/ | Thumbnails | Generated thumbnails and previews | — | Yes |
encoded-video/ | EncodedVideo | Transcoded video files | — | Yes |
PostgreSQL data at DB_DATA_LOCATION is always critical and must be backed up either through database dumps or volume snapshots.
Sources: docs/docs/administration/backup-and-restore.md211-314
When the storage template engine is disabled (default since v1.92.0):
UPLOAD_LOCATION/upload/{userId}/UPLOAD_LOCATION/library/ is unused for new assetsWhen the storage template engine is enabled:
UPLOAD_LOCATION/library/{userId}/ (or library/{storageLabel}/ if a storage label is set for the user)library/ and new assets go to upload/The StorageTemplateMigration job moves all existing assets to match the current template when run manually.
Sources: docs/docs/administration/backup-and-restore.md229-309
To fully recover an Immich instance:
UPLOAD_LOCATION/backups/UPLOAD_LOCATION/upload/ and/or UPLOAD_LOCATION/library/UPLOAD_LOCATION/profile/Thumbnails (thumbs/) and encoded videos (encoded-video/) can be regenerated after restore by running the thumbnail generation and video transcoding jobs from Administration > Job Queues.
When the database and files cannot be backed up atomically:
immich-server during backup to guarantee full consistency.Sources: docs/docs/administration/backup-and-restore.md316-322
For off-host backup automation, the community template uses Borg to version and deduplicate backups:
pg_dumpall writes a database dump to UPLOAD_LOCATION/database-backup/UPLOAD_LOCATION, excluding regenerable folders (thumbs/, encoded-video/)This approach ensures the dump and asset files are always captured together. When using this script the built-in Immich database backup can be disabled to avoid duplicate storage.
Sources: docs/docs/guides/template-backup-script.md1-91
The following JobName entries run on a schedule or as event-driven cleanup:
JobName | QueueName | Purpose |
|---|---|---|
DatabaseBackup | BackupDatabase | Scheduled and on-demand database dump |
AuditLogCleanup | Background | Prune old audit log entries |
SessionCleanup | Background | Remove expired sessions |
TagCleanup | Background | Remove orphaned tags |
PersonCleanup | Background | Remove orphaned person records |
MemoryCleanup | Background | Clean expired memories |
MemoryGenerate | Background | Generate new "On This Day" memories |
NotificationsCleanup | Background | Prune old notifications |
UserDeleteCheck | Background | Process pending user deletions |
Sources: server/src/types.ts288-392
All jobs can be triggered from Administration > Job Queues > Create job. The DatabaseBackup job creates an on-demand backup that counts toward the retention limit. PersonCleanup, TagCleanup, and UserDeleteCheck are useful after bulk delete operations that may leave orphaned records.
Immich uses database migrations to evolve the schema across versions while preserving data integrity.
The database schema is managed through a migration system that applies incremental changes:
Key Migration Principles:
| Principle | Implementation |
|---|---|
| Forward-only | Migrations apply in order, no automatic rollback |
| Idempotent | Safe to re-run if interrupted |
| Data preservation | Never delete data without explicit backup |
| Version compatibility | Database must support current and previous app version during upgrades |
Sources: docker/docker-compose.yml59-72
Before Upgrading:
BackupDatabase jobDuring Upgrade:
After Upgrade:
Vector Extension Migrations:
The database includes vector search extensions that may require updates:
Vector extension upgrades must be coordinated with schema migrations to ensure compatibility.
Sources: docker/docker-compose.yml59 docker/docker-compose.prod.yml66
Immich maintains audit tables to track changes for mobile synchronization. These tables require periodic cleanup to prevent unbounded growth.
Audit Table Purposes:
| Audit Table | Tracks | Retention Period |
|---|---|---|
album_audit | Album creation/updates/deletion | Until client sync + 30 days |
asset_audit | Asset deletions only | Until client sync + 30 days |
user_audit | User profile changes | Until client sync + 30 days |
partner_audit | Partner relationship changes | Until client sync + 30 days |
album_user_audit | Album membership changes | Until client sync + 30 days |
Sources: Overview diagram architecture
Audit records can be safely deleted after all clients have synchronized past them:
Manual Audit Cleanup:
Currently, audit table cleanup must be performed manually via SQL:
Automated Cleanup Considerations:
For production deployments with many mobile clients:
Sources: Overview diagram showing audit table architecture
Immich includes automated and manual maintenance jobs for system health.
Sources: server/src/services/job.service.ts11-41 server/src/services/library.service.ts35-77 server/src/services/notification.service.ts76-79
Manual maintenance jobs can be triggered via the job creation endpoint:
Job Types and Mappings:
| API Job Name | Internal Job Name | Purpose |
|---|---|---|
TagCleanup | JobName.TagCleanup | Remove orphaned tags |
PersonCleanup | JobName.PersonCleanup | Remove orphaned person records |
UserCleanup | JobName.UserDeleteCheck | Process pending user deletions |
MemoryCleanup | JobName.MemoryCleanup | Clean up expired memories |
MemoryCreate | JobName.MemoryGenerate | Generate new memories |
BackupDatabase | JobName.DatabaseBackup | Trigger manual backup |
Job Execution Flow:
Sources: server/src/services/job.service.ts44-187
Library Deletion Cleanup:
The library deletion process is asynchronous and handled in chunks to avoid overwhelming the system:
Sources: server/src/services/library.service.ts355-393 server/src/constants.ts
Immich provides job monitoring and error notification capabilities for maintenance awareness.
Sources: server/src/services/job.service.ts49-63 server/src/services/notification.service.ts81-109
When database backups fail, the system creates a notification for the admin user:
Notification Creation:
Notification Structure:
NotificationType.JobFailedNotificationLevel.ErrorSources: server/src/services/notification.service.ts81-109
Docker Compose includes health checks for critical services:
Sources: docker/docker-compose.yml50-56 e2e/docker-compose.yml52-57
To restore from an Immich database backup:
Prerequisites:
.sql.gz format)Restoration Steps:
Database Connection Parameters:
The backup uses pg_dumpall which includes all databases, roles, and schemas. Restoration requires:
DB_URL environment variableDB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASE_NAMEExample Restoration Commands:
Sources: server/src/services/backup.service.ts76-196
Version Compatibility:
Backups include version information in the filename for compatibility tracking:
Vector Extension Considerations:
The database includes vector search extensions (vectorchord, pgvectors) that must be available during restoration:
Ensure the PostgreSQL image includes the same or compatible vector extensions.
Sources: docker/docker-compose.yml59 server/src/services/backup.service.ts106-117
Monitor disk usage for critical storage locations:
Orphaned files may accumulate when assets are deleted or moved. The system handles cleanup through job processing:
Asset Deletion Flow:
File Types Cleaned:
AssetFileType.Original - Original uploaded fileAssetFileType.Preview - Preview thumbnailAssetFileType.Thumbnail - Small thumbnailAssetFileType.EncodedVideo - Transcoded videoAssetFileType.Sidecar - XMP sidecar fileSources: server/src/enum.ts
The storage template migration system moves files according to configuration:
Sources: server/src/services/storage-template.service.ts129-148 server/src/services/job.service.ts83-88
Database Backup Failures:
| Issue | Symptom | Solution |
|---|---|---|
| Unsupported PostgreSQL version | Backup fails with error | Verify PostgreSQL version 14-18 |
| Permission denied | Cannot write to backup directory | Check volume mount permissions |
| Disk space exhausted | Backup .tmp files remain | Free disk space, cleanup will run |
| Lock contention | Multiple instances attempt backup | Only one instance should hold lock |
Job Queue Backlog:
Monitor queue status through the admin API:
/api/jobs/queues - View queue statisticsjobCounts.waiting and jobCounts.activeLibrary Sync Issues:
Sources: server/src/services/library.service.ts79-159 server/src/services/library.service.ts279-317
Service Logs:
Job Error Logs:
Job errors are logged by the JobService and also create notifications:
EventRepository emits for job lifecycleSources: server/src/services/job.service.ts49-63
After System Failure:
Verify Service Status
docker compose psdocker compose ps --format jsonVerify Database
docker exec immich_postgres pg_isreadyVerify Storage
Verify Job Processing
Verify Assets
Sources: docker/docker-compose.yml32-34 docker/docker-compose.yml52-56
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.