diff --git a/database.go b/database.go
index 892de2eb9520d6027f70d1771048e00773638dfd..d698426b38779f943d00959b7e50005cb694f3f6 100644
--- a/database.go
+++ b/database.go
@@ -7,6 +7,7 @@ import (
 	"encoding/gob"
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"math/rand"
 	"os"
@@ -400,6 +401,61 @@ func (db *Database) Scan(bucket []byte) *DatabaseIterator {
 	return &DatabaseIterator{iter: it}
 }
 
+func writeBytes(w io.Writer, b []byte) error {
+	binary.Write(w, binary.LittleEndian, uint32(len(b)))
+	_, err := w.Write(b)
+	return err
+}
+
+func readBytes(r io.Reader) ([]byte, error) {
+	var sz uint32
+	if err := binary.Read(r, binary.LittleEndian, &sz); err != nil {
+		return nil, err
+	}
+	b := make([]byte, sz)
+	_, err := r.Read(b)
+	return b, err
+}
+
+// Dump the contents of the database to a Writer.
+func (db *Database) Dump(w io.Writer) error {
+	it := db.ldb.NewIterator(nil, &ldbopt.ReadOptions{DontFillCache: true})
+	defer it.Release()
+	count := 0
+	for it.Next() {
+		writeBytes(w, it.Key())
+		writeBytes(w, it.Value())
+		count++
+	}
+	log.Printf("dumped %d entries from the database", count)
+	return nil
+}
+
+// Restore a backup to the current database (assuming it is empty).
+func (db *Database) Restore(r io.Reader) error {
+	count := 0
+	for {
+		key, err := readBytes(r)
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return err
+		}
+		value, err := readBytes(r)
+		if err == io.EOF {
+			return errors.New("unexpected eof")
+		}
+		if err != nil {
+			return err
+		}
+		db.RawPut(key, value)
+		count++
+	}
+	log.Printf("restored %d entries to the database", count)
+	return db.Reindex()
+}
+
 // Reindex the entire database. This is an administrative operation,
 // to be performed after an incompatible index schema change. It will
 // delete the existing index and re-create it from scratch.
diff --git a/database_test.go b/database_test.go
index 6755d36a6a032487306070869982e13829c493bd..0f419ebc99ec7ee348ae24afa0e9d4b69e17f088 100644
--- a/database_test.go
+++ b/database_test.go
@@ -1,6 +1,7 @@
 package liber
 
 import (
+	"bytes"
 	"io"
 	"io/ioutil"
 	"os"
@@ -20,6 +21,15 @@ func (td *testDatabase) Close() {
 	os.RemoveAll(td.path)
 }
 
+func newEmptyTestDatabase(t *testing.T) (*testDatabase, *Database) {
+	path, _ := ioutil.TempDir("", "testdb-")
+	db, err := NewDb(path)
+	if err != nil {
+		t.Fatalf("NewDb(): %v", err)
+	}
+	return &testDatabase{db: db, path: path}, db
+}
+
 func newTestDatabase(t *testing.T) (*testDatabase, *Database) {
 	path, _ := ioutil.TempDir("", "testdb-")
 	db, err := NewDb(path)
@@ -251,6 +261,32 @@ func TestDatabase_Reindex(t *testing.T) {
 	doTest()
 }
 
+func TestDatabase_DumpAndRestore(t *testing.T) {
+	td, db := newTestDatabase2(t)
+	defer td.Close()
+	book, _ := db.GetBook(td.refbookid)
+	m := book.Metadata
+
+	var buf bytes.Buffer
+	if err := db.Dump(&buf); err != nil {
+		t.Fatalf("Dump: %v", err)
+	}
+
+	td2, db2 := newEmptyTestDatabase(t)
+	defer td2.Close()
+
+	if err := db2.Restore(&buf); err != nil {
+		t.Fatalf("Restore: %v", err)
+	}
+
+	// Find the sample book.
+	if result, err := db2.Find(m.Uniques()); err != nil {
+		t.Errorf("Not found: %v", err)
+	} else if result.Id != book.Id {
+		t.Errorf("Bad match with ISBN: got=%d, expected=%d", result.Id, book.Id)
+	}
+}
+
 // func TestDatabase_Find2(t *testing.T) {
 // 	td, db := newTestDatabase(t)
 // 	defer td.Close()
diff --git a/web.go b/web.go
index 62316d35338f8331991488301e3b8598a5f2cece..ce657228d3dcf48c130e8258205986815e35b264 100644
--- a/web.go
+++ b/web.go
@@ -12,6 +12,7 @@ import (
 	_ "image/png"
 	"io"
 	"log"
+	"net"
 	"net/http"
 	"net/url"
 	"os"
@@ -324,6 +325,19 @@ func (s *uiServer) handleShowThumbnail(book *Book, w http.ResponseWriter, req *h
 	w.Write(data)
 }
 
+func (s *uiServer) handleDumpDatabase(w http.ResponseWriter, req *http.Request) {
+	// Only allow requests from localhost.
+	host, _, _ := net.SplitHostPort(req.RemoteAddr)
+	ip := net.ParseIP(host)
+	if !ip.IsLoopback() {
+		http.Error(w, "Unauthorized", http.StatusUnauthorized)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/octet-stream")
+	s.db.Dump(w)
+}
+
 func (s *uiServer) handleHome(w http.ResponseWriter, req *http.Request) {
 	render("index.html", w, nil)
 }
@@ -376,6 +390,7 @@ func NewHttpServer(db *Database, storage, cache *RWFileStorage, addr string) *ht
 		http.FileServer(http.Dir(filepath.Join(*htdocsDir, "static")))))
 	r.HandleFunc("/api/sync/upload", syncsrv.handleSyncUpload).Methods("POST")
 	r.HandleFunc("/api/sync/diff", syncsrv.handleDiffRequest).Methods("POST")
+	r.HandleFunc("/internal/dump_db", uisrv.handleDumpDatabase)
 	r.Handle("/book/cover/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowCover))
 	r.Handle("/book/thumb/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowThumbnail))
 	r.Handle("/book/{id:[0-9]+}", uisrv.withBook(uisrv.handleShowBook))