# VPS 12-24-720 Forgejo/Woodpecker migration plan

Last updated: `2026-05-30` (`UTC`)

## Purpose

This is the tracked, non-sensitive migration plan for moving the current
Forgejo/Woodpecker control-plane VPS to a new IONOS `VPS 12-24-720` Ubuntu
`24.04` Linux server, while also preparing the host for JetBrains Rider Gateway
remote development and Visual Studio Community 2022 remote Linux development.

Sensitive companion:

- canonical sensitive runbook copy:
  `C:\HyperTwist\docs\ops\FORGEJO_WOODPECKER_VPS_A_TO_Z_SENSITIVE_RUNBOOK_VPS_12_24_720_UBUNTU_24_04.md`

Handling note:

- the runbook copy carries live and historical secret-bearing material
- treat it as sensitive documentation regardless of which repo-local copy you open

## Current baseline

Current canonical runbook:

- `C:\HyperTwist\docs\ops\FORGEJO_WOODPECKER_VPS_A_TO_Z_SENSITIVE_RUNBOOK_VPS_12_24_720_UBUNTU_24_04.md`
- archived predecessor runbook:
  `C:\HyperTwist\docs\ops\archive\DEPRECATED_FORGEJO_WOODPECKER_VPS_A_TO_Z_SENSITIVE_RUNBOOK.md`
- old-host reconstruction index:
  `C:\HyperTwist\docs\ops\OLD_VPS_RECONSTRUCTION_INDEX_2026-05-29.md`

Current live control-plane shape documented there:

- Forgejo
- Woodpecker server
- VPS-local ops-only Woodpecker Docker agent
- stack root: `/srv/scriptorium-stack`
- NGINX reverse proxy
- certbot-managed TLS
- backup scripts and cron jobs
- ops guards and journald retention policy

## Live discovery findings from 2026-05-29

### New VPS `212.227.13.220`

Confirmed directly over SSH:

- host reachable over SSH as `root`
- Ubuntu `24.04.4 LTS` is installed
- no active Plesk services found
- no installed Plesk packages found
- no Apache or NGINX listener active yet
- only SSH is listening publicly at the moment
- current global IPv4: `212.227.13.220`
- no global IPv6 address was visible during inspection
- root filesystem is effectively empty and healthy with ample free space

Operational consequence:

- no reinstall is required just to remove Plesk
- proceed with direct bootstrap on the existing new host
- do not publish `AAAA` records for the new host until a real global IPv6 is
  present and configured

### Old production/control-plane VPS `212.227.22.66`

Confirmed directly over SSH:

- still the live Forgejo/Woodpecker control-plane host
- stack is still under `/srv/scriptorium-stack`
- containers healthy:
  - `forgejo`
  - `forgejo-postgres`
  - `woodpecker-server`
  - `woodpecker-agent`
- current live static roots:
  - `/var/www/scriptoriumai`
  - `/var/www/verticaltension`

Important nuance:

- the old host still has a `*.plesk.page` hostname label
- that does **not** mean active Plesk is still running
- treat it as leftover hostname labeling only

## Recommended migration method

Preferred method:

- fresh Ubuntu `24.04` bootstrap on the new VPS
- then explicit application-level stack migration

Not recommended as the default:

- whole-disk clone
- opaque VM/image lift-and-shift

Reason:

- explicit migration preserves the stack and data that matter
- it avoids dragging stale host state into the replacement server
- rollback is simpler because the old VPS can remain live until the new one is
  validated

## Exact migration scope

Migrate:

- Forgejo Postgres data
- Woodpecker Postgres data
- Forgejo data directory
- Woodpecker server-data directory
- `/srv/scriptorium-stack/.env`
- `/srv/scriptorium-stack/docker-compose.yml`
- active NGINX vhost files that still belong on the replacement host
- backup scripts
- cron files
- ops guard scripts
- journald retention config
- alerting env, if still in use

Recreate on the new host rather than copying blindly:

- SSH host keys
- firewall state
- certbot certificate issuance
- machine-specific `systemd` state
- package caches
- Docker image/cache churn

## Pending operator input

### New VPS access

Need:

- new VPS public IPv4: `212.227.13.220`
- new VPS public IPv6, if any
- root SSH access method
- confirmation that Ubuntu `24.04` is installed: yes
- confirmation whether the IONOS image includes Plesk or any other panel:
  checked directly, no active Plesk present

