{"guide":"# Upgrade Guide \u2014 Redirector\n\nEvery command below is given for **Linux/macOS (sh)**, **Windows (PowerShell)**,\n**Docker** and **bare-metal Python**. Copy the block that matches your setup.\n\n> **Current:** `3.1.1` \u2014 see [CHANGELOG.md](../CHANGELOG.md) and\n> [Releases](https://github.com/authoritydmc/redirector/releases).\n\n---\n\n## 0. The 30-second version\n\n1. **Back up** \u2014 `/admin/backup` \u2192 *Create backup*, or the CLI command below.\n2. **Upgrade** \u2014 `docker compose pull && docker compose up -d` (or\n   `git pull` + restart for bare metal).\n3. **Verify** \u2014 `curl http://localhost/api/health/state`.\n4. **If it broke** \u2014 restore the archive. Do **not** run `flask db downgrade`.\n\nNothing else is required. Migrations run automatically on start, and a snapshot\nis taken automatically first if any migration is pending.\n\n---\n\n## 1. Before you upgrade\n\n### 1a. Back up\n\n**In the UI** (recommended): `http://localhost/admin/backup` \u2192 *Create backup*.\nThe archive lands in `data/backups/` and is downloadable from the same page.\n\n**From the command line** (works whether or not the app is running \u2014 the\nsnapshot uses SQLite's online backup API, so it is safe on a live database):\n\n```sh\n# Docker, Linux / macOS\ndocker compose exec app python -m app.utils.backup create --label \"before upgrade\"\n```\n\n```powershell\n# Docker, Windows (PowerShell)\ndocker compose exec app python -m app.utils.backup create --label \"before upgrade\"\n```\n\n```sh\n# Bare metal, Linux / macOS\n.venv/bin/python -m app.utils.backup create --label \"before upgrade\"\n```\n\n```powershell\n# Bare metal, Windows (PowerShell)\n.\\.venv\\Scripts\\python.exe -m app.utils.backup create --label \"before upgrade\"\n```\n\nCopy at least one archive **off the host**. An archive sitting in the same\ndirectory as the data protects you from a bad upgrade, not from a lost disk.\n\n### 1b. Note what you are running\n\n```sh\ncurl http://localhost/system-info\ncurl http://localhost/api/data-dir\n```\n\nKeep the version string. If you need to roll back, you need the exact tag.\n\n---\n\n## 2. Docker Compose \u2014 the common case\n\n### Linux / macOS\n\n```sh\ncd /path/to/redirector\ndocker compose pull                    # prebuilt image\n#   ...or, if you build from source:\ngit pull\ndocker compose build --no-cache\ndocker compose up -d\ndocker compose logs -f --tail=50 app   # watch migrations run\ndocker compose ps\n```\n\n### Windows (PowerShell)\n\n```powershell\ncd C:\\path\\to\\redirector\ndocker compose pull\n#   ...or, if you build from source:\ngit pull\ndocker compose build --no-cache\ndocker compose up -d\ndocker compose logs -f --tail=50 app\ndocker compose ps\n```\n\n### After either\n\n```sh\ncurl http://localhost/health\ncurl http://localhost/api/health/state\n```\n\nYour data is kept because `./data` is bind-mounted to `/app/data` in\n`docker-compose.yml`. **Do not delete `data/`, and do not run\n`docker compose down -v`.** The `-v` flag deletes named volumes; see\n[DATA-PERSISTENCE.md](DATA-PERSISTENCE.md).\n\n---\n\n## 3. Plain Docker (no compose)\n\n### Linux / macOS\n\n```sh\ndocker pull rajlabs/redirector:latest\ndocker rm -f redirector\ndocker run -d \\\n  --name redirector \\\n  --restart unless-stopped \\\n  -p 80:80 \\\n  -v \"$(pwd)/data:/app/data\" \\\n  -e REDIRECTOR_DATA_DIR=/app/data \\\n  rajlabs/redirector:latest\n```\n\n### Windows (PowerShell)\n\n```powershell\ndocker pull rajlabs/redirector:latest\ndocker rm -f redirector\ndocker run -d --name redirector --restart unless-stopped -p 80:80 `\n  -v \"${PWD}\\data:/app/data\" `\n  -e REDIRECTOR_DATA_DIR=/app/data `\n  rajlabs/redirector:latest\n```\n\nThe `-v \"$(pwd)/data:/app/data\"` is the part that matters. Omit it and you get a\nbrand new empty install with a new admin password \u2014 and your old data is still on\ndisk, unharmed, in the folder you forgot to mount.\n\nWith Redis, add `-e REDIS_HOST=redis --link redis:redis`.\n\n---\n\n## 4. Bare metal (venv + gunicorn / wsgi)\n\nStop the service before migrating, so nothing is writing while the schema moves.\n\n### Linux / macOS\n\n```sh\nsudo systemctl stop redirector        # or: pkill -f gunicorn\ncd /path/to/redirector\ngit fetch --tags\ngit checkout v3.1.1                   # or: git pull origin main\n\npython3 -m venv .venv\n.venv/bin/pip install -r requirements.txt\n.venv/bin/python -m app.utils.backup create --label \"before upgrade\"\nFLASK_APP=wsgi:app .venv/bin/flask db upgrade\nsudo systemctl start redirector\n```\n\n`FLASK_APP=wsgi:app` is what tells the migration CLI which application to\nopen. The entrypoint sets it for you; set it yourself when running migrations by\nhand, or the command can only find the app if it happens to run from the project\ndirectory.\n\n### Windows (Service)\n\n```powershell\nStop-Service Redirector\ncd C:\\path\\to\\redirector\ngit fetch --tags\ngit checkout v3.1.1                   # or: git pull origin main\n\n.\\.venv\\Scripts\\Activate.ps1\npip install -r requirements.txt\n.\\.venv\\Scripts\\python.exe -m app.utils.backup create --label \"before upgrade\"\n$env:FLASK_APP = \"wsgi:app\"\n.\\.venv\\Scripts\\flask.exe db upgrade\nStart-Service Redirector\n```\n\n### Windows (foreground, for testing)\n\n```powershell\n.\\.venv\\Scripts\\python.exe wsgi.py\n```\n\n### macOS (launchd)\n\n```sh\nlaunchctl stop com.authoritydmc.redirector\ncd /path/to/redirector\ngit fetch --tags && git checkout v3.1.1\npython3 -m venv .venv && .venv/bin/pip install -r requirements.txt\nFLASK_APP=wsgi:app .venv/bin/flask db upgrade\nlaunchctl start com.authoritydmc.redirector\n```\n\n---\n\n## 5. What the entrypoint does on every start\n\n`entrypoint.sh` is the part that makes upgrades safe, and it runs in this order\nfor a reason:\n\n1. **Check the data directory** is present and writable. Fail loudly. A volume\n   that silently became read-only is a common cause of \"the app is up but my\n   redirects are gone\" \u2014 actually, of *saving* failing, which is worse.\n2. **Apply a staged restore**, if one is waiting in `.restore-pending.zip`.\n3. **Refuse to run an old image against newer data.** If the database's schema\n   revision is not in this build's migration chain, the container exits with an\n   explanation rather than downgrading your schema.\n4. **Snapshot, but only when a migration is actually pending.** Backing up on\n   every restart fills the volume with identical copies and teaches people to\n   ignore the backup directory.\n5. **Migrate, with bounded retries.** The old loop was\n   `until flask db upgrade; do sleep 2; done`, which retried forever: a genuinely\n   broken migration left a container that looked alive but never served traffic.\n   It now fails after `REDIRECTOR_MIGRATE_ATTEMPTS` (default 5) and prints where\n   to find the rollback snapshot.\n6. **Start gunicorn.**\n\nUseful overrides:\n\n| Variable | Effect |\n|---|---|\n| `REDIRECTOR_DATA_DIR` | Where the data directory is. Default `/app/data` in Docker, `./data` otherwise. |\n| `REDIRECTOR_MIGRATE_ATTEMPTS` | Migration retry count. Default `5`. |\n| `REDIRECTOR_SKIP_MIGRATIONS=1` | Start without migrating. For triage only \u2014 the app may not match the schema. |\n| `REDIRECTOR_APP_VERSION` | Version reported by `/system-info`. Set at build time by the Dockerfile. |\n\n---\n\n## 6. Verify after upgrading\n\n```sh\ncurl http://localhost/health              # {\"status\":\"ok\",\"version\":\"3.1.1+...\"}\ncurl http://localhost/api/health/state    # data dir writable, schema revision, pending migrations\ncurl http://localhost/api/latest-version  # what is published\n```\n\n```powershell\ncurl.exe http://localhost/health\ncurl.exe http://localhost/api/health/state\n```\n\nIn the UI, check **`/system-info`** \u2014 the banner shows *Update available* when a\nnewer tag exists \u2014 and **`/admin/backup`** for the schema state and your archive\ninventory.\n\nThen confirm a shortcut actually resolves, because that is the thing you care\nabout:\n\n```sh\ncurl -sI http://localhost/health-test | head -1   # expect: HTTP/1.1 302\n```\n\n---\n\n## 7. Rollback\n\n**Roll back by restoring a backup, not by downgrading the schema.**\n`flask db downgrade` inverts a migration and can drop columns that hold your data.\nIt is not a rollback strategy.\n\n### Step 1 \u2014 pick the version\n\n```sh\ndocker compose logs --tail=80 app\n```\n\nLook for the `[redirector] FATAL:` line. It names the specific problem: a\nmigration that failed, or a schema newer than the image supports.\n\n### Step 2 \u2014 start the previous version (data untouched)\n\n```sh\n# Docker Compose\ndocker compose down\nsed -i 's|image: rajlabs/redirector:latest|image: rajlabs/redirector:3.1.0|' docker-compose.yml\ndocker compose up -d\n```\n\n```powershell\n# Docker Compose, Windows\ndocker compose down\n(Get-Content docker-compose.yml) -replace 'rajlabs/redirector:latest', 'rajlabs/redirector:3.1.0' | Set-Content docker-compose.yml\ndocker compose up -d\n```\n\nIf the container refused to start because the data is *newer* than the image, the\nprevious image is the right one \u2014 the refusal is the guard working.\n\n### Step 3 \u2014 if the data itself needs restoring\n\n```sh\n# Linux / macOS\ndocker compose down\ndocker compose run --rm --no-deps app \\\n  python -m app.utils.backup restore /app/data/backups/<name>.zip\ndocker compose up -d\n```\n\n```powershell\n# Windows (PowerShell)\ndocker compose down\ndocker compose run --rm --no-deps app `\n  python -m app.utils.backup restore /app/data/backups/<name>.zip\ndocker compose up -d\n```\n\nThe app is stopped, so the restore is applied immediately. It takes a safety\nsnapshot of the current state first, so this step is itself reversible.\n\nThen migrations bring the restored database forward to whatever the running\nversion expects.\n\n---\n\n## 8. If things go wrong\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| New admin password after upgrade | `/app/data` is not the folder you think \u2014 usually a named volume replaced a bind mount | See [DATA-PERSISTENCE.md](DATA-PERSISTENCE.md) \u00a74; your data is usually still in `./data` |\n| `FATAL: the database schema (X) is NEWER than this image supports` | Image older than the data | `docker compose pull && docker compose up -d`, or restore a backup taken with the older version |\n| `FATAL: database migration failed after 5 attempts` | A migration is broken, or the volume is read-only | `docker compose logs app`; check volume permissions; restore the `pre-upgrade` snapshot from `data/backups/` |\n| Container restarts forever, no traffic | A previous version of the entrypoint's infinite retry loop | Upgrade to an image with bounded retries; check the logs for the real error |\n| `/r/` stopped working | The hosts entry was lost | `127.0.0.1 r` \u2014 see `/enable-r-instructions` and `GET /api/r-status` |\n| `no such table: upstream_cache` in the logs | Migrations have not run yet | Check the entrypoint output; run `FLASK_APP=wsgi:app flask db upgrade` manually if the app was started outside the entrypoint |\n\n---\n\n## 9. FAQ\n\n**Do I need to recreate the database?** No. Migrations add columns; they do not\nrecreate tables.\n\n**Will my shortcuts stay?** Yes, as long as `/app/data` still points at the same\nhost folder. The database path in the config is stored relative so that moving\nthe folder between machines works.\n\n**Do I need `flask db migrate`?** No, and you should not run it against a\nproduction install \u2014 it autogenerates a migration from whatever drift the local\nmodels have. Upgrades apply existing migrations with `flask db upgrade`.\n\n**Is the admin password in the backup archive?** Yes, hashed, alongside your MFA\nseeds. Treat archives as secrets: anyone with one can read the config.\n\n**How do I know an update is available?** Every page checks\n`GET /api/latest-version` once a day (cached in `localStorage`); `/system-info`\nshows a persistent banner when a newer tag exists.\n\n---\n\n*Need help?* Open an issue at\n`https://github.com/authoritydmc/redirector/issues` and include\n`/admin/backup/state.json`, the output of `curl http://localhost/api/health/state`,\nand `docker compose logs app`.\n"}
