# Sovereign AI Workstation: Setup and Migration Guide Version: 2026-10-04 Companion article: https://jwatte.com/blog/sovereign-ai-playbook-remote-desktop-commander/ ## Read before running This is a phased deployment guide, not an unattended installer. Stop when a check fails. It contains no real account credentials, server identifiers, or customer data. Never paste passwords, OAuth codes, API keys, private keys, or generated credential files into an AI conversation or public repository. The reference host was tested on Ubuntu 26.04.1 LTS after a controlled upgrade from 24.04, with Node 24, independently managed Python 3.12, mirrored NVMe storage, and four Docker services. Use a supported Ubuntu LTS release validated with your hardware and applications. Never initialize disks or reinstall an OS over existing data. Examples assume a normal Linux user and a tested administrator route. They deliberately do not automate purchases, DNS delegation, disk formatting, firmware updates, OS release upgrades, provider sign-in, bulk production deployment, or deletion of an old workstation. ## 1. Hardware and cost The OVHcloud US Eco catalog checked October 4, 2026 lists SYS-5 from $125/month and a $125 setup fee. The configuration family starts with dual Intel Xeon Silver 4214R processors, 96 GB RAM, two 960 GB NVMe drives, and public bandwidth from 1 Gbps. Check current stock, location, taxes, renewal and bandwidth terms: https://eco.us.ovhcloud.com/. That is an Intel Xeon example, not an AMD EPYC quote. Compare EPYC offers independently. The budget excludes AI subscriptions, model API calls, generated artwork, backup storage and paid access services. CPU-only 70B inference is an experiment, not a promised responsive service. Use software RAID1 during the provider's initial installation where appropriate. Two mirrored drives yield roughly one drive's usable capacity. Preserve both EFI boot paths and test recovery. RAID is not a backup. Self-hosting is not complete independence: the infrastructure provider, Remote MCP relay, cloud APIs and optional Cloudflare access path remain trust boundaries. A locally hosted dashboard does not prove every request stays local. ## 2. Software map | Layer | Software | Purpose | |---|---|---| | Host | Ubuntu LTS, OpenSSH, systemd, tmux | OS, secure access, service startup and reconnectable sessions | | Runtimes | Node 24 via nvm; Python via uv | Explicit tool runtimes independent of OS defaults | | Remote control | Remote Desktop Commander | Authorized terminal and filesystem bridge | | Native assistants | Claude Code, OpenAI Codex, Kimi Code | Provider-supported interactive account sessions | | Git editing | Aider | Editing with a configured API or local model | | Local inference | Ollama | Load and serve local models | | Gateway | LiteLLM and PostgreSQL | Routing, limited virtual keys and persistent gateway state | | Dashboard | Open WebUI | Browser interface to the gateway | | Deployment | Git, gh, Netlify CLI, Wrangler | Repositories and platform-specific management | | Recovery | Restic and rclone | Encrypted snapshots and controlled transport | | Optional | cloudflared, Tailscale, browser desktop | Access routes for a documented need | | Optional | Deno, Vercel CLI, PM2 | Existing project/runtime requirements | | Optional | Gitleaks, Trivy, Figma integration, Ideogram | Secret review, vulnerability review, design context and artwork | | Optional | Qdrant, n8n | An actual retrieval or automation workload | Do not have two supervisors fight to restart the same process. Scanners are useful checks, not proof of perfect security. Optional components are not all installed or required by this guide. ## 3. Preflight and base packages First inspect the intended host and compare it with your provider console: ```bash hostnamectl cat /etc/os-release uname -r id lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS cat /proc/mdstat free -h df -h / /boot systemctl --failed ``` Confirm SSH and the provider's recovery console before filtering ports or restarting. Retain the old workstation. Review the proposed package changes: ```bash sudo apt-get update sudo apt-get --simulate upgrade sudo apt-get upgrade --no-remove sudo apt-get install --no-remove ca-certificates curl gnupg git jq tmux \ unzip python3 python3-yaml build-essential ripgrep restic rclone ufw ``` On a fresh host using SSH port 22 only, preserve SSH before enabling filtering: ```bash sudo ufw allow 22/tcp sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw enable sudo ufw status verbose ``` Do not blindly apply this block to a host using another SSH port or existing production listeners. Inspect Docker-published ports separately; UFW alone is not a complete container exposure policy. ## 4. User runtimes and CLIs Use official sources. Download and inspect installers and follow the publisher's integrity instructions. A locally calculated checksum is an audit record, not verification unless compared against a trusted expected value. Install nvm using https://github.com/nvm-sh/nvm#installing-and-updating, then: ```bash export NVM_DIR="$HOME/.nvm" . "$NVM_DIR/nvm.sh" nvm install 24 nvm alias default 24 nvm use 24 node --version npm --version ``` Record the exact Node patch release used in services. A systemd process does not inherit interactive shell initialization. Install uv from https://docs.astral.sh/uv/getting-started/installation/, then: ```bash uv python install 3.12 uv tool install --managed-python --python 3.12 aider-chat==0.86.2 uv tool update-shell aider --version ``` Managed Python prevents Aider from silently depending on the operating system's interpreter. These observed deployment-tool versions form a reproducibility record, not a recommendation to freeze versions indefinitely: ```bash npm install --global @openai/codex@0.160.0 netlify-cli@27.10.2 wrangler@4.147.0 codex --version netlify --version wrangler --version ``` Install Claude Code through https://code.claude.com/docs/en/setup and Kimi Code through its official documentation at https://www.kimi.com/code/docs/en/. Verify the publisher, selected package and installed interface. Similarly named npm and Python distributions can differ; do not install two distributions that both own the `kimi` command. The reference host reported Claude Code 2.1.285 and Kimi Code 2.1.1. Install GitHub CLI from https://cli.github.com/. Add Deno or Vercel CLI only where an inventoried project needs them. Preserve project lockfiles rather than replacing them with a global dependency set. ## 5. Subscription authentication is not API authentication Complete native sign-ins in your terminal and the provider's browser, not a shared transcript: ```bash codex login --device-auth codex login status claude auth login --claudeai claude auth status kimi login ``` Confirm command options against your installed versions. Use the provider's documented headless device flow or callback forwarding as needed. Do not assume copying another machine's session files establishes a supported working login. Subscriptions have limits and terms. Do not inject OAuth subscription tokens into a general API gateway. API requests need supported API credentials and billing. Do not silently switch to paid APIs when a subscription reaches its limit. To avoid environment conflicts for one Claude subscription process: ```bash env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN -u ANTHROPIC_BASE_URL claude ``` Do not replace `~/.claude/settings.json`. Preserve permissions, hooks, MCP settings and project memories. ## 6. Remote Desktop Commander Official instructions: https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/remote-device/README.md. Install a selected version in a dedicated user directory: ```bash mkdir -p "$HOME/.local/share/desktop-commander-runtime" npm install --prefix "$HOME/.local/share/desktop-commander-runtime" \ @wonderwhy-er/desktop-commander@0.2.52 node "$HOME/.local/share/desktop-commander-runtime/node_modules/@wonderwhy-er/desktop-commander/dist/index.js" remote ``` Authorize the device in your browser, confirm matching codes, and connect the AI-side integration. Verify hostname through an actual harmless remote command. Run the connector as a normal account. Keep unrelated files and secrets outside its intended scope. Review restrictions rather than clearing all blocked commands. Tool filters are not a complete sandbox. After initial authorization, stop the foreground launcher before enabling a service. Substitute actual absolute paths in this user unit: ```ini # ~/.config/systemd/user/desktop-commander.service [Unit] Description=Authorized Remote Desktop Commander After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=/home/YOUR_USER Environment=HOME=/home/YOUR_USER Environment=PATH=/home/YOUR_USER/.nvm/versions/node/YOUR_NODE_VERSION/bin:/home/YOUR_USER/.local/bin:/usr/local/bin:/usr/bin:/bin ExecStart=/home/YOUR_USER/.nvm/versions/node/YOUR_NODE_VERSION/bin/node /home/YOUR_USER/.local/share/desktop-commander-runtime/node_modules/@wonderwhy-er/desktop-commander/dist/index.js remote Restart=on-failure RestartSec=15 UMask=0077 NoNewPrivileges=yes [Install] WantedBy=default.target ``` `NoNewPrivileges=yes` deliberately prevents privilege escalation from that service. Use a separately approved maintenance session for administrator work. Protect saved connector credentials. ```bash sudo loginctl enable-linger "$USER" systemctl --user daemon-reload systemctl --user enable --now desktop-commander.service systemctl --user is-active desktop-commander.service ``` Test real remote execution and a controlled restart. Keep SSH and the recovery console independent. Stopping the process and revoking authorization are different operations. ## 7. Docker and deployment directory Install Docker Engine and Compose using https://docs.docker.com/engine/install/ubuntu/. The repository suite must match the running Ubuntu release. Docker-group membership is effectively privileged; these examples use `sudo docker` rather than granting an AI account blanket access. ```bash sudo docker version sudo docker compose version sudo systemctl enable --now docker mkdir -p "$HOME/sovereign-ai/config" chmod 700 "$HOME/sovereign-ai" cd "$HOME/sovereign-ai" ``` All following relative files belong here. Do not initialize over an existing installation without comparing and backing it up. ## 8. Create credentials once Save as `initialize-secrets.py`. It refuses to overwrite `.env` and does not print passwords: ```python import os, pathlib, secrets os.umask(0o077) root = pathlib.Path.cwd() if (root / '.env').exists(): raise SystemExit('Existing .env retained; review instead of regenerating.') email = input('Dashboard administrator email: ').strip() if '@' not in email or any(c in email for c in '\r\n\"\x27$# '): raise SystemExit('Enter a simple valid email address.') values = {k: secrets.token_hex(32) for k in ( 'POSTGRES_PASSWORD', 'LITELLM_SALT_KEY', 'LITELLM_UI_PASSWORD', 'WEBUI_SECRET_KEY', 'WEBUI_ADMIN_PASSWORD')} values['LITELLM_MASTER_KEY'] = 'sk-' + secrets.token_hex(32) values['WEBUI_ADMIN_EMAIL'] = email with (root / '.env').open('x') as f: f.write(''.join(k + '=' + v + '\n' for k, v in values.items())) with (root / 'INITIAL-ACCESS.txt').open('x') as f: f.write('Open WebUI: http://localhost:8080\nUser: ' + email + '\nPassword: ' + values['WEBUI_ADMIN_PASSWORD'] + '\n\nLiteLLM: http://localhost:4000/ui' + '\nUser: admin\nPassword: ' + values['LITELLM_UI_PASSWORD'] + '\n') for name in ('.env', 'INITIAL-ACCESS.txt'): (root / name).chmod(0o600) print('Private initial credentials created. Read them only in your own terminal.') ``` ```bash python3 initialize-secrets.py printf '%s\n' '.env' 'INITIAL-ACCESS.txt' 'backups/' > .gitignore ``` Keep recovery credentials in an independent vault. Never place this directory beneath a website publish directory. ## 9. Gateway configuration Save as `config/litellm.yaml`: ```yaml model_list: - model_name: local-hermes3 litellm_params: model: ollama_chat/hermes3:8b api_base: http://ollama:11434 timeout: 180 general_settings: master_key: os.environ/LITELLM_MASTER_KEY database_url: os.environ/DATABASE_URL store_prompts_in_spend_logs: false litellm_settings: set_verbose: false ``` No external model is configured. Review all application and proxy logging before sending sensitive data. One disabled log option is not a universal no-logging guarantee. ## 10. Private Compose baseline Save as `compose.yaml`: ```yaml name: sovereign-ai x-defaults: &defaults restart: unless-stopped logging: driver: json-file options: {max-size: "10m", max-file: "3"} services: db: <<: *defaults image: postgres:16 environment: POSTGRES_DB: litellm POSTGRES_USER: litellm POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required} volumes: [postgres_data:/var/lib/postgresql/data] networks: [database] mem_limit: 2g healthcheck: test: [CMD-SHELL, "pg_isready -U litellm -d litellm"] interval: 5s timeout: 5s retries: 30 ollama: <<: *defaults image: ollama/ollama:latest environment: OLLAMA_HOST: 0.0.0.0:11434 OLLAMA_NUM_PARALLEL: "1" OLLAMA_MAX_LOADED_MODELS: "1" OLLAMA_KEEP_ALIVE: 5m volumes: [ollama_data:/root/.ollama] ports: ["127.0.0.1:11434:11434"] networks: [app] cpus: 24 mem_limit: 48g healthcheck: test: [CMD, ollama, list] interval: 15s timeout: 10s retries: 30 litellm: <<: *defaults image: docker.litellm.ai/berriai/litellm:main-stable command: ["--config=/app/config.yaml", "--host=0.0.0.0", "--port=4000"] environment: DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD:?required}@db:5432/litellm LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?required} LITELLM_SALT_KEY: ${LITELLM_SALT_KEY:?required} STORE_MODEL_IN_DB: "True" UI_USERNAME: admin UI_PASSWORD: ${LITELLM_UI_PASSWORD:?required} LITELLM_TELEMETRY: "False" volumes: [./config/litellm.yaml:/app/config.yaml:ro] ports: ["127.0.0.1:4000:4000"] networks: [app, database] mem_limit: 4g depends_on: db: {condition: service_healthy} ollama: {condition: service_healthy} healthcheck: test: [CMD, python3, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness', timeout=5)"] interval: 15s timeout: 10s start_period: 90s retries: 30 webui: <<: *defaults image: ghcr.io/open-webui/open-webui:v0.11.4 environment: WEBUI_AUTH: "True" WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:?required} WEBUI_ADMIN_EMAIL: ${WEBUI_ADMIN_EMAIL:?required} WEBUI_ADMIN_PASSWORD: ${WEBUI_ADMIN_PASSWORD:?required} ENABLE_SIGNUP: "False" ENABLE_OLLAMA_API: "False" ENABLE_OPENAI_API: "True" OPENAI_API_BASE_URL: http://litellm:4000/v1 OPENAI_API_KEY: ${WEBUI_GATEWAY_KEY:-not-provisioned} ENABLE_DIRECT_CONNECTIONS: "False" ENABLE_FORWARD_USER_INFO_HEADERS: "False" ENABLE_EVALUATION_ARENA_MODELS: "False" CORS_ALLOW_ORIGIN: http://localhost:8080;http://127.0.0.1:8080 DO_NOT_TRACK: "true" SCARF_NO_ANALYTICS: "true" ANONYMIZED_TELEMETRY: "False" volumes: [webui_data:/app/backend/data] ports: ["127.0.0.1:8080:8080"] networks: [app] mem_limit: 12g depends_on: litellm: {condition: service_healthy} healthcheck: test: [CMD, python3, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=5)"] interval: 20s timeout: 10s start_period: 120s retries: 30 volumes: postgres_data: ollama_data: webui_data: networks: app: database: internal: true ``` Adjust limits for smaller hosts. `cpus: 24` is a quota, not core pinning. WebUI reaches the `litellm` service on a shared network; localhost inside a container does not refer to another container. Resolve and record selected image digests before deploying. Review the image versions and vulnerability findings. The initial pull uses mutable tags where shown. Save as `lock-images.py`: ```python import json, pathlib, subprocess root = pathlib.Path.cwd() output = root / 'compose.images.json' if output.exists(): raise SystemExit('Existing image lock retained. Review updates explicitly.') cmd = ['sudo', 'docker', 'compose', '--env-file', str(root / '.env'), '-f', str(root / 'compose.yaml')] subprocess.run(cmd + ['config', '--quiet'], check=True) subprocess.run(cmd + ['pull'], check=True) # Capture in memory: the expanded configuration contains credentials. Never print it. cfg = json.loads(subprocess.check_output(cmd + ['config', '--format', 'json'])) locked = {'services': {}} for name, spec in cfg['services'].items(): obj = json.loads(subprocess.check_output(['sudo', 'docker', 'image', 'inspect', spec['image']]))[0] digests = obj.get('RepoDigests') or [] if not digests: raise SystemExit('No digest available for ' + name) locked['services'][name] = {'image': digests[0]} output.write_text(json.dumps(locked, indent=2) + '\n') print('Image references locked. Retain this file with the deployment record.') ``` ```bash python3 lock-images.py dc() { sudo docker compose --env-file "$PWD/.env" -f "$PWD/compose.yaml" -f "$PWD/compose.images.json" "$@"; } dc config --quiet dc up -d --wait --wait-timeout 600 db ollama litellm dc exec -T ollama ollama pull hermes3:8b ``` The model download consumes network and disk capacity, not a paid inference API. Review the model's license for your use. ## 11. Restricted dashboard key Save as `create-dashboard-key.py`. Do not supply the administrator key to WebUI: ```python import json, pathlib, urllib.request p = pathlib.Path('.env') v = dict(line.split('=', 1) for line in p.read_text().splitlines() if line and not line.startswith('#')) if v.get('WEBUI_GATEWAY_KEY'): raise SystemExit('Existing dashboard key retained.') request = urllib.request.Request('http://127.0.0.1:4000/key/generate', data=json.dumps({'key_alias': 'open-webui', 'models': ['local-hermes3']}).encode(), headers={'Authorization': 'Bearer ' + v['LITELLM_MASTER_KEY'], 'Content-Type': 'application/json'}) with urllib.request.urlopen(request, timeout=45) as response: key = json.load(response)['key'] if not isinstance(key, str) or not key.startswith('sk-') or '\n' in key: raise SystemExit('Unexpected key response; inspect privately.') with p.open('a') as f: f.write('WEBUI_GATEWAY_KEY=' + key + '\n') p.chmod(0o600) print('Restricted dashboard key configured; value not displayed.') ``` ```bash python3 create-dashboard-key.py dc up -d --wait --wait-timeout 600 webui dc ps ``` Add external API models only through authenticated administration or protected configuration. Verify provider account region, endpoint and current model IDs. Set suitable key limits and budgets and test enforcement. Deliberately update the dashboard key's model allowlist when adding a model. Do not disable authentication to solve a permission error. ## 12. Verify before public exposure ```bash curl --fail http://127.0.0.1:8080/health curl --fail http://127.0.0.1:4000/health/readiness curl --fail http://127.0.0.1:11434/api/tags ss -lnt ``` Unauthenticated protected model requests should be rejected. Verify signup is disabled. Save this as `verify-gateway.py` for a small authenticated local test that does not display the key: ```python import json, pathlib, urllib.request, urllib.error v = dict(line.split('=', 1) for line in pathlib.Path('.env').read_text().splitlines() if line and not line.startswith('#')) try: urllib.request.urlopen('http://127.0.0.1:4000/v1/models', timeout=10) except urllib.error.HTTPError as e: if e.code != 401: raise SystemExit('Unexpected unauthenticated status: ' + str(e.code)) else: raise SystemExit('Gateway accepted a request without credentials.') request = urllib.request.Request('http://127.0.0.1:4000/v1/chat/completions', data=json.dumps({'model': 'local-hermes3', 'messages': [ {'role': 'user', 'content': 'Reply exactly with LOCAL_TEST_OK'}], 'max_tokens': 16, 'temperature': 0}).encode(), headers={'Authorization': 'Bearer ' + v['WEBUI_GATEWAY_KEY'], 'Content-Type': 'application/json'}) with urllib.request.urlopen(request, timeout=180) as response: result = json.load(response) reply = result['choices'][0]['message']['content'].strip() if 'LOCAL_TEST_OK' not in reply: raise SystemExit('Model answered, but the acceptance response differed.') print('Authenticated local generation passed. No credentials displayed.') ``` ```bash python3 verify-gateway.py ``` From your office computer, substitute your actual user and server: ```bash ssh -N -o ExitOnForwardFailure=yes \ -L 127.0.0.1:8080:127.0.0.1:8080 \ -L 127.0.0.1:4000:127.0.0.1:4000 USER@YOUR_SERVER ``` Verify the first SSH host key through an independent trusted channel. Never disable host-key checking to bypass a changed-key warning. Browser localhost must be on the computer running this tunnel. Read `INITIAL-ACCESS.txt` privately, change initial passwords, and test a complete signed-in browser chat. Health endpoints alone do not prove that path. ## 13. Cloudflare, DNS and optional browser desktop Begin with SSH forwarding. For public hostnames, create a named Cloudflare Tunnel and a deny-by-default Access application for the intended identities. Configure one-time PIN or your selected identity provider. Test an allowed identity and a rejected unauthorized visitor. Check authoritative nameservers first. A domain visible in a dashboard is not necessarily authoritative there. If Netlify remains authoritative, verify whether your Cloudflare account supports the needed partial-zone or subdomain onboarding. A CNAME alone does not establish a protected Cloudflare application. Do not move an entire zone or modify mail records casually. Origin address depends on where cloudflared runs. On the host, use the host's loopback service. In Docker, use the service name on a shared private network, such as `http://webui:8080`, not that container's localhost. Tunnel setup: https://developers.cloudflare.com/tunnel/get-started/ Origin testing: https://developers.cloudflare.com/tunnel/troubleshooting/https-origins/ A persistent browser desktop is optional. Give it authenticated access, limited workspace mounts and a private profile. Never publish an unauthenticated debugging port. Windows encrypted browser sessions are not a portable Linux authentication mechanism. Export bookmarks/extensions as appropriate, then reauthenticate normally. ## 14. Migration inventory and verified transfer Preserve relative directories. Never flatten all same-named `CLAUDE.md`, memory, or script files into one folder. For every project, record its full source path, Git remote/branch/commit, uncommitted changes, submodules and LFS use, locked dependencies, build and test commands, provider/team/account IDs, domains, deployment branch, base/publish directories, functions and worker bindings. Record environment variable names and authorized secret sources, not their values in the public inventory. Inventory Windows Scheduled Tasks, services, mapped drives, local databases, Windows-only scripts, global npm tools, Python environments, cloud-drive sync, browser logins and Windows-protected credentials. Export or reauthorize dependencies through supported methods. Copying AppData is not a Linux deployment. Exclude caches and `node_modules` only with a working dependency lock and tested reinstall. Track exclusions explicitly: a sensitive-filename filter may omit legitimate configuration. Source-only export is not a complete account or disaster-recovery backup. Transfer over authenticated encrypted transport to a restricted staging directory. Compare the archive hash, extract with path-traversal protection, then compare each selected destination file to the source manifest. Record missing, changed, excluded and unreadable files. Do a final delta inventory after authors stop editing on the old workstation. Retain the original until sign-off. ## 15. Repositories and deployment handover Use private repositories unless public source is intentional. Prefer one repository per independent site; a motel portfolio can use one repository with a separate property directory and deployment mapping. Preserve existing history and functioning hosting connections. An empty repository is not a backup. Authenticate and verify deployment tools on the destination: ```bash gh auth login gh auth status netlify login netlify status wrangler login wrangler whoami ``` Use documented headless flows where required. Never store tokens in command history. Secret-scan and review staged files before pushing. Build from the destination using lockfiles. A Git checkout does not recreate hosted databases, provider secrets, scheduled functions or worker bindings. For a reviewed Netlify project, select its exact site ID and start with a preview: ```bash netlify deploy --site YOUR_SITE_ID --dir YOUR_BUILD_DIRECTORY ``` Validate the returned preview and its actual contents before production: ```bash netlify deploy --prod --site YOUR_SITE_ID --dir YOUR_BUILD_DIRECTORY ``` These command shapes do not replace an existing project deployment script. Preserve function/edge-function packaging, validation gates and site-specific configuration. Use `wrangler deploy --dry-run` for a Workers packaging check, then verify account, environment, bindings and routes before production. Netlify CLI: https://docs.netlify.com/api-and-cli-guides/cli-guides/get-started-with-cli/ Wrangler: https://developers.cloudflare.com/workers/wrangler/commands/ ## 16. Backups and recovery Use an independent Restic repository and separately protected credentials. The retiring AWS machine must not be the only backup location or the only holder of decryption keys. Back up sources, protected configuration, version locks, service secrets and application-consistent database exports. Test a PostgreSQL dump by restoring to a disposable database. Use SQLite's backup mechanism or a consistent stopped-service snapshot for WebUI. Copying live database files blindly is not a verified backup. After privately configuring `RESTIC_REPOSITORY`, `RESTIC_PASSWORD_FILE` and storage credentials, the basic workflow is: ```bash # init is ONLY for a new repository. restic init restic backup "$HOME/projects" "$HOME/sovereign-ai" \ --exclude '**/node_modules' --exclude '**/.cache' restic snapshots restic check --read-data ``` The Compose directory alone does not contain Docker named-volume data. Include consistent volume/database exports. Restore into a separate test location and compare files and application behavior before enabling retention and pruning. Record the restore result, not merely the upload result. Restic: https://restic.readthedocs.io/en/stable/ Rclone: https://rclone.org/docs/ Use rclone for a deliberate drop zone or transfer, not blind bidirectional synchronization of live Git repositories. Document conflict and deletion behavior before scheduling it. ## 17. Maintenance Treat package, container, CLI, firmware and OS-release updates as separate changes. Record versions and rollback procedures, read release notes, and review vulnerabilities. Digest pinning does not install security updates by itself. ```bash sudo apt-get update sudo apt-get --simulate upgrade sudo apt-get upgrade --no-remove fwupdmgr get-upgrades do-release-upgrade -c ``` The final two commands check availability only. Firmware installation and a real `sudo do-release-upgrade` require an explicit maintenance window, independent backup, recovery console, reviewed removals and post-reboot validation. Keep safety checks enabled. Do not force a firmware installation just to erase a historical warning. Preserve required provider boot, serial-console, network and SSH settings. Review third-party repository suites after release upgrades. Verify both EFI paths, RAID, running kernel, package consistency, firewall listeners, connector restart, database health and authenticated inference. Ubuntu upgrades: https://documentation.ubuntu.com/server/how-to/software/upgrade-your-release/ Firmware tools: https://github.com/fwupd/fwupd If the provider dashboard retains the original OS label, use a documented metadata operation or support request. Do not reinstall just to change a label. A local OS probe does not itself update the provider's record. ## 18. Decommission sign-off Every unchecked item is a reason to retain the source or an equivalent recoverable environment. - [ ] All selected files matched source hashes; final delta completed. - [ ] Exclusions and unreadable files reviewed, not ignored. - [ ] All accessible hosting teams/accounts and directly shared projects inventoried. - [ ] Repositories contain reviewed source and required history without exposed secrets. - [ ] Each project builds and passes meaningful tests from the destination. - [ ] Preview and production verified by deployment ID/content, not an old HTTP 200 page. - [ ] Functions, worker bindings, databases, secrets, schedules and webhooks work without AWS. - [ ] Authoritative DNS, mail, TLS and access policies validated. - [ ] Native AI and deployment-provider sign-ins work on the new host. - [ ] Windows-only jobs, mapped drives, browser logins and local data have replacements. - [ ] A reboot restored services and remote control without the office computer. - [ ] Independent encrypted backups and credentials survive deletion of AWS. - [ ] A restore was tested and its result recorded. - [ ] Monitoring, cost controls, rollback ownership and an observation period are agreed. - [ ] The owner approved final retirement after reviewing evidence. This guide is a baseline and checklist, not certification that a specific migration is complete. Keep dated private records of actual results, failures and exceptions.