### DNS and nameserver information

Need:

- exact hostnames to move
- current authoritative DNS provider
- whether nameserver delegation itself is changing, or only record targets
- if nameservers are changing:
  - new nameserver hostnames
  - glue IPs, if relevant
- if only records are changing:
  - exact `A` and `AAAA` targets
- whether shared-zone `MX`, `TXT`, SPF, DKIM, DMARC, or unrelated records must
  remain untouched
- approved TTL-lowering window before cutover

Already clarified:

- `scriptoriumai.io`, `verticaltension.com`, and `www.verticaltension.com` are
  already in production on the old VPS
- `hypertwist.app` is not yet configured for production on the new VPS
- `vectorshell.com` is not yet configured for production on the new VPS

### Cutover scope

Need:

- confirm whether this VPS replacement is:
  - control-plane only
  - or control-plane plus app/API/static vhosts currently on the old host

### Development-access setup

Need:

- desired non-root Linux username for development
- whether that user should receive Docker-group access
- whether the repo should be cloned fresh on the new VPS or copied from the old
  server

## Execution sequence

### Phase A. Audit and freeze

1. Verify the old VPS is healthy enough to migrate.
2. Export and verify:
   - Forgejo Postgres dump
   - Woodpecker Postgres dump
   - Forgejo data tarball
   - Woodpecker data tarball
3. Snapshot current live config:
   - compose
   - NGINX vhosts
   - cron files
   - timers
   - certbot state
4. Lower TTLs before cutover if possible.

### Phase B. Bootstrap the new host

1. Patch and harden Ubuntu `24.04`.
2. Install baseline packages:
   - `openssh-server`
   - `rsync`
   - `curl`
   - `unzip`
   - `zip`
   - `make`
   - `gdb`
   - `build-essential`
   - `nginx`
   - `certbot`
   - `python3-certbot-nginx`
   - `docker.io`
   - `docker-compose-plugin`
   - `postgresql-client`
3. Remove any unwanted panel image components before stack deployment.

### Phase C. Restore the control-plane stack

1. Recreate `/srv/scriptorium-stack`.
2. Copy in `.env` and `docker-compose.yml`.
3. Restore data directories.
4. Start the stack before DNS cutover.
5. Restore Postgres data if needed.

### Phase D. Recreate backup and guard surfaces

Reinstall and verify:

- `/usr/local/bin/scriptorium-nightly-backup.sh`
- `/usr/local/bin/scriptorium-disk-guard.sh`
- `/usr/local/bin/scriptorium-forgejo-health-guard.sh`
- `/etc/cron.d/scriptorium-nightly-backup`
- `/etc/cron.d/scriptorium-ops-guards`
- `/etc/cron.d/scriptorium-maintenance`
- `/etc/systemd/journald.conf.d/90-scriptorium-limits.conf`
- `/etc/scriptorium/ops-alerts.env`

### Phase E. Recreate NGINX and TLS

1. Install intended NGINX vhosts.
2. Validate `nginx -t`.
3. After DNS points to the new VPS, reissue certbot certificates on the new
   host.

### Phase F. Pre-cutover validation

Validate locally on the new VPS:

- containers healthy
- Forgejo reachable
- Woodpecker reachable
- gRPC loopback binding present
- cron files present
- backup scripts callable
- NGINX config valid

### Phase G. DNS or nameserver cutover

1. Change only the intended records or nameserver delegation.
2. Wait for propagation.
3. Reissue TLS if needed after propagation.
4. Verify every moved hostname externally.

### Phase H. Acceptance checks

Require all of the following before deleting the old VPS:

- Forgejo login works
- SSH clone/fetch works
- Woodpecker UI loads
- webhook-triggered pipeline activity works
- at least one real backup run completes on the new host
- Rider remote development connects
- Visual Studio remote Linux development connects

## Current source-versus-production parity finding

This is the key result for migration safety.

### Source-control parity

Confirmed:

- local `HyperTwist` app repo `HEAD` and remote `origin/main` match at:
  - `0ea26569902f181ebeb7c9db2c14c2c0b59ae466`
- local `ScriptoriumAI` repo `HEAD` and remote `origin/main` match at:
  - `118a85c0fee3fff34c071dd78d7cfe3eacc8ba95`
