
PostgreSQL stores each cluster’s databases and system files inside its data directory. When that directory lives on a disk that also hosts the operating system, the cluster can run out of space or start competing for I/O with other processes.
This guide shows how to move the PostgreSQL data directory to a new mount point by copying the files with rsync, updating postgresql.conf, and adjusting the Ubuntu AppArmor profile so PostgreSQL can access the new path. You will also use systemctl to stop and start PostgreSQL and psql to verify the migration.
By the end, PostgreSQL should start cleanly from the new data directory, and you will have a .bak rollback path if you need to revert.
/var/lib/postgresql/<version>/main, and you can confirm it with SHOW data_directory; in psql.rsync is preferred over cp because it preserves permissions, timestamps, symlinks, and special file metadata, and it can resume by copying only differences.SHOW data_directory; in psql./var/lib/postgresql/<version>/main.bak to rollback safely by renaming it back and reverting data_directory.Before you move the data directory, understand what each component inside it does. An incomplete or corrupted copy of any of these files will prevent PostgreSQL from starting or cause silent data loss.
Configuration files are also part of this picture. Even though postgresql.conf is commonly a symlink into /etc/postgresql/<version>/main/postgresql.conf, PostgreSQL reads the active data_directory value from that file.
The default layout looks like this (replace 14 with your installed PostgreSQL major version):
/var/lib/postgresql/
└── 14/
└── main/
├── base/
├── global/
├── pg_wal/
├── pg_hba.conf
├── postgresql.conf -> /etc/postgresql/14/main/postgresql.conf
└── PG_VERSION
Each entry in that tree has a specific role:
base/ holds the actual on-disk database files. Each subdirectory inside base/ corresponds to a database in the cluster, identified by its object ID (OID). This is the largest part of the data directory on most systems.global/ holds cluster-wide system catalog tables such as pg_database and pg_authid. These tables track which databases exist and which roles can access them.pg_wal/ holds write-ahead log (WAL) segments. PostgreSQL writes changes here before applying them to the main data files. If pg_wal/ is missing or incomplete after a migration, PostgreSQL cannot guarantee data consistency and will refuse to start.pg_hba.conf controls client authentication rules. It defines which hosts can connect, which users they can connect as, and which authentication method applies.PG_VERSION is a plain text file containing the PostgreSQL major version number. PostgreSQL reads this at startup to confirm the data directory format matches the running binary.The data_directory directive in postgresql.conf tells PostgreSQL where to find those files. After the migration, the only PostgreSQL configuration change you typically need is updating data_directory, alongside the AppArmor path permissions required on Ubuntu.
Before you move files, confirm your PostgreSQL major version and verify that AppArmor is active on the host.
To complete this guide, you will need:
An Ubuntu server with a non-root user with sudo privileges. If you need to set this up, see Initial Server Setup with Ubuntu.
PostgreSQL installed on your server. If you need installation steps, see How To Install and Use PostgreSQL on Ubuntu.
Your PostgreSQL major version number. You can get it with either of these commands:
psql --version
pg_lsclusters
sudo aa-status
/mnt/volume_nyc1_01, but you must substitute your own mount point path.If you need a refresher on the file copy method used here, read How to Use rsync to Sync Local and Remote Directories. If you need to prepare the disk that backs the mount point, see How to Partition and Format Storage Devices in Linux.
Confirm where PostgreSQL is currently reading data from, then check that the new mount point has enough free space for the full cluster directory.
Run psql as the postgres user and print the current data_directory:
sudo -u postgres psql
SHOW data_directory;
data_directory
-----------------------------
/var/lib/postgresql/14/main
(1 row)
Exit psql with:
\q
Now confirm the mount point is available and has enough space:
df -h /mnt/volume_nyc1_01
Filesystem Size Used Avail Use% Mounted on
/dev/sdx1 100G 10G 90G 10% /mnt/volume_nyc1_01
Also check how large your current data directory is so you can compare it to the destination free space: <$>[warning] The destination must have at least as much free space as the current data directory size. <$>
Check the current size of your data directory:
sudo du -sh /var/lib/postgresql/
12G /var/lib/postgresql/
If the mount point is new or freshly attached, confirm it is accessible before running the copy:
ls -la /mnt/volume_nyc1_01
total 8
drwxr-xr-x 2 root root 4096 Jan 10 09:00 .
drwxr-xr-x 12 root root 4096 Jan 10 08:55 ..
The rsync command in Step 3 preserves permissions and ownership from the source directory, but the destination filesystem must support standard POSIX file attributes. FAT32 and exFAT volumes do not; use ext4 or xfs for a PostgreSQL data directory.
Stop PostgreSQL before copying files so the data directory does not change during the migration.
Run:
sudo systemctl stop postgresql
Then verify the service is stopped:
sudo systemctl status postgresql
● postgresql.service - PostgreSQL RDBMS
Loaded: loaded
Active: inactive (dead)
As an alternative check, use pg_lsclusters to confirm the cluster is down:
pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
14 main 5432 down postgres /var/lib/postgresql/14/main ...
Copy the stopped cluster files with rsync so permissions, timestamps, and special metadata carry over to the new mount point.
Run the copy command:
sudo rsync -av /var/lib/postgresql /mnt/volume_nyc1_01
sending incremental file list
postgresql/
postgresql/14/
postgresql/14/main/
postgresql/14/main/PG_VERSION
postgresql/14/main/base/
postgresql/14/main/global/
postgresql/14/main/pg_hba.conf
postgresql/14/main/pg_wal/
...
sent 2,873,452 bytes received 412 bytes 819,733.47 bytes/sec
total size is 2,872,198 speedup is 1.00
The final summary line confirms rsync completed the transfer.
Optional progress for large directories:
sudo rsync -av --progress /var/lib/postgresql /mnt/volume_nyc1_01
Flag behavior:
-a enables archive mode, which preserves permissions, timestamps, symlinks, owner, group, and recursively copies directories.-v prints more detail so you can follow what rsync is transferring.rsync copies the postgresql directory itself into the destination, which results in the expected directory layout under your mount point.If you want background on what these flags mean, see How to Use rsync to Sync Local and Remote Directories.
rsync can be safely re-run if the copy is interrupted. After the cluster is stopped, rerunning rsync continues by transferring any remaining differences, which makes the copy effectively idempotent.
Do not use cp -r for this migration. cp -r does not reliably preserve all POSIX permissions and special file attributes that PostgreSQL expects for database directories.
Rename the current data directory to a .bak path so you have a fast rollback option without re-copying data.
After the copy completes, move the old directory out of the way:
sudo mv /var/lib/postgresql/14/main /var/lib/postgresql/14/main.bak
Keep this .bak directory until you confirm the new cluster starts and the application behaves as expected. This preserves a rollback path that does not require re-running rsync.
To roll back, rename the backup directory back to main:
sudo mv /var/lib/postgresql/14/main.bak /var/lib/postgresql/14/main
Then revert postgresql.conf so data_directory points to /var/lib/postgresql/<version>/main, and start PostgreSQL again.
Point PostgreSQL at the new location by updating the data_directory directive in the active postgresql.conf.
Edit the configuration file:
sudo nano /etc/postgresql/14/main/postgresql.conf
On a default PostgreSQL installation, the data_directory line is often commented out because PostgreSQL uses a compiled-in default when no explicit value is set. When you open the file, you may see one of these three states:
The line is commented out with a # prefix:
#data_directory = '/var/lib/postgresql/14/main' # use data in another directory
The line is absent entirely, with only a comment block referencing the default.
The line is already uncommented with the default path set explicitly:
data_directory = '/var/lib/postgresql/14/main'
In the first two cases, add the data_directory line explicitly with the new path. In the third case, replace the existing value. In all cases, the result should be a single uncommented data_directory line pointing to the new mount point.
Find the existing data_directory line in postgresql.conf:
data_directory = '/var/lib/postgresql/14/main'
Replace it with the new path (substitute your major version and mount point):
data_directory = '/mnt/volume_nyc1_01/postgresql/14/main'
After saving the file, confirm the value that PostgreSQL will read:
grep -v '^#' /etc/postgresql/14/main/postgresql.conf | grep data_directory
data_directory = '/mnt/volume_nyc1_01/postgresql/14/main'
The -v '^#' flag excludes commented lines so only the active directive is returned. Without this filter, grep would also return commented-out data_directory lines and make the output ambiguous.
Update the AppArmor local override so PostgreSQL can read and write the new data directory path on Ubuntu.
On Ubuntu, AppArmor restricts which filesystem paths PostgreSQL is allowed to access. After a data directory move to a non-default path, PostgreSQL can fail to start with a permission denied error unless the AppArmor profile allows the new directory.
First check whether AppArmor is enforcing the PostgreSQL profile:
sudo aa-status | grep postgres
/usr/lib/postgresql/14/bin/postgres
List the available AppArmor local override files to confirm the correct filename for your PostgreSQL version:
ls /etc/apparmor.d/local/
usr.lib.postgresql.14.bin.postgres
If the local override file for your PostgreSQL binary does not exist, create it as an empty file. AppArmor will load it when you reload the profile.
Next, edit the AppArmor local override file for the PostgreSQL binary. Use your PostgreSQL major version in the path:
sudo nano /etc/apparmor.d/local/usr.lib.postgresql.14.bin.postgres
The rules in this local file are applied to the corresponding AppArmor profile for the PostgreSQL server binary. The path in the rule should match the data_directory path you set in Step 5.
Add a rule that grants read/write and locks on the new data directory tree:
/mnt/volume_nyc1_01/postgresql/** rwk,
In AppArmor rule syntax, r grants read access, w grants write access, and k grants file locking, which PostgreSQL requires to coordinate concurrent access to data files.
This rule covers the whole subtree under the mount point, so PostgreSQL can write files in base/, global/, and pg_wal/.
Reload AppArmor so the change takes effect:
sudo systemctl reload apparmor
Alternatively, you can reload the profile by parsing it directly:
sudo apparmor_parser -r /etc/apparmor.d/usr.lib.postgresql.14.bin.postgres
The path here points to the main profile file in /etc/apparmor.d/, not the local override file in /etc/apparmor.d/local/. When apparmor_parser reloads the main profile, it automatically includes any rules in the corresponding local override file, so editing the local file and reloading the main profile is the correct sequence.
Skipping Step 6 is one of the most common reasons for Permission denied errors after a PostgreSQL data directory migration on Ubuntu, even when directory ownership and chmod values are correct.
Start PostgreSQL, then confirm it is online and that psql reports the new data_directory.
Start the service:
sudo systemctl start postgresql
Check service status:
sudo systemctl status postgresql
● postgresql.service - PostgreSQL RDBMS
Active: active (exited) since ...
Confirm the cluster is online and points to the new directory:
pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
14 main 5432 online postgres /mnt/volume_nyc1_01/postgresql/14/main ...
Verify inside psql:
sudo -u postgres psql
SHOW data_directory;
data_directory
----------------------------------------
/mnt/volume_nyc1_01/postgresql/14/main
(1 row)
Run additional checks to catch obvious data access issues:
-- List all databases to find your database name
\l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
myapp_db | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
(2 rows)
-- Connect to your database and list its tables
\c myapp_db
\dt
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | users | table | postgres
public | orders | table | postgres
(2 rows)
-- Spot-check a critical table row count to confirm data integrity
SELECT COUNT(*) FROM users;
count
-------
1042
(1 row)
Substitute your actual database name from the \l output and your table name from the \dt output. The row count should match what you expect based on your application data. A count of zero on a table that should have rows indicates a copy problem — do not proceed until the cause is identified. Before removing the backup directory, also run an application smoke test, as migrations can look healthy at startup but surface issues when your app reads specific tables or uses specific extensions.
Remove the backup only after the new data directory has been running stably for at least one application cycle.
Before removal, confirm that pg_lsclusters shows the correct Data directory path, the application has completed at least one full read/write cycle without errors (for example, one business day of normal traffic or one successful deployment and smoke test), and no application errors have appeared in logs since the migration.
Then remove the backup directory:
sudo rm -Rf /var/lib/postgresql/14/main.bak
When migration steps do not work, narrow down the cause by checking service status, AppArmor enforcement, and the exact configuration file PostgreSQL is reading.
Symptom: systemctl start postgresql returns a failure, and journalctl shows permission denied messages for the new data directory path.
Cause 1: The AppArmor profile was not updated. Before pointing to Step 6, confirm AppArmor is the actual cause by checking the PostgreSQL journal for the specific error:
sudo journalctl -u postgresql -n 50
Look for lines containing Permission denied referencing the new data directory path. If those lines are present, complete Step 6, reload AppArmor, and try starting PostgreSQL again. If the journal shows a different error, continue to Cause 2 and Cause 3 before revisiting the AppArmor profile.
Cause 2: Incorrect filesystem ownership or permissions. Fix:
sudo chown -R postgres:postgres /mnt/volume_nyc1_01/postgresql
sudo chmod 700 /mnt/volume_nyc1_01/postgresql/14/main
Cause 3: The data_directory path contains a typo or trailing whitespace. Fix: re-check the configured value in the active postgresql.conf file using grep.
Symptom: SHOW data_directory; returns the old path after restart.
Cause 1: You updated the wrong postgresql.conf file. This happens when multiple PostgreSQL versions are installed or when you edited a file under a different version directory.
Cause 2: The change was not saved before restarting PostgreSQL.
Fix: confirm which config file PostgreSQL is using:
sudo -u postgres psql -c "SHOW config_file;"
Then verify data_directory inside that exact file.
Symptom: psql or applications cannot connect, even though PostgreSQL appears to be running.
Cause 1: pg_hba.conf in the new data directory differs from what your environment expects, or PostgreSQL authentication settings were not copied as expected.
Cause 2: The socket directory configuration changed, or the client is connecting to the wrong socket path.
Fix: check PostgreSQL logs:
sudo journalctl -u postgresql --since "10 minutes ago"
Also check the configured Unix socket directories:
sudo -u postgres psql -c "SHOW unix_socket_directories;"
Use these Q&A entries to handle common variations and failure modes during a data directory migration.
A: Stop PostgreSQL so the data files do not change during the copy. Copy the stopped data directory to the new mount point with rsync, then update the active postgresql.conf data_directory value to the new path. Add an AppArmor local rule for that path, start PostgreSQL again, and verify with psql that SHOW data_directory; reports the new directory.
A: PostgreSQL stores cluster data under /var/lib/postgresql/<version>/main. The <version> segment matches the PostgreSQL major version installed on your system. You can confirm the exact directory with SHOW data_directory; in psql.
A: Edit postgresql.conf and change the data_directory directive to the new absolute path. Restart PostgreSQL so it reads the updated configuration and starts using the migrated directory.
A: Set data_directory in postgresql.conf to the absolute path of the migrated cluster directory. The directive typically looks like data_directory = '/mnt/your-mount/postgresql/<version>/main'. Substitute your mount point and PostgreSQL major version.
A: The most common cause on Ubuntu is an AppArmor profile restriction that does not allow PostgreSQL to access the new path. Check Step 6 first, then verify ownership and permissions on the new directory. Also confirm that data_directory in the active postgresql.conf matches the real path on disk.
A: Yes, but you still need to account for AppArmor. AppArmor applies to the real target path that the filesystem resolves, so a symlink does not bypass path restrictions. If your profile only allows the default directory, PostgreSQL can still fail until you update the AppArmor local rule.
A: rsync is typically the best choice because it preserves permissions and special metadata while copying recursively. Since you stop PostgreSQL first, rerunning rsync after interruptions converges on the same target state without needing to recopy everything from scratch.
A: No. Moving an existing data directory does not require initdb because PostgreSQL is already initialized inside the migrated directory. initdb is for creating a brand-new cluster, not for relocating an existing one.
After you stop PostgreSQL, copy the data directory to the new mount point, update postgresql.conf, and update AppArmor, PostgreSQL should start cleanly from the new path. Verify the migration with pg_lsclusters and psql, then keep the .bak directory until you confirm stable application behavior.
For related migration patterns, see the tutorial collection at How to Move a PostgreSQL Data Directory to a New Location.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
This comment has been deleted
I follow these steps except in part 2 I use /home/postgresql for my new data location - I have most of my available space allocated to my home directory and want the postgresql database there.
Outside of this I follow all of your steps and when I restart the service it starts for systemctl start postgresql and when I run status.
But, when i try sudo - u postgres psql I get this error
SpongeBob@SpongeBoB:/var/lib# sudo -u postgres psql psql: error: connection to server on socket “/var/run/postgresql/.s.PGSQL.5432” failed: No such file or directory Is the server running locally and accepting connections on that socket?
Any idea why this is? Do I have to use /mnt?
On Postgres 16
# sudo rsync -av /var/lib/postgresql/ /mnt/volume_sfo3_03/
The conf should be
. . .
data_directory = '/mnt/volume_sfo3_03/16/main'
. . .
Notice the current docs have the extra /postgres/ path
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.