diff --git a/trac/db/api.py b/trac/db/api.py
index 829b7e6..715c32d 100644
--- a/trac/db/api.py
+++ b/trac/db/api.py
@@ -16,6 +16,7 @@
 
 import os
 import urllib
+import time
 
 from trac.config import Option, IntOption
 from trac.core import *
@@ -48,6 +49,9 @@ class IDatabaseConnector(Interface):
     def to_sql(table):
         """Return the DDL statements necessary to create the specified table,
         including indices."""
+        
+    def backup(dest):
+        """Backup the database to a location defined by trac.backup_dir"""
 
 
 class DatabaseManager(Component):
@@ -59,6 +63,9 @@ class DatabaseManager(Component):
         [wiki:TracEnvironment#DatabaseConnectionStrings string] for this
         project""")
 
+    backup_dir = Option('trac', 'backup_dir', 'db',
+        """Database backup location""")
+
     timeout = IntOption('trac', 'timeout', '20',
         """Timeout value for database connection, in seconds.
         Use '0' to specify ''no timeout''. ''(Since 0.11)''""")
@@ -81,6 +88,22 @@ class DatabaseManager(Component):
             self._cnx_pool.shutdown(tid)
             if not tid:
                 self._cnx_pool = None
+                
+    def backup(self, dest=None):
+        connector, args = self._get_connector()
+        if not dest:
+            backup_dir = self.backup_dir
+            if backup_dir[0] != "/":
+                backup_dir = os.path.join(self.env.path, backup_dir)
+            db_str = self.config.get('trac', 'database')
+            db_name, db_path = db_str.split(":",1)
+            dest_name = '%s.%i.%d.bak' % (db_name, self.env.get_version(),int(time.time()))
+            dest = os.path.join(backup_dir, dest_name)
+        else:
+            backup_dir = os.path.dirname(dest)
+        if not os.path.exists(backup_dir):
+            os.makedirs(backup_dir)
+        connector.backup(dest)
 
     def _get_connector(self): ### FIXME: Make it public?
         scheme, args = _parse_db_str(self.connection_uri)
diff --git a/trac/db/mysql_backend.py b/trac/db/mysql_backend.py
index e69a83c..724030c 100644
--- a/trac/db/mysql_backend.py
+++ b/trac/db/mysql_backend.py
@@ -142,6 +142,8 @@ class MySQLConnector(Component):
                   '_'.join(index.columns), table.name,
                   self._collist(table, index.columns))
 
+    def backup(self, dest):
+        raise TracError("MySQL backup not implemented")
 
 class MySQLConnection(ConnectionWrapper):
     """Connection wrapper for MySQL."""
diff --git a/trac/db/postgres_backend.py b/trac/db/postgres_backend.py
index 9c7b3b2..2df6e02 100644
--- a/trac/db/postgres_backend.py
+++ b/trac/db/postgres_backend.py
@@ -14,12 +14,14 @@
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
-import re
+import re, sys, os, time
 
 from trac.core import *
-from trac.db.api import IDatabaseConnector
+from trac.config import Option
+from trac.db.api import IDatabaseConnector, _parse_db_str
 from trac.db.util import ConnectionWrapper
 from trac.util import get_pkginfo
+from subprocess import Popen, PIPE
 
 psycopg = None
 PgSQL = None
@@ -33,6 +35,11 @@ class PostgreSQLConnector(Component):
 
     implements(IDatabaseConnector)
 
+    pg_dump_bin = Option('trac', 'pg_dump_bin', 'pg_dump',
+        """Location of pg_dump for Postgres database backups""")
+    compression = Option('trac', 'backup_compression', '8',
+        """Gzip backup compression level (if supported by backend)""")
+
     def __init__(self):
         self._version = None
 
@@ -65,8 +72,12 @@ class PostgreSQLConnector(Component):
         cnx = self.get_connection(path, user, password, host, port, params)
         cursor = cnx.cursor()
         if cnx.schema:
-            cursor.execute('CREATE SCHEMA "%s"' % cnx.schema)
-            cursor.execute('SET search_path TO %s', (cnx.schema,))
+            try:
+                cursor.execute('CREATE SCHEMA "%s"' % cnx.schema)
+                cursor.execute('SET search_path TO %s', (cnx.schema,))
+                cnx.commit()
+            except Exception, e:
+                cnx.rollback()
         from trac.db_default import schema
         for table in schema:
             for stmt in self.to_sql(table):
@@ -93,6 +104,40 @@ class PostgreSQLConnector(Component):
             yield 'CREATE %s INDEX "%s_%s_idx" ON "%s" ("%s")' % (unique, table.name, 
                    '_'.join(index.columns), table.name, '","'.join(index.columns))
 
+    def backup(self, dest_file):
+        # pg_dump -n schemaname dbname | gzip > filename.gz
+        db_url = self.env.config.get('trac', 'database')
+        scheme, db_prop = _parse_db_str(db_url)
+        db_name = os.path.basename(db_prop['path'])
+        args = [self.pg_dump_bin, '-C', '-d', '-x', '-Z', self.compression,
+                '-U', db_prop['user'],
+                '-h', db_prop['host'],
+                '-p', str(db_prop['port'])]
+
+        if 'schema' in db_prop['params']:
+            args.extend(['-n', db_prop['params']['schema'], db_name])
+        else:
+            args.extend([db_name])
+
+        dest_file = "%s.gz" % (dest_file,)
+        args.extend(['>', dest_file])
+        if sys.platform == 'win':
+            # XXX TODO verify on windows
+            args = ['cmd', '/c', ' '.join(args)]
+        else:
+            args = ['bash', '-c', ' '.join(args)]
+        
+        environ = os.environ.copy()
+        environ['PGPASSWORD'] = db_prop['password']
+        #print >> sys.stderr, "backup command %r" % (args,)
+        #print >> sys.stderr, "backup props %r" % (db_prop,)
+        #print >> sys.stderr, "backup to %s" % dest_file
+        p = Popen(args, env=environ, shell=False, bufsize=0, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=True)
+        p.wait()
+        p.stdout.close()
+        p.stderr.close()
+        if not os.path.exists(dest_file):
+            raise TracError("Backup attempt failed")
 
 class PostgreSQLConnection(ConnectionWrapper):
     """Connection wrapper for PostgreSQL."""
diff --git a/trac/db/sqlite_backend.py b/trac/db/sqlite_backend.py
index 84a8dd9..bc69ee1 100644
--- a/trac/db/sqlite_backend.py
+++ b/trac/db/sqlite_backend.py
@@ -146,6 +146,18 @@ class SQLiteConnector(Component):
     def to_sql(cls, table):
         return _to_sql(table)
 
+    def backup(self, dest_file):
+        """Simple SQLite-specific backup of the database.
+
+        @param dest: Destination file; if not specified, the backup is stored in
+                     a file called db_name.trac_version.bak
+        """
+        import shutil
+        db_str = self.config.get('trac', 'database')
+        db_name = os.path.join(self.env.path, db_str[7:])
+        shutil.copy(db_name, dest_file)
+        if not os.path.exists(dest_file):
+            raise TracError("Backup attempt failed")
 
 class SQLiteConnection(ConnectionWrapper):
     """Connection wrapper for SQLite."""
diff --git a/trac/env.py b/trac/env.py
index a579397..c0ec4c2 100644
--- a/trac/env.py
+++ b/trac/env.py
@@ -414,15 +414,7 @@ class Environment(Component, ComponentManager):
         @param dest: Destination file; if not specified, the backup is stored in
                      a file called db_name.trac_version.bak
         """
-        import shutil
-
-        db_str = self.config.get('trac', 'database')
-        if not db_str.startswith('sqlite:'):
-            raise TracError(_('Can only backup sqlite databases'))
-        db_name = os.path.join(self.path, db_str[7:])
-        if not dest:
-            dest = '%s.%i.bak' % (db_name, self.get_version())
-        shutil.copy (db_name, dest)
+        DatabaseManager(self).backup(dest)
 
     def needs_upgrade(self):
         """Return whether the environment needs to be upgraded."""
diff --git a/trac/tests/backup.py b/trac/tests/backup.py
new file mode 100644
index 0000000..53f8b56
--- /dev/null
+++ b/trac/tests/backup.py
@@ -0,0 +1,43 @@
+import trac
+from trac import db_default
+from trac.db import sqlite_backend
+from trac.env import Environment
+
+import os.path
+import unittest
+import tempfile
+import shutil
+
+from trac.tests.functional.testenv import FunctionalTestEnvironment
+
+class DatabaseBackupTestCase(unittest.TestCase):
+
+    env_class = FunctionalTestEnvironment
+
+    def setUp(self):
+        trac_source_tree = os.path.normpath(os.path.join(trac.__file__, '..',
+                                                     '..'))
+        port = 8000 + os.getpid() % 1000
+        dirname = "testenv%s" % port
+        dirname = os.path.join(trac_source_tree, dirname)
+
+        baseurl = "http://127.0.0.1:%s" % port
+        self._testenv = self.env_class(dirname, port, baseurl)
+
+    def tearDown(self):
+        """remove the test environment"""
+        self._testenv.destroy()
+
+    def test_backup(self):
+        """Testing backup"""
+        # raises TracError if backup fails
+        env = self._testenv.get_trac_environment()
+        env.backup()
+
+
+
+def suite():
+    return unittest.makeSuite(DatabaseBackupTestCase,'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --cc trac/tests/functional/testenv.py
index 6514e36,f1921c7..0000000
--- a/trac/tests/functional/testenv.py
+++ b/trac/tests/functional/testenv.py
@@@ -63,20 -58,18 +63,19 @@@ class FunctionalTestEnvironment(object)
      def get_dburi(self):
          if os.environ.has_key('TRAC_TEST_DB_URI'):
              dburi = os.environ['TRAC_TEST_DB_URI']
- 
 +            # Assume the schema 'tractest' for Postgres
-             if dburi.startswith("postgres") and "?schema=" not in dburi:
-                 dburi += "?schema=tractest"
+             if dburi.startswith("postgres") and dburi.find("?schema=") < 0:
+                 return "%s?schema=testenv%s" % (dburi, self.port)
              return dburi
          return 'sqlite:db/trac.db'
      dburi = property(get_dburi)
  
      def destroy(self):
          """Remove all of the test environment data."""
-         if self.dburi.startswith("postgres"):
+         if os.path.exists(self.dirname) and self.dburi.startswith("postgres"):
 -            # if we're using a schema, destroy it now.  initenv, called later,
 -            # will fail otherwise, and this is more consistent with testing
 -            # against sqlite
 +            # We'll remove the schema automatically for Postgres if it exists.
 +            # With this, you can run functional tests multiple times without
 +            # running external tools (just like when running against sqlite)
              import trac.db.api as db_api
              env = self.get_trac_environment()
              env_db = env.get_db_cnx()