- local `VerticalTension` repo `HEAD` and remote `origin/main` match at:
  - `91bee21a7101ef08d3e91db9040f8a3ec8d3dd27`

### Working-tree cleanliness nuance

Observed locally:

- `ScriptoriumAI` working tree is dirty
- `VerticalTension` working tree is effectively clean except for an untracked
  workspace file

Operational consequence:

- if rebuilding from local source, use a clean clone or a clean detached export,
  not the current dirty `ScriptoriumAI` working tree

### Production artifact parity

The three important truths are:

- source-control heads are safe
- `VerticalTension` production is now reproducible from a fresh `HEAD` build
- `ScriptoriumAI` production is **not** provably reconstructible from the
  currently tracked clean source alone

#### `ScriptoriumAI`

Observed:

- latest repo `HEAD` is:
  - `118a85c0fee3fff34c071dd78d7cfe3eacc8ba95`
  - commit date: `2026-05-08 18:05:14 +0200`
- latest tracked frontend artifact update in git is older:
  - `012fc396ea6eba826f27f4a0f0c9cc17a3354af8`
  - commit date: `2026-04-23 14:56:12 +0200`
- production `scriptoriumai.io` currently serves:
  - `/assets/index-DKzZ39HF.js`
  - `/assets/index-D8zU9W-S.css`
- those live assets report:
  - `Last-Modified: Fri, 08 May 2026 16:20:04 GMT`
  - which is `2026-05-08 18:20:04 +0200`
- current tracked/local
  `C:\ScriptoriumAI\scriptoriumai-ui\dist\index.html` references:
  - `/assets/index-B2KwrMUv.js`
  - `/assets/index-D8zU9W-S.css`
- the production JS hash `DKzZ39HF` was not found in tracked
  `scriptoriumai-ui/dist/index.html` history during local inspection

Meaning:

- the latest committed source-control state is the May 8 repo `HEAD`
- the latest known deployed public artifact state is the May 8 production web
  root on the old VPS
- those are related in time, but not provably byte-identical
- the exact live `ScriptoriumAI` public surface should therefore be preserved
  from the old VPS as deployed content, not re-derived from git alone during
  the first cutover

Safety rule for `ScriptoriumAI`:

- preserve both:
  - repo `HEAD`
  - old VPS deployed state under `/var/www/scriptoriumai` and related app roots

#### `VerticalTension`

Observed:

- production `verticaltension.com` currently serves:
  - `/assets/index-CgVsNXoc.js`
  - `/assets/index-ismjD07_.css`
- a fresh detached build from repo `HEAD`
  `91bee21a7101ef08d3e91db9040f8a3ec8d3dd27` reproduced:
  - `index-CgVsNXoc.js`
  - `index-ismjD07_.css`

Meaning:

- `VerticalTension` production is aligned with a fresh `HEAD` build
- preserving the live deployed web root is still prudent for rollback, but the
  current public site is not showing the same ambiguity seen in `ScriptoriumAI`

Cross-product safety rule:

- for zero-surprise cutover, archive and copy the live deployed content from
  the old VPS for all public surfaces
- after the new VPS is stable, intentional rebuild/redeploy can then replace
  those copied artifacts where desired

## Confirmed operator decisions

As of the current migration pass, these decisions are now treated as confirmed:

- this is a **full-host replacement**, not only a control-plane-only move
- all current production domains should end up on the new VPS
- DNS stays hosted at IONOS
- nameserver delegation does **not** need to change for remote development
- remote development only depends on SSH reachability, Linux user/tooling
  setup, and the host being stable

Additional cutover rule:

- `www.verticaltension.com` must receive explicit NGINX vhost coverage and a
  certificate that matches that host on the new VPS
- this is necessary because live inspection of the old host currently shows a
  certificate-name mismatch on `https://www.verticaltension.com`

TLS continuity rule:

- for the currently covered ScriptoriumAI and VerticalTension hostnames, the
  existing VPS-side Let's Encrypt material can be copied to the new VPS as an
  **initial continuity bridge**
- this does **not** require changing IONOS nameserver delegation or relying on
  the IONOS certificate UI
- browsers only care about the certificate actually presented by NGINX on the
  VPS they connect to
- `www.verticaltension.com` remains the exception because the currently observed
  old-host certificate inventory does **not** cover that name, so leaving it
  strictly "as-is" would preserve the mismatch instead of fixing it

