Lifecycle ledger: invoice is not auto-payment, cancellation is not logout
Check the official Terms, selected invoice and client-area controls before relying on any date. The public rules below were checked 2026-11-05 and may change.
| Event | Public rule to plan around | Operational consequence |
|---|---|---|
| Renewal notice | Invoice/email generated 14 days before due date | Record the actual due date; invoice creation does not prove auto-debit |
| Ordinary cancellation | Request through client area at least 5 days before renewal | Set an earlier private reminder; retain the confirmation |
| Contract services | At least 7 days before renewal | Use the longer deadline if your contract requires it |
| Immediate cancellation | Help says termination within 24 hours and data loss | Export and restore-test before choosing immediate |
| End-of-cycle cancellation | Service remains until paid period ends | Still export early; do not assume later data access |
| Unpaid service | Most services terminate 7 days after invoice due date; data permanently removed | Nonpayment is not a safe backup or cancellation strategy |
| Refunds | Default no-refund/no-money-back posture; discretionary case-by-case review | Do not buy a long term expecting a routine refund |
Auto-debit behavior was not established for every payment method. Inspect your own stored-payment settings and invoice, or ask support a specific question. Disabling a payment method, removing a card, or letting an invoice lapse does not replace a cancellation request.
The refund help points users to contact support for case review. Preserve the exact offer, invoice, payment record and relevant technical evidence; do not describe a discretionary request as an entitlement.
Recovery objectives before the first backup
RackNerd’s default VPS terms exclude managed backups unless separately agreed. Shared/reseller JetBackup claims do not apply to KVM. Public pages did not establish a general user-accessible KVM snapshot feature, so this guide does not depend on one.
Write three targets before scripting:
- RPO: how much new data can you lose? A daily workflow export still loses up to a day of credentials, edits and execution state.
- RTO: how long can the service remain unavailable while you provision, restore and test? An annual VPS price does not provide instant recovery.
- Retention: how many daily/weekly/monthly restore points, and who can delete them? Keep credentials and repository-delete privileges separate where possible.
A complete n8n recovery set includes the full .n8n state—including credential encryption material—the actual SQLite or Postgres database, custom nodes, relevant binary data, deployment definition, exact application/DB versions, and any external storage. Workflow/credential JSON alone omits users, settings, execution history and possibly the encryption context. Never print or publish these artifacts.
Discover and prepare without exposing secrets
Run discovery from the n8n operations inventory. The export script supports two explicit paths:
- Compose: the actual project/file, n8n writer service names, and—if Postgres—the database service/container authentication method.
- systemd/native: actual n8n writer units, native state directory, and local Postgres peer/authentication path.
If neither matches, stop. Do not install a parallel database or assume /opt/ai, /root/.n8n, n8n, or postgres names. You need root/sudo for service stops and complete state ownership, plus enough free space for the staging copy and dump.
Install Restic only from the current official installation documentation. Also install SQLite/Postgres client tools only if your selected branch requires them. Verify versions and package provenance; do not execute a copied remote shell pipeline.
Choose an off-provider destination you control. The example uses Restic SFTP; an SSH key for backupuser must already be configured without exposing its private contents. On the VPS as root:
install -m 700 -d /root/.config/racknerd-recovery
read -r -s -p 'New independent Restic repository password: ' RESTIC_PASSWORD; printf '\n'
printf '%s\n' "$RESTIC_PASSWORD" > /root/.config/racknerd-recovery/restic-password
unset RESTIC_PASSWORD
chmod 600 /root/.config/racknerd-recovery/restic-password
cat > /root/.config/racknerd-recovery/restic.env <<'EOF'
RESTIC_REPOSITORY=sftp:backupuser@backup.example.net:/srv/restic/racknerd-01
RESTIC_PASSWORD_FILE=/root/.config/racknerd-recovery/restic-password
EOF
chmod 600 /root/.config/racknerd-recovery/restic.env
Replace the repository placeholder before use. Store a second offline copy of the repository password in your password manager; losing it makes the backup unusable. The mode-600 file enables unattended jobs but anyone who obtains it plus repository access can decrypt backups—protect and rotate access deliberately.
Initialize once, after confirming the destination is empty/intended:
set -Eeuo pipefail
set -a
. /root/.config/racknerd-recovery/restic.env
set +a
restic init
restic snapshots
If init says the repository already exists, stop and verify rather than overwriting or reinitializing a shared location. restic.env contains the password-file path, not the password itself; still never paste it or the SFTP key into tickets.
Backup runbook
Checked n8n state and database export
The following script is an editorial starting point, not a RackNerd or n8n official script. Review and replace every REPLACE_ value. It supports SQLite by copying the entire state directory only after stopping n8n; for Postgres it pauses all configured n8n writers while leaving the database available, streams pg_dump to a restrictive .partial file, tests nonempty output, and atomically renames it. It restarts only writers that were verified running, including on an error path.
Save as /usr/local/sbin/n8n-recovery-export, then edit the configuration block. No passwords are echoed.
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# REQUIRED: replace all values for the discovered deployment.
DEPLOYMENT='compose' # compose OR systemd
N8N_HOME='/srv/REPLACE_N8N_STATE' # full .n8n-equivalent state directory
DB_TYPE='postgres' # sqlite OR postgres
PROJECT_DIR='/srv/REPLACE_COMPOSE_PROJECT'
COMPOSE_FILE='compose.yml' # actual discovered filename
WRITER_SERVICES=(n8n) # every n8n writer; add workers if present
N8N_UNITS=(n8n.service) # native only; every writer unit
POSTGRES_SERVICE='postgres' # Compose Postgres only
POSTGRES_USER='postgres'
POSTGRES_DB='n8n'
EXPORT_ROOT='/var/lib/aixvps-recovery'
# Absolute source paths outside N8N_HOME that recovery requires.
# Include discovered binary/custom-node/external-storage paths; () is not sufficient.
EXTRA_PATHS=(
'/srv/REPLACE_N8N_BINARY_OR_EXTERNAL_DATA'
'/srv/REPLACE_N8N_CUSTOM_NODES'
)
# Non-secret deployment definition and manually reviewed REDACTED config template.
# Renaming a raw .env/secrets file does NOT make it safe; inspect every line first.
DEPLOYMENT_ARTIFACT='/srv/REPLACE_REDACTED_COMPOSE_OR_UNIT_DEFINITION'
[[ $EUID -eq 0 ]] || { echo 'Run as root.' >&2; exit 1; }
[[ $N8N_HOME == /* && $N8N_HOME != / && -d $N8N_HOME ]]
[[ $EXPORT_ROOT == /* && $EXPORT_ROOT != / ]]
[[ $DB_TYPE == sqlite || $DB_TYPE == postgres ]]
[[ $DEPLOYMENT == compose || $DEPLOYMENT == systemd ]]
[[ ${#WRITER_SERVICES[@]} -gt 0 && ${#N8N_UNITS[@]} -gt 0 ]]
[[ $N8N_HOME != *REPLACE_* && $PROJECT_DIR != *REPLACE_* ]] || {
echo 'Replace discovery placeholders first.' >&2; exit 1;
}
[[ ${#EXTRA_PATHS[@]} -gt 0 ]] || { echo 'Extra recovery paths are required.' >&2; exit 1; }
[[ ${ARTIFACT_REDACTION_REVIEWED:-} == 'I_REVIEWED_EVERY_LINE_NO_SECRETS' ]] || {
echo 'Set ARTIFACT_REDACTION_REVIEWED only after line-by-line secret review.' >&2; exit 1;
}
[[ $DEPLOYMENT_ARTIFACT != *REPLACE_* && -f $DEPLOYMENT_ARTIFACT ]] || {
echo 'Supply a redacted deployment/config artifact; never a raw secret .env.' >&2; exit 1;
}
[[ ${DEPLOYMENT_ARTIFACT##*/} != .env ]] || { echo 'Refusing raw .env artifact.' >&2; exit 1; }
for extra in "${EXTRA_PATHS[@]}"; do
[[ $extra == /* && $extra != / && $extra != *REPLACE_* && -e $extra ]] || {
echo "Missing/unresolved required recovery path: $extra" >&2; exit 1;
}
[[ $extra != "$N8N_HOME" && $extra != "$N8N_HOME"/* ]] || {
echo "Extra path duplicates N8N_HOME: $extra" >&2; exit 1;
}
done
for cmd in cp find sort sha256sum xargs; do command -v "$cmd" >/dev/null; done
install -d -m 700 "$EXPORT_ROOT"
exec 9>"$EXPORT_ROOT/.export.lock"
flock -n 9 || { echo 'Another export is running.' >&2; exit 1; }
STAGE=$(mktemp -d "$EXPORT_ROOT/export.XXXXXXXX")
STOPPED=0
EXPORT_SUCCESS=0
CLEANUP_NEEDS_ATTENTION=0
safely_remove_failed_stage() {
local stage_parent
stage_parent=$(dirname -- "$STAGE")
[[ $stage_parent == "$EXPORT_ROOT" && $STAGE == "$EXPORT_ROOT"/export.* && $STAGE != */../* ]] || {
echo "CRITICAL: unsafe stage path; sensitive data may remain at $STAGE" >&2
CLEANUP_NEEDS_ATTENTION=1
return 1
}
if ! chmod -R u+rwX -- "$STAGE" || ! rm -rf --one-file-system -- "$STAGE"; then
echo "CRITICAL: failed-stage cleanup incomplete; sensitive data may remain at $STAGE" >&2
CLEANUP_NEEDS_ATTENTION=1
return 1
fi
if [[ -e $STAGE ]]; then
echo "CRITICAL: failed stage still exists at $STAGE" >&2
CLEANUP_NEEDS_ATTENTION=1
return 1
fi
printf '%s\n' 'Failed plaintext staging directory removed.' >&2
}
compose() (
cd "$PROJECT_DIR"
docker compose -f "$COMPOSE_FILE" "$@"
)
writers_running() {
local unit service running
if [[ $DEPLOYMENT == compose ]]; then
running=$(compose ps --status running --services)
for service in "${WRITER_SERVICES[@]}"; do
grep -Fxq "$service" <<<"$running" || return 1
done
else
for unit in "${N8N_UNITS[@]}"; do systemctl is-active --quiet "$unit" || return 1; done
fi
}
pause_writers() {
STOPPED=1
if [[ $DEPLOYMENT == compose ]]; then
compose stop "${WRITER_SERVICES[@]}"
else
systemctl stop "${N8N_UNITS[@]}"
fi
}
resume_writers() {
if [[ $DEPLOYMENT == compose ]]; then
compose up -d "${WRITER_SERVICES[@]}"
else
systemctl start "${N8N_UNITS[@]}"
fi
}
on_exit() {
local rc=$?
trap - EXIT
find "$STAGE" -type f -name '*.partial' -delete || rc=1
if [[ $STOPPED -eq 1 ]]; then
if ! resume_writers; then
echo 'CRITICAL: writer restart failed; inspect before enabling traffic.' >&2
rc=1
fi
fi
if [[ $EXPORT_SUCCESS -ne 1 ]]; then
safely_remove_failed_stage || rc=1
elif [[ $CLEANUP_NEEDS_ATTENTION -eq 1 ]]; then
rc=1
fi
exit "$rc"
}
trap on_exit EXIT
writers_running || {
echo 'Not all configured writers were running; no stop/copy attempted.' >&2
exit 1
}
pause_writers
mkdir -p "$STAGE/n8n-home" "$STAGE/extra" "$STAGE/deployment"
cp -a "$N8N_HOME/." "$STAGE/n8n-home/"
index=0
: >"$STAGE/EXTRA-PATHS.tsv"
for extra in "${EXTRA_PATHS[@]}"; do
index=$((index + 1))
dest=$(printf '%s/extra/%03d' "$STAGE" "$index")
cp -a -- "$extra" "$dest"
printf '%03d\t%s\n' "$index" "$extra" >>"$STAGE/EXTRA-PATHS.tsv"
done
cp -a -- "$DEPLOYMENT_ARTIFACT" "$STAGE/deployment/recovery-definition"
if [[ $DB_TYPE == sqlite ]]; then
[[ -f $STAGE/n8n-home/database.sqlite ]] || {
echo 'SQLite database absent at discovered state path.' >&2; exit 1;
}
elif [[ $DB_TYPE == postgres ]]; then
mkdir -p "$STAGE/database"
DUMP="$STAGE/database/n8n.dump"
PARTIAL="$DUMP.partial"
if [[ $DEPLOYMENT == compose ]]; then
compose exec -T "$POSTGRES_SERVICE" \
pg_dump --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --format=custom \
>"$PARTIAL"
else
runuser -u postgres -- \
pg_dump --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --format=custom \
>"$PARTIAL"
fi
[[ -s $PARTIAL ]] || { echo 'pg_dump produced an empty file.' >&2; exit 1; }
mv -- "$PARTIAL" "$DUMP"
fi
# Non-secret hand-written inventory; create before running if desired.
cat >"$STAGE/RESTORE-NOTES.txt" <<EOF
Created: $(date --iso-8601=seconds)
Deployment: $DEPLOYMENT
Database: $DB_TYPE
Record exact n8n/DB image versions privately; EXTRA-PATHS.tsv maps required extra data.
EOF
(
cd "$STAGE"
find . -type f ! -name 'SHA256SUMS*' -print0 | sort -z |
xargs -0 -r sha256sum >SHA256SUMS.tmp
[[ -s SHA256SUMS.tmp ]]
mv SHA256SUMS.tmp SHA256SUMS
)
[[ -s $STAGE/SHA256SUMS ]]
resume_writers
STOPPED=0
EXPORT_SUCCESS=1
printf 'EXPORT_DIR=%s\n' "$STAGE"
printf '%s\n' 'Verify application readiness before sending this staging directory off-site.'
Set mode 0700. First test on a disposable synthetic deployment. A forced pg_dump failure must leave no .dump or .partial, return nonzero, and restart only the writers the script stopped. A failed writer restart is critical: keep traffic disabled until manual verification. On every unsuccessful export, the exit handler removes the staged plaintext tree after attempting writer recovery; cleanup preserves a nonzero status. If guarded cleanup fails, it reports the exact stage as a critical unresolved artifact—secure or remove it manually after verifying the path. Only a successful export leaves a stage for Restic.
SQLite rollback/recovery: if a copy fails, the original stopped database remains untouched. The exit trap attempts restart; verify readiness. Do not delete the original or retry by copying a live database.
Postgres rollback/recovery: a failed dump does not alter the source. Remove only the script’s .partial (the trap does this) and investigate authentication, database name, disk and logs. Never point a production n8n instance at the staging dump.
Immediately run the discovered health/readiness check after the export. Record exact n8n and database image/package versions in a private, non-secret inventory. EXTRA-PATHS.tsv maps each numbered staged copy to its original absolute path, and deployment/recovery-definition preserves the reviewed redacted Compose/unit definition. The script refuses unresolved/missing extra paths and requires an explicit line-by-line redaction attestation for the deployment artifact. A basename check is only a backstop: renaming a raw .env or secrets file does not make it safe. The script cannot determine whether your list is semantically complete, so the restore drill must prove every integration and custom node.
Encrypt off-site and verify every exit status
Source the repository settings without printing them and send the exact staging directory:
set -Eeuo pipefail
set -a
. /root/.config/racknerd-recovery/restic.env
set +a
EXPORT_DIR='/var/lib/aixvps-recovery/export.REPLACE_WITH_REPORTED_NAME'
test -d "$EXPORT_DIR"
test -s "$EXPORT_DIR/SHA256SUMS"
(
cd "$EXPORT_DIR" || exit 1
sha256sum -c SHA256SUMS >/dev/null
)
restic backup "$EXPORT_DIR" --tag racknerd-n8n --tag manual || {
rc=$?
printf 'Restic backup FAILED (exit %s); do not mark this run successful.\n' "$rc" >&2
exit "$rc"
}
restic snapshots --latest 1
set -e alone can hide status inside conditionals or command chains, so keep the explicit check. Restic exit code 3 means some source data could not be read; it is still failure for this runbook. Do not schedule deletion or cancellation after a partial backup.
After the first backup and on a recurring schedule:
restic check
# More expensive full data verification; schedule deliberately:
restic check --read-data
Treat every nonzero result as failure. check validates repository structure/data, not whether n8n can start from the restored application state. Only an isolated application restore closes that gap.
Retention is an explicit change. After several proven restore points, choose policy and test it manually, for example:
restic forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6 || {
rc=$?
printf 'Retention/prune FAILED (exit %s); investigate before retrying.\n' "$rc" >&2
exit "$rc"
}
restic check
This sample deletes older restore points and can consume substantial I/O. Confirm legal retention and off-provider capacity first. Do not combine a first backup and first prune in one unattended job. Keep a separate immutable/versioned copy if repository deletion is a material risk.
After success, remove the local plaintext staging directory only when the application is healthy, repository check passed, and you no longer need it for the imminent restore drill. Verify the path is under /var/lib/aixvps-recovery/ and never use an unexpanded variable in rm -rf. Restic retains the encrypted copy.
Isolated restore drill: prove it without firing webhooks
Use a separate disposable host or isolated VM with no production data. Install the same recorded n8n and database versions and Restic. Keep production DNS, queues and trigger endpoints unchanged. Create a private restore directory and repository configuration independently; do not overwrite /root/.n8n, the live database or production Compose files.
install -m 700 -d /var/tmp/racknerd-restore
set -Eeuo pipefail
set -a
. /root/.config/racknerd-recovery/restic.env
set +a
restic snapshots
restic restore latest --target /var/tmp/racknerd-restore || {
rc=$?
printf 'Restore FAILED (exit %s).\n' "$rc" >&2
exit "$rc"
}
RESTORED_SUMS=$(find /var/tmp/racknerd-restore -type f -name SHA256SUMS -print -quit)
test -n "$RESTORED_SUMS"
RESTORED_DIR=$(dirname "$RESTORED_SUMS")
(
cd "$RESTORED_DIR" || exit 1
sha256sum -c SHA256SUMS
)
printf 'Verified staging restored at %s\n' "$RESTORED_DIR"
Hash agreement proves transfer integrity, not app recovery. Before starting the application, verify that every required source in EXTRA-PATHS.tsv has a corresponding nonempty staged entry under extra/, inspect deployment/recovery-definition, and restore each numbered item to its intended isolated path. A missing entry or custom-node/binary-data dependency fails the drill. The application test must remain isolated:
- Prepare the exact old n8n image/package and matching Postgres major version if applicable. Pull required images before cutting network access.
- For SQLite, place the entire restored state directory at the discovered n8n home path with original ownership/modes; do not regenerate encryption material. For Postgres, create a fresh isolated database cluster and restore the custom dump with
pg_restore; use the preserved secret configuration through a protected file, never terminal output. - Start the restored stack on a Docker network created with
--internal, or an equivalent VM network with no outbound internet. Publish no public port. If local UI inspection is needed, bind only127.0.0.1on the restore host and use an SSH tunnel. - Verify editor/user access, credential decryption with a non-sending test, workflows, every custom node and binary/external-data fixture from
EXTRA-PATHS.tsv, and expected database counts. Confirm the restored redacted deployment definition points only at isolated resources. Do not enable email, webhook, queue or scheduled-trigger egress. - Stop the stack before making any network change. If later promoting the restored system, explicitly deactivate schedules/webhooks first and plan cutover so old and new writers cannot run together.
A native SQLite smoke check after restoring files can inspect integrity without starting n8n:
test -f "$RESTORED_DIR/n8n-home/database.sqlite"
sqlite3 "$RESTORED_DIR/n8n-home/database.sqlite" 'PRAGMA integrity_check;'
Expected output is ok; any other result or nonzero exit fails the drill. This does not replace application-level credential/readiness tests. For Postgres, use a temporary isolated cluster and pg_restore according to the recorded major version; inspect its exit status and logs, then query expected non-sensitive counts.
Record date, snapshot ID, versions, RTO, verifier and any missing artifact. Fix the export and repeat until recovery succeeds. Delete the disposable environment only after retaining the non-secret drill report.
Migration and cancellation
For the general difference between stopping, cancelling and deleting, see the VPS cancellation policy guide. For export-versus-provider-rollback concepts, see backups versus snapshots.
Cancel old last
A safe migration overlaps the old annual service and new host. Build the replacement from a fresh image, harden it using the setup runbook, restore into isolation, then plan a final writer pause and delta backup. Do not keep two active n8n writers consuming the same webhooks.
- Lower DNS TTL in advance if your DNS provider supports it. Preserve the canonical URL plan and TLS path.
- Pause old n8n writers and external producers. Run the final checked export, Restic backup and repository verification. Confirm no new writes occurred afterward.
- Restore to the new host using matching versions. Verify database integrity, encryption material, custom nodes,
/healthz/readiness, a credential-decryption test and one controlled workflow with external side effects disabled. - Cut over DNS/reverse proxy and webhook destinations once. Resume only the new writers. Monitor old and new logs privately; the old service must not process work.
- Keep the old VPS intact through an observation window and at least one successful new-host backup/restore check. Do not cancel merely because the new editor loads.
- Export invoices and the private version/topology inventory. Delete old public DNS records only after traffic is confirmed moved; retain needed private recovery records.
- In RackNerd’s client area, choose end of billing cycle unless immediate destruction is intentional. Submit at least five days before ordinary renewal, seven for contract services. Read the exact service identity and data-loss warning before confirming.
- Save the cancellation confirmation. Check for a separate invoice or active service later; immediate cancellation help says termination can occur within 24 hours and data is lost.
Rollback: if the new host fails before cutover, keep DNS/producers unchanged and resume old writers only after confirming no new-host writes. If failure occurs after new writes, do not blindly resume the old database; reconcile or export the new state first. If cancellation has already terminated the old VPS, the off-provider repository and restore drill are the recovery path.
The acceptance test is a working new service, one writer, verified offsite backups, preserved billing records and explicit old-service cancellation confirmation. Only then remove old recovery credentials and local staging according to your retention policy.
Official terms and operational references
Primary sources: RackNerd Terms, billing/cancellation help, refund help, n8n backup documentation, n8n environment variables, Restic backup, Restic restore, and Restic check. Checked 2026-11-05. Product behavior can differ by image/version; discover first and test on disposable systems.