Maintenance

Como alterar a pasta data do PostgreSQL para outro disco com segurança

PG Monitoring Team August 02, 2026 14 min de leitura

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 PGDATA ou -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 PGDATA do host; redimensione ou migre pelos recursos do provedor.
  • Este procedimento move arquivos da mesma versão major do PostgreSQL. Não substitui pg_upgrade nem 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.conf durante 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

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.

Related Articles

Ready to experience better PostgreSQL monitoring?

Join thousands of teams who switched from traditional tools to PG Monitoring's AI-powered platform.

Fale conosco