## Authorship preservation posture

Authoritative source:

- local Windows folder:
  `C:\Users\Anthracite Ace\Desktop\Authorship`

Current measured local size:

- total: `17.70 GiB`
- `.zip` files: `11.78 GiB` across `459` files
- non-zip files: `5.93 GiB`

Recommended migration posture:

- keep the full local folder authority before any destructive cleanup
- include `.zip` backup history in the ongoing mirror lane on the new VPS
- wire the scheduled Authorship mirror job so both the working files and the
  backup zip history continue syncing automatically

Reason:

- the new VPS storage envelope now makes the larger continuous sync acceptable
- the operator explicitly wants the backup zip history kept live on the new VPS,
  not only retained as a one-shot cold archive
- that increases sync volume, but it removes a second archive lane and keeps the
  remote copy operationally complete

## Storage posture for the current local corpus

Measured local sizes during this pass:

- `C:\visual_studio_solutions\multi_project`: `0.74 GiB`
- `C:\VectorShell`: `28.22 GiB`
- `C:\ScriptoriumAI`: `15.86 GiB`
- `C:\HyperTwist`: `14.69 GiB`
- `C:\NyxOS`: `4.30 GiB`
- `C:\Workspaces`: `80.74 GiB`
- `C:\Users\Anthracite Ace\Desktop\Authorship`: `17.70 GiB`
- `C:\VerticalTension`: `0.11 GiB`

Important nuance:

- the current observed `C:\VectorShell` footprint is materially below the
  operator's rough `~100 GiB` estimate
- inside `C:\VectorShell`, the largest current slices are:
  - `mirrors`: `17.54 GiB`
  - `UnrealVectorShell`: `6.51 GiB`
  - `sentrux`: `3.11 GiB`
- if other large VectorShell-related stores exist outside `C:\VectorShell`,
  they must be added separately to any final storage budget

Operational conclusion:

- the current observed local corpus comfortably fits within the new VPS
  capacity envelope
- the current measured remote-development seed set
  (`multi_project`, `VectorShell`, `HyperTwist`, `ScriptoriumAI`,
  `VerticalTension`, `NyxOS`, `Workspaces`) totals about `144.66 GiB`
- adding the full local `Authorship` authority raises that current measured
  working payload to about `162.36 GiB`
- long-term repo growth is still real, so production retention policy should
  stay intentional rather than assuming the VPS is an infinite mirror warehouse

## Expanded remote-development seed scope

Confirmed additional remote-development payloads to seed onto the new VPS:

- `C:\visual_studio_solutions\multi_project`
- `C:\VectorShell`
- `C:\HyperTwist`
- `C:\ScriptoriumAI`
- `C:\VerticalTension`
- `C:\NyxOS`
- `C:\Workspaces`

Recommended target root on the new VPS:

- `/srv/dev`

Operational note:

- these are best treated as a seeded remote-development workspace surface, not
  as public web roots
- the new helper script
  `C:\ScriptoriumAI\ops\ssh\sync-vps-remote-dev-workspaces.ps1`
  now codifies this seed set

## Rider remote-development setup

Official source basis:

- JetBrains Rider remote-development docs

Key requirements:

- SSH to a Linux host
- SFTP enabled
- recent AMD64 Linux
- JetBrains recommends `2+` cores, `4+ GB` RAM, `5+ GB` disk
- outbound access to JetBrains download URLs, or manual installer upload

Recommended first-project setup (example: HyperTwist):

- create a non-root development user
- clone the first remote-development project to a stable path such as `/home/<user>/src/HyperTwist`
- use Rider Gateway from the local machine to connect over SSH
- keep enough space available under the backend cache path, or choose a custom
  install path

## Visual Studio Community 2022 remote Linux setup

Official source basis:

- Microsoft Learn Visual Studio 2022 Linux development docs

Important distinction:

- this is not the same thin-client remote IDE model as Rider Gateway
- for C++/CMake, Visual Studio uses SSH plus `rsync` and builds remotely
- for .NET on Linux, Visual Studio supports SSH attach-debug scenarios

Linux-side prerequisites for the C++ path include:

- `openssh-server`
- `build-essential`
- `gdb`
- `rsync`
- `make`
- `zip`
- `cmake`

Recommended use:

- Rider Gateway for full remote IDE sessions
- Visual Studio Community 2022 for Linux C++/CMake remote build/debug and, when
  needed, .NET SSH attach-debug

