Skip to content

Conversation

@durzo
Copy link

@durzo durzo commented Jan 17, 2026

Backup and Restore Feature for Nginx Proxy Manager

Overview

This contribution adds a complete backup and restore system to Nginx Proxy Manager, enabling administrators to export their entire configuration to a portable ZIP archive and restore it on the same or different instance.

The backup format is database-agnostic, allowing migration between any supported database backend (SQLite, MySQL, PostgreSQL).

The feature preserves all database records, SSL certificates (Let's Encrypt and custom), access list credentials, and automatically regenerates nginx configurations upon restore.

PR Notes

I created this because I run NPM in various places and have been bitten by things suddenly breaking, usually on upgrade. I know there are manual ways to completely backup the data and lets-encrypt folders but I wanted something native that was quick and easy while still doing it "right".

I have tested this for about a week locally, more testers are always good, but I believe it is ready for PR.

I understand this is a large one, and hopefully that wont put you off considering it.

I have not contributed to this repository before, so please forgive me if I have done something incorrectly (coding standards, etc).

I welcome your feedback!

Features

Export Functionality

  • Exports all database tables excluding soft-deleted records
  • Packages Let's Encrypt certificates including archive directories, renewal configs, DNS challenge credentials, and ACME account data
  • Packages custom SSL certificates
  • Packages htpasswd files for access lists
  • Optional password protection using ZIP encryption (ZipCrypto/zip20)
  • Generates timestamped ZIP files

Import Functionality

  • Validates backup version compatibility
  • Supports password-protected backups
  • Complete data replacement (purges existing configuration before restore)
  • Restores certificate files with proper directory structure
  • Recreates Let's Encrypt live directory symlinks pointing to archive files
  • Regenerates all nginx configurations for proxy hosts, redirection hosts, dead hosts, and streams
  • Tests and reloads nginx after restore
  • Forces user logout and session invalidation after restore

Audit Logging

  • Records export and import events in the audit log
  • Tracks table counts (users, certificates, hosts, etc.) in audit metadata

Screenshots

main-interface restore-interface

Architecture

Backend Components

backend/internal/backup.js - Core backup logic

Key constants:

  • BACKUP_VERSION - Format version for compatibility checking
  • TABLE_CONFIG - Defines export/import order respecting foreign key dependencies
  • DELETE_TABLE_ORDER - Reverse order for safe deletion
  • JSON_FIELDS - Fields requiring serialization for raw knex inserts

Key functions:

  • exportAll() - Creates backup ZIP with database JSON and certificate files
  • importAll() - Entry point for restore operation
  • performImport() - Clears database, restores files, imports tables
  • createBackupZip() - Assembles ZIP archive with all components
  • extractZipDirectory() - Extracts ZIP with optional password support
  • restoreCertificateFiles() - Restores Let's Encrypt and custom SSL files
  • createLiveSymlinks() - Recreates certbot-style symlinks from archive to live
  • restoreAccessListFiles() - Restores htpasswd files
  • purgeNginxConfigs() / purgeCertificateFiles() / purgeAccessListFiles() - Pre-import cleanup
  • regenerateNginxConfigs() - Rebuilds all nginx configs post-import

backend/routes/backup.js - REST API endpoints

  • GET /api/backup/export - Download backup (5 minute timeout)
  • POST /api/backup/import - Upload and restore backup (10 minute timeout)

Frontend Components

frontend/src/pages/Settings/Backup.tsx - React UI component

  • Export section with optional password protection
  • Password confirmation validation
  • Import section with file picker
  • Confirmation modal with password field for encrypted backups
  • Progress indicators during operations

frontend/src/api/backend/backup.ts - API client functions

  • exportBackup() - Downloads backup via blob response
  • importBackup() - Uploads backup via FormData

frontend/src/hooks/useBackup.ts - React Query mutations

  • useExportBackup() - Handles export with loading state
  • useImportBackup() - Handles import with automatic logout on success

Database Tables Handled

In dependency order:

  1. setting
  2. user
  3. auth
  4. user_permission
  5. certificate
  6. access_list
  7. access_list_auth
  8. access_list_client
  9. proxy_host
  10. redirection_host
  11. dead_host
  12. stream

Audit log is cleared but not exported (fresh start on restore).

Dependencies Added

{
  "archiver": "^5.3.0",
  "archiver-zip-encrypted": "^2.0.0",
  "unzipper": "^0.12.3"
}
  • archiver - ZIP creation with compression
  • archiver-zip-encrypted - ZIP encryption support (registered as custom format)
  • unzipper - ZIP extraction with password support

Note: The implementation uses ZipCrypto (zip20) encryption rather than AES-256 because the unzipper library only supports legacy ZIP encryption for extraction.

File Structure in Backup ZIP

npm-backup-{timestamp}.zip
├── database.json              # All database tables
├── letsencrypt/
│   ├── accounts/              # ACME account data
│   ├── archive/
│   │   └── npm-{id}/          # Certificate files (cert, chain, fullchain, privkey)
│   ├── live/
│   │   └── npm-{id}/
│   │       └── README         # Certbot README file
│   ├── renewal/
│   │   └── npm-{id}.conf      # Renewal configuration
│   ├── credentials/
│   │   └── credentials-{id}   # DNS challenge credentials
│   └── renewal-hooks/         # Renewal hook scripts
├── custom_ssl/
│   └── npm-{id}/              # Custom certificate directories
└── access/
    └── {id}                   # htpasswd files

Implementation Details

Raw Knex Inserts

The import uses raw knex inserts instead of Objection.js model methods to:

  • Preserve original IDs (avoids auto-increment conflicts)
  • Preserve hashed passwords (bypasses $beforeInsert hooks that would re-hash)
  • Preserve timestamps exactly as exported

JSON fields are manually stringified before insert since raw knex does not handle object serialization.

Symlink Recreation

Let's Encrypt certificates use a live/archive directory structure where live contains symlinks to the latest versioned files in archive. The restore process:

  1. Copies archive directory with versioned files (e.g., fullchain1.pem, fullchain2.pem)
  2. Scans for highest version number per file type
  3. Creates relative symlinks in live directory (e.g., fullchain.pem -> ../../archive/npm-X/fullchain2.pem)

Session Handling

After successful import:

  • Frontend clears AuthStore and React Query cache
  • User is redirected to login page after 1 second delay

Files Modified/Added

File Change Type
backend/internal/backup.js Added
backend/routes/backup.js Added
backend/routes/main.js Modified (route registration)
backend/logger.js Modified (added backup logger)
backend/package.json Modified (dependencies)
frontend/src/api/backend/backup.ts Added
frontend/src/api/backend/index.ts Modified (export)
frontend/src/hooks/useBackup.ts Added
frontend/src/hooks/index.ts Modified (export)
frontend/src/pages/Settings/Backup.tsx Added
frontend/src/pages/Settings/Layout.tsx Modified (navigation)
frontend/src/locale/src/en.json Modified (i18n strings)
frontend/src/components/Table/Formatter/EventFormatter.tsx Modified (audit events)
test/cypress/e2e/api/Backup.cy.js Added
test/cypress/plugins/backendApi/client.js Modified (buffer methods)
test/cypress/plugins/backendApi/task.js Modified (buffer tasks)

Localization Strings Added

The following i18n keys were added to frontend/src/locale/src/en.json:

Key Default Message
object.event.exported Exported {object}
object.event.imported Imported {object}
settings.backup Backup & Restore
settings.backup.export.button Export Backup
settings.backup.export.description Download a backup of all your configuration including hosts, access lists, certificates, users, and settings.
settings.backup.export.password.confirm Confirm password
settings.backup.export.password.enable Protect with password
settings.backup.export.password.label Password
settings.backup.export.password.mismatch Passwords do not match
settings.backup.export.secrets-warning This backup may contain sensitive data including SSL certificates, private keys, DNS provider credentials, and htpasswd files. Consider using password protection.
settings.backup.export.success Backup exported successfully
settings.backup.export.title Export Configuration
settings.backup.import.confirm.button Import Backup
settings.backup.import.confirm.file File
settings.backup.import.confirm.logout You will be logged out and required to log in again after the import is complete.
settings.backup.import.confirm.message Are you sure you want to import this backup? All existing hosts, access lists, certificates, users, and settings will be replaced.
settings.backup.import.confirm.title Confirm Import
settings.backup.import.confirm.warning This will permanently delete all existing configuration!
settings.backup.import.description Restore configuration from a previously exported backup file.
settings.backup.import.password.hint Enter password if the backup is encrypted
settings.backup.import.password.label Password (if encrypted)
settings.backup.import.progress Importing backup... This may take a few minutes.
settings.backup.import.success Backup imported successfully. You may need to refresh the page.
settings.backup.import.title Import Configuration
settings.backup.import.warning Warning: Importing a backup will replace ALL existing configuration data. This action cannot be undone.

Security Considerations

  • Backups contain sensitive data (private keys, credentials, hashed passwords)
  • Password protection is optional but recommended for backups stored externally
  • Import & Export require settings:update (admin) permission
  • Credential files are restored with restricted permissions (0600)
  • Users are warned that backup files should be treated as sensitive in the export UI

Testing

Cypress E2E Test

test/cypress/e2e/api/Backup.cy.js - Full backup/restore cycle test

The test performs a complete integration test of the backup feature:

  1. Setup Phase: Creates one resource of every type:

    • Non-admin user
    • Custom certificate with uploaded files
    • Access list with htpasswd credentials and client rules
    • Proxy host (linked to certificate and access list)
    • Redirection host
    • Dead host (404 host)
    • Stream
  2. Export Phase: Downloads backup ZIP and stores it in memory

  3. Deletion Phase: Deletes all created resources to simulate data loss

  4. Import Phase: Uploads the backup ZIP to restore all data

  5. Verification Phase: Re-authenticates and verifies all resources were restored with correct data

  6. Cleanup Phase: Deletes all restored resources in the after() hook to ensure subsequent test suites start with a clean database state

Test Infrastructure Modifications

test/cypress/plugins/backendApi/client.js - Added buffer handling methods:

Method Purpose
getBuffer(path) GET request returning raw Buffer for file downloads (backup export)
postBuffer(path, buffer, fieldName, fileName) POST request with Buffer as multipart file upload (backup import)

test/cypress/plugins/backendApi/task.js - Added Cypress tasks:

Task Purpose
backendApiGetBuffer Downloads backup file, returns {data: number[], length: number} for Cypress serialization
backendApiPostBuffer Uploads backup file from array buffer, converts back to Buffer before sending

Note: Cypress tasks cannot pass Buffer objects directly between the Node.js plugin process and the browser, so the buffer is converted to/from a number array for serialization.

Linked issues

#2109 #4373 #4752

@durzo
Copy link
Author

durzo commented Jan 18, 2026

@jc21 the jenkins job has been stuck running for 6+ hours but failed to install cypress, are you able to stop it running? there does not appear to be a configured global timeout.

@durzo durzo marked this pull request as draft January 22, 2026 21:34
@durzo durzo marked this pull request as ready for review January 22, 2026 21:34
@durzo durzo force-pushed the feat-backup-restore branch from ee0dd42 to 39442bc Compare January 22, 2026 21:41
@durzo
Copy link
Author

durzo commented Jan 22, 2026

every time I trigger the CI it fails in a different area. i have squashed and force pushed without any actual changes.

Run 6 is now failing on a random apt install because of unexpected file sizes.

I'm running out of ideas here. as far as I'm concerned the CI tests are all passing and this is definitely ready for review/integration.

@durzo durzo force-pushed the feat-backup-restore branch from 39442bc to 4aaff28 Compare January 22, 2026 22:07
@nginxproxymanagerci
Copy link

CI Error:

/bin/bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8): No such file or directory
certbot-node: Pulling from nginxproxymanager/nginx-full
Digest: sha256:365ace03c4c9343a52d03c7c3db643827ad5d52a1b73202d61409f767b9f781a
Status: Image is up to date for nginxproxymanager/nginx-full:certbot-node
docker.io/nginxproxymanager/nginx-full:certbot-node
�[1;34m❯ �[1;36mBuilding Frontend ...�[0m
yarn install v1.22.22
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning " > @uiw/react-textarea-code-editor@3.1.1" has unmet peer dependency "@babel/runtime@>=7.10.0".
[4/4] Building fresh packages...
Done in 26.97s.
yarn run v1.22.22
$ biome lint
Checked 224 files in 76ms. No fixes applied.
Done in 0.15s.
yarn run v1.22.22
$ formatjs compile-folder src/locale/src src/locale/lang
Done in 0.26s.
yarn run v1.22.22
$ /app/frontend/node_modules/.bin/vitest run --no-color

 RUN  v4.0.6 /app/frontend

$ formatjs compile-folder src/locale/src src/locale/lang


⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/locale/Utils.test.tsx [ src/locale/Utils.test.tsx ]
Error: Failed to parse JSON file, invalid JSON syntax found at position -1
  Plugin: vite:json
  File: /app/frontend/src/locale/lang/ga.json
 ❯ TransformPluginContext._formatLog node_modules/vite/dist/node/chunks/config.js:31106:43
 ❯ TransformPluginContext.error node_modules/vite/dist/node/chunks/config.js:31103:14
 ❯ TransformPluginContext.handler node_modules/vite/dist/node/chunks/config.js:8803:11
 ❯ EnvironmentPluginContainer.transform node_modules/vite/dist/node/chunks/config.js:30905:51
 ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:26043:26

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯


 Test Files  1 failed (1)
      Tests  no tests
   Start at  22:08:02
   Duration  406ms (transform 62ms, setup 59ms, collect 0ms, tests 0ms, environment 148ms, prepare 3ms)

$ ./src/locale/scripts/locale-sort.sh
bg.json is already sorted
de.json is already sorted
en.json is already sorted
es.json is already sorted
fr.json is already sorted
ga.json is already sorted
id.json is already sorted
it.json is already sorted
ja.json is already sorted
ko.json is already sorted
nl.json is already sorted
pl.json is already sorted
ru.json is already sorted
sk.json is already sorted
tr.json is already sorted
vi.json is already sorted
zh.json is already sorted

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

@durzo durzo force-pushed the feat-backup-restore branch from 4aaff28 to 9e92551 Compare January 22, 2026 22:34
@durzo
Copy link
Author

durzo commented Jan 22, 2026

no changes, just a new force push. CI run 7 fails again in a different area.

🤞 for run 8

@durzo durzo force-pushed the feat-backup-restore branch from 9e92551 to e69d09f Compare January 23, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant