Alterar a pasta de dados do PostgreSQL só é simples quando todas as premissas estão corretas: o cluster certo foi parado, a cópia final é consistente, o serviço lê a configuração editada, as permissões sobreviveram e o diretório antigo continua disponível para rollback. Este guia reúne esses requisitos em um procedimento protegido para Debian/Ubuntu e em um script Bash funcional.
Um agrado da nossa equipe: o script abaixo está pronto para copiar ou baixar diretamente. Leia o escopo e ensaie em um ambiente restaurado antes de executar em produção.
Escopo: onde este script funciona
O exemplo automatizado atende PostgreSQL instalado por pacotes Debian/Ubuntu, administrado como cluster nomeado pelo postgresql-common e pelo systemd. Os argumentos são versão major, nome do cluster e novo caminho absoluto:
sudo bash move-postgresql-data-directory-debian.sh 16 main /data/postgresql/16/main
Por padrão, o script recusa um destino no mesmo filesystem. Essa proteção detecta o caso comum em que /data existe, mas o disco não foi montado. Se a troca de pasta no mesmo filesystem for realmente intencional, execute com ALLOW_SAME_FILESYSTEM=1 depois de conferir a capacidade.
Ele propositalmente não tenta ser universal:
- Em RHEL, Rocky, AlmaLinux ou instalação por source, o serviço costuma localizar o cluster por
PGDATAou-D; adapte o override do serviço em vez de assumir o layout externo do Debian. - Em Docker ou Kubernetes, mova ou substitua o volume/PVC e siga o modelo de ownership e rollout do orquestrador.
- Amazon RDS, Aurora, Cloud SQL e Azure Database for PostgreSQL não expõem o
PGDATAdo host; redimensione ou migre pelos recursos do provedor. - Este procedimento move arquivos da mesma versão major do PostgreSQL. Não substitui
pg_upgradenem serve para migrar entre arquiteturas.
Como Debian/Ubuntu encontra a pasta data
Nesses pacotes, a configuração normalmente fica em /etc/postgresql/VERSAO/CLUSTER/, enquanto os dados ficam em /var/lib/postgresql/VERSAO/CLUSTER/. O serviço parte da configuração externa e o parâmetro data_directory aponta para o armazenamento real. O PostgreSQL só aplica esse parâmetro na inicialização.
O script recusa continuar se pg_settings.sourcefile informar que data_directory veio de outro arquivo incluído ou de outro mecanismo. Essa proteção é proposital: editar silenciosamente o arquivo errado transforma uma cópia limpa em restart malsucedido.
Antes da janela de manutenção
- Faça um backup válido e prove que consegue restaurá-lo. Manter a pasta antiga ajuda no rollback, mas não substitui backup.
- Monte o destino de forma persistente por UUID ou identificador estável e teste
mount -a. Se o disco não montar e a pasta existir no filesystem raiz, o PostgreSQL pode gravar no dispositivo errado. - Confirme que o filesystem suporta a durabilidade exigida pelo PostgreSQL e tem capacidade, inodes e IOPS esperados.
- Pause a gestão de configuração que possa sobrescrever o
postgresql.confdurante a mudança. - Drene o tráfego da aplicação e reserve uma janela de restart. A primeira cópia roda online; a sincronização final acontece após parada limpa.
findmnt /data
df -hT /data
df -ih /data
sudo -u postgres psql -XAtqc "SHOW data_directory"
sudo -u postgres psql -XAtqc "SHOW config_file"
Tablespaces são separados: diretórios referenciados por PGDATA/pg_tblspc são links simbólicos. O script preserva os links; não copia o conteúdo dos tablespaces externos. Inventarie pg_tablespace separadamente se esses volumes também precisarem mudar.
O que o script faz
Migração em duas passagens: downtime curto e rollback explícito
A passagem online é somente uma cópia preparatória; ela não é um backup utilizável isoladamente. A consistência vem do segundo rsync após a parada limpa. O script edita a configuração Debian ativa, inicia o mesmo cluster, consulta SHOW data_directory e restaura automaticamente a configuração se qualquer comando da virada falhar.
Script completo de migração
Salve como move-postgresql-data-directory-debian.sh ou baixe o arquivo pronto:
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
echo "Uso: sudo $0 <versao_major> <cluster> <novo_diretorio_data>"
echo "Exemplo: sudo $0 16 main /data/postgresql/16/main"
}
fail() {
echo "ERRO: $*" >&2
exit 1
}
log() {
echo "[$(date --iso-8601=seconds)] $*"
}
[[ $EUID -eq 0 ]] || fail "Execute este script como root."
[[ $# -eq 3 ]] || { usage; exit 2; }
PG_VERSION="$1"
PG_CLUSTER="$2"
REQUESTED_DATA="$3"
SERVICE="postgresql@${PG_VERSION}-${PG_CLUSTER}.service"
CONFIG="/etc/postgresql/${PG_VERSION}/${PG_CLUSTER}/postgresql.conf"
[[ "$PG_VERSION" =~ ^[0-9]+$ ]] || fail "Versão major inválida."
[[ "$PG_CLUSTER" =~ ^[A-Za-z0-9_-]+$ ]] || fail "Nome de cluster inválido."
[[ "$REQUESTED_DATA" =~ ^/[A-Za-z0-9._/-]+$ ]] || fail "O novo caminho deve ser absoluto e conter apenas letras, números, ponto, underscore, barra ou hífen."
[[ "$REQUESTED_DATA" != "/" ]] || fail "Recusando usar / como PGDATA."
for command_name in pg_lsclusters psql realpath rsync runuser systemctl; do
command -v "$command_name" >/dev/null || fail "Comando ausente: $command_name"
done
[[ -f "$CONFIG" ]] || fail "Configuração não encontrada: $CONFIG"
systemctl is-active --quiet "$SERVICE" || fail "$SERVICE não está ativo."
PORT="$(pg_lsclusters --no-header | awk -v version="$PG_VERSION" -v cluster="$PG_CLUSTER" '$1 == version && $2 == cluster { print $3; exit }')"
[[ "$PORT" =~ ^[0-9]+$ ]] || fail "Não foi possível descobrir a porta do cluster."
PSQL=(runuser -u postgres -- psql -XAt --no-password --port="$PORT" --dbname=template1)
OLD_DATA="$("${PSQL[@]}" --command='SHOW data_directory')"
SETTING_SOURCE="$("${PSQL[@]}" --command="SELECT sourcefile FROM pg_settings WHERE name = 'data_directory'")"
[[ -n "$OLD_DATA" ]] || fail "Não foi possível ler data_directory."
[[ -n "$SETTING_SOURCE" ]] || fail "data_directory não veio de um arquivo de configuração."
OLD_DATA="$(realpath -e "$OLD_DATA")"
NEW_DATA="$(realpath -m "$REQUESTED_DATA")"
CONFIG_REAL="$(realpath -e "$CONFIG")"
SETTING_SOURCE_REAL="$(realpath -e "$SETTING_SOURCE")"
[[ "$SETTING_SOURCE_REAL" == "$CONFIG_REAL" ]] || fail "data_directory vem de $SETTING_SOURCE_REAL, não de $CONFIG_REAL. Adapte o runbook para esse layout."
[[ "$OLD_DATA" != "$NEW_DATA" ]] || fail "Origem e destino são idênticos."
[[ -f "$OLD_DATA/PG_VERSION" ]] || fail "PG_VERSION não existe em $OLD_DATA."
[[ "$(tr -d '[:space:]' < "$OLD_DATA/PG_VERSION")" == "$PG_VERSION" ]] || fail "A origem pertence a outra versão major do PostgreSQL."
case "$NEW_DATA/" in
"$OLD_DATA/"*) fail "O destino não pode ficar dentro do data directory atual." ;;
esac
if [[ -d "$NEW_DATA" ]] && [[ -n "$(find "$NEW_DATA" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
fail "O destino existe e não está vazio: $NEW_DATA"
fi
install -d -o postgres -g postgres -m 700 "$NEW_DATA"
REQUIRED_KB="$(du -sk "$OLD_DATA" | awk '{ print $1 }')"
AVAILABLE_KB="$(df -Pk "$NEW_DATA" | awk 'NR == 2 { print $4 }')"
MINIMUM_KB=$(( REQUIRED_KB + REQUIRED_KB / 10 ))
(( AVAILABLE_KB >= MINIMUM_KB )) || fail "Espaço insuficiente: necessário ${MINIMUM_KB} KiB, disponível ${AVAILABLE_KB} KiB."
OLD_DEVICE="$(df -P "$OLD_DATA" | awk 'NR == 2 { print $1 }')"
NEW_DEVICE="$(df -P "$NEW_DATA" | awk 'NR == 2 { print $1 }')"
if [[ "$OLD_DEVICE" == "$NEW_DEVICE" && "${ALLOW_SAME_FILESYSTEM:-0}" != "1" ]]; then
fail "Origem e destino estão no mesmo filesystem ($OLD_DEVICE). O novo disco está montado? Use ALLOW_SAME_FILESYSTEM=1 somente se isso for intencional."
fi
log "Origem: $OLD_DATA"
log "Destino: $NEW_DATA"
log "Iniciando cópia preparatória online. Esta passagem não é backup."
ONLINE_RSYNC_STATUS=0
rsync -aH --numeric-ids --delete "$OLD_DATA/" "$NEW_DATA/" || ONLINE_RSYNC_STATUS=$?
if (( ONLINE_RSYNC_STATUS != 0 && ONLINE_RSYNC_STATUS != 24 )); then
fail "O rsync online falhou com status $ONLINE_RSYNC_STATUS."
fi
if (( ONLINE_RSYNC_STATUS == 24 )); then
log "Arquivos mudaram durante a cópia online (status 24); a passagem offline fará a reconciliação."
fi
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
CONFIG_BACKUP="${CONFIG}.before-pgdata-move.${TIMESTAMP}"
cp -a -- "$CONFIG" "$CONFIG_BACKUP"
ROLLBACK_REQUIRED=1
rollback() {
local exit_code=$?
trap - EXIT
if (( ROLLBACK_REQUIRED )); then
echo "Falha na virada; restaurando $CONFIG_BACKUP" >&2
systemctl stop "$SERVICE" || true
cp -a -- "$CONFIG_BACKUP" "$CONFIG"
systemctl start "$SERVICE" || echo "O restart automático no diretório antigo falhou; veja journalctl -u $SERVICE." >&2
fi
exit "$exit_code"
}
trap rollback EXIT
log "Parando $SERVICE para a sincronização final."
systemctl stop "$SERVICE"
systemctl is-active --quiet "$SERVICE" && fail "$SERVICE não parou."
log "Executando sincronização final offline."
rsync -aH --numeric-ids --delete "$OLD_DATA/" "$NEW_DATA/"
chown postgres:postgres "$NEW_DATA"
chmod 700 "$NEW_DATA"
if grep -Eq '^[[:space:]]*data_directory[[:space:]]*=' "$CONFIG"; then
sed -Ei "s|^[[:space:]]*data_directory[[:space:]]*=.*$|data_directory = '${NEW_DATA}'|" "$CONFIG"
else
printf "
data_directory = '%s'
" "$NEW_DATA" >> "$CONFIG"
fi
log "Iniciando $SERVICE com o novo data directory."
systemctl start "$SERVICE"
ACTUAL_DATA="$("${PSQL[@]}" --command='SHOW data_directory')"
ACTUAL_DATA="$(realpath -e "$ACTUAL_DATA")"
[[ "$ACTUAL_DATA" == "$NEW_DATA" ]] || fail "O PostgreSQL iniciou com $ACTUAL_DATA em vez de $NEW_DATA."
"${PSQL[@]}" --command='SELECT version();'
"${PSQL[@]}" --command='SELECT pg_is_in_recovery();'
ROLLBACK_REQUIRED=0
trap - EXIT
log "Migração concluída com sucesso."
log "Backup da configuração: $CONFIG_BACKUP"
log "Dados antigos preservados em: $OLD_DATA"
log "Não remova a pasta antiga antes de validar backup, aplicação, logs e réplicas."
Execute e acompanhe a virada
chmod 750 move-postgresql-data-directory-debian.sh
sudo ./move-postgresql-data-directory-debian.sh 16 main /data/postgresql/16/main
sudo systemctl status postgresql@16-main.service --no-pager
sudo journalctl -u postgresql@16-main.service -n 100 --no-pager
A maior parte dos bytes é transferida na passagem online. O downtime contém o delta final, a alteração de configuração, o restart e a consulta de validação. Clusters com muita escrita produzem um delta maior; planeje a janela de acordo.
Validação pós-migração
sudo -u postgres psql -X -d template1 -c "SHOW data_directory;"
sudo -u postgres psql -X -d template1 -c "SHOW config_file;"
sudo -u postgres psql -X -d template1 -c "SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY 1;"
findmnt /data
df -hT /data
sudo ss -ltnp | grep postgres
- Faça smoke tests de leitura e escrita da aplicação, não somente
SELECT 1. - Inspecione os logs do PostgreSQL por arquivos ausentes, erros de permissão ou mensagens de recovery.
- Valide replicação física e lógica, archive, jobs de backup, monitoramento e políticas de segurança baseadas em caminho.
- Reinicie o host em outra janela programada para provar que o mount e o serviço sobrevivem ao boot.
Rollback enquanto a pasta antiga existe
O script restaura automaticamente a configuração antiga quando a própria virada falha. Se um teste posterior da aplicação encontrar problema, interrompa as escritas antes do rollback; depois que clientes gravam no novo diretório, a cópia antiga fica desatualizada.
sudo systemctl stop postgresql@16-main.service
sudo cp -a /etc/postgresql/16/main/postgresql.conf.before-pgdata-move.TIMESTAMP /etc/postgresql/16/main/postgresql.conf
sudo systemctl start postgresql@16-main.service
sudo -u postgres psql -XAtqc "SHOW data_directory"
Não “misture” dois diretórios PGDATA. Se o novo cluster recebeu escritas, resolva o problema no lugar ou use um plano de recuperação/migração consciente do banco. Fazer rsync de arquivos de um cluster ativo ou mais novo para a pasta antiga não é um método seguro de resolver conflitos.
Quando remover o diretório antigo?
Somente depois de o novo mount sobreviver a restart, os testes da aplicação ficarem verdes, backups e réplicas concluírem com sucesso, o monitoramento mostrar a instância esperada e a janela de retenção para rollback expirar. Então confirme que nenhum processo usa o caminho antigo, arquive o que a política exigir e remova-o pelo processo controlado de mudança. O script fornecido nunca apaga a pasta antiga.
Mantenha o novo storage observável
Uma troca correta ainda pode gerar incidente futuro se o filesystem não montar depois do reboot, crescer além da previsão ou entregar outra latência. O PG Monitoring correlaciona crescimento e I/O do storage com queries, checkpoints, WAL, bloat e replicação, permitindo provar que a mudança melhorou o sistema — e não apenas trocou um caminho.
Referências: a documentação de localização de arquivos explica a relação entre PGDATA, -D, config_file e data_directory; a referência do postgres documenta PGDATA como localização padrão do diretório de dados.