## Recommended next action

Do not change DNS yet.

Next action is to complete the pre-cutover archive and reconstruction pass:

1. restore working SSH access to `212.227.13.220`
2. archive the old VPS live surfaces and config
3. restore the full selected role set on the new VPS
4. validate NGINX/vhost/cert readiness there, including `www.verticaltension.com`
5. only then perform the DNS record switch at IONOS

Current blocker:

- the existing private key at
  `C:\ScriptoriumAI\ops\ssh\scriptorium_vps_ed25519`
  does **not** currently authenticate to the new VPS
- the locally stored generated password also failed when tested against the new
  VPS
- practical resolution is either:
  - provide the current root password for `212.227.13.220`
  - or install
    `C:\ScriptoriumAI\ops\ssh\scriptorium_vps_ed25519.pub`
    into `/root/.ssh/authorized_keys` on the new VPS

## DNS record recommendations based on current findings

### `hypertwist.app`

Current observed public DNS:

- `A @ -> 217.160.0.30`
- `AAAA @ -> 2001:8d8:100f:f000::200`

Recommended production cutover records for the new VPS:

- change `A @` to `212.227.13.220`
- remove the current `AAAA @` record for now
- add `CNAME www -> hypertwist.app`

Reason:

- the new VPS currently has no confirmed global IPv6
- keeping the placeholder IONOS `AAAA` would continue sending IPv6-capable
  clients to the wrong host

### `vectorshell.com`

Current observed public DNS:

- `A @ -> 217.160.0.41`
- `AAAA @ -> 2001:8d8:100f:f000::200`

Recommended production cutover records for the new VPS:

- change `A @` to `212.227.13.220`
- remove the current `AAAA @` record for now
- add `CNAME www -> vectorshell.com`

Keep unchanged:

- current mail records
- `_domainconnect`
- `_dmarc`
- DKIM-related CNAMEs
- `autodiscover`

### `scriptoriumai.io`

Current observed public DNS already points to the old production VPS:

- `A @ -> 212.227.22.66`
- `A api -> 212.227.22.66`
- `A app -> 212.227.22.66`
- `A ci -> 212.227.22.66`
- `A git -> 212.227.22.66`
- `CNAME www -> scriptoriumai.io`

For the full-host cutover window, change all of these `A` records to:

- `212.227.13.220`

Keep unchanged:

- `CNAME www -> scriptoriumai.io`
- current mail records
- `_domainconnect`
- `_dmarc`
- DKIM-related CNAMEs
- CAA records

### `verticaltension.com`

Current observed public DNS already points to the old production VPS:

- `A @ -> 212.227.22.66`
- `A www -> 212.227.22.66`
- `A api -> 212.227.22.66`
- `A app -> 212.227.22.66`
- `A ci -> 212.227.22.66`
- `A git -> 212.227.22.66`

For the full-host cutover window, change all of these `A` records to:

- `212.227.13.220`

Keep unchanged:

- the existing non-`A` `www` records
- current mail records
- `_domainconnect`
- `_dmarc`
- DKIM-related CNAMEs

Important nuance:

- do **not** convert `www.verticaltension.com` to `CNAME` in this pass
- simply update the existing `A www` target once the new VPS is ready
- the new VPS must explicitly serve `www.verticaltension.com` with matching TLS

## `www` guidance

Short answer:

- `www` is not technically required
- `www` can still be useful
- Google does not need `www` specifically
- the real rule is to choose one canonical host and redirect the other

Practical recommendation:

- for new public sites such as `hypertwist.app` and `vectorshell.com`, add
  `www` and redirect it to the primary canonical host
- keep apex as canonical if you want consistency with the current
  `scriptoriumai.io` setup

Why `www` can be useful:

- easier aliasing through `CNAME`
- cleaner future CDN or proxy routing options
- people still type it out of habit

Why it is not mandatory:

- search engines treat apex and `www` equally if canonicalization is correct
- there is no inherent SEO advantage just from having `www`

SSL clarification:

- the old idea that one certificate class is only for `www` and another is only
  for apex is not the general rule here
- with modern certificates, including Let's Encrypt, one certificate can cover
  both apex and `www` as SAN entries if you request both names
- the real limitation is DNS and validation wiring, not a universal
  `www`-versus-apex certificate class rule
