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

Encontre esse problema antes que ele afete a produção

Monitore queries, capacidade, autovacuum, índices e replicação continuamente com contexto operacional.

Fale conosco