migration runbook · draft

stm-next: conwizamit VPS → Celnet WHM

Django replacement for the journals.stmjournals.com WordPress site moves from the temporary VPS onto the Celnet fleet host. Four reversible phases; cutover is 5–15 minutes.

📅 2026-10-02 👤 Author: Wisp 📍 Source: 187.127.143.55 · Target: 67.225.224.49 ⚡ Status: not started

At a glance

key numbers
4
Phases
5–15 min
Downtime
6
Open Questions
7 days
Rollback window

Data flow

source → destination
Source
conwizamit VPS
187.127.143.55 · temporary
gunicorn:8099 · Caddy proxy
Postgres on-host
→migrate
Destination
Celnet WHM
67.225.224.49 · fleet home
stmnext system user · Apache ProxyPass
Postgres 15 (fresh)

Phases

four reversible stages
Phase 1
Prep Celnet
Install PG, user, deploy, loopback test
Phase 2
Staging + data sync
Dump restore, PDFs rsync, test URL
Phase 3
Cutover
Delta sync + CF DNS flip (5-15 min)
Phase 4
Monitor + decommission
7-day watch, then shut VPS service

Open decisions

answered before Phase 3
⚪
1 · Final public URL Stay at stmjournals.celnet.in, or replace WordPress at journals.stmjournals.com?
⚪
2 · Downtime window 5-minute target (needs careful delta tooling) or 15-minute comfort?
⚪
3 · Cutover timing Which midnight IST? Quiet traffic, Boss asleep → runbook mode.
⚪
4 · cPanel vhost owner Which existing cPanel user hosts the ProxyPass? Or add a dedicated one?
⚪
5 · articles.stmjournals.com Moves together? Same app serves both today.
⚪
6 · WordPress retirement If Q1 = replace WP, when does the WP site come down? 10,100-article 301 map needed.
Phase 1

Prep Celnet — zero downtime

0 / 8 ▸

1.1 Install PostgreSQL 15

# ssh -i ~/.ssh/journals_server -p 522 [email protected]
dnf module reset postgresql -y
dnf module enable postgresql:15 -y
dnf install -y postgresql-server postgresql-contrib libpq-devel
/usr/bin/postgresql-setup --initdb
systemctl enable --now postgresql
systemctl status postgresql --no-pager

1.2 Create role + database

sudo -u postgres psql <<SQL
CREATE ROLE stmnext LOGIN PASSWORD '<generated-strong-password>';
CREATE DATABASE stmnext OWNER stmnext ENCODING 'UTF8'
  LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;
GRANT ALL PRIVILEGES ON DATABASE stmnext TO stmnext;
SQL

1.3 Create system user

useradd -r -m -d /home/stmnext -s /sbin/nologin stmnext
chmod 750 /home/stmnext

1.4 Clone repo + venv

sudo -u stmnext git clone <repo-url> /home/stmnext/app
cd /home/stmnext/app
sudo -u stmnext git checkout <commit on VPS — currently fe4862e>
sudo -u stmnext /usr/bin/python3.12 -m venv .venv
sudo -u stmnext .venv/bin/pip install --upgrade pip
sudo -u stmnext .venv/bin/pip install -r requirements.txt

1.5 Write env files (secrets rotated)

# /home/stmnext/app/.env.web  (mode 600, owner stmnext)
DJANGO_SECRET_KEY=<fresh — do not reuse VPS>
DJANGO_DEBUG=0
DJANGO_ALLOWED_HOSTS=stmjournals.celnet.in,articles.stmjournals.com,articles.stmjournals.in,127.0.0.1,localhost
DJANGO_BEHIND_PROXY=1
EMAIL_HOST=... EMAIL_PORT=... EMAIL_USE_TLS=...
EMAIL_HOST_USER=... EMAIL_HOST_PASSWORD=...
DEFAULT_FROM_EMAIL=... ORDER_NOTIFY_TO=...

# /home/stmnext/app/.env.db  (mode 600)
DJANGO_DB_NAME=stmnext DJANGO_DB_USER=stmnext
DJANGO_DB_PASSWORD=<from 1.2>
DJANGO_DB_HOST=127.0.0.1 DJANGO_DB_PORT=5432

1.6 Migrate + collectstatic (empty DB)

sudo -u stmnext bash -c '
cd /home/stmnext/app
set -a; . .env.web; . .env.db; set +a
.venv/bin/python manage.py migrate --noinput
.venv/bin/python manage.py collectstatic --noinput
'

1.7 systemd service

# /etc/systemd/system/stmnext.service
[Unit]
Description=STM Journals (stm-next)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
User=stmnext
Group=stmnext
WorkingDirectory=/home/stmnext/app
EnvironmentFile=/home/stmnext/app/.env.web
EnvironmentFile=/home/stmnext/app/.env.db
ExecStart=/home/stmnext/app/.venv/bin/gunicorn stmnext.wsgi:application \
    --bind 127.0.0.1:8099 \
    --workers 3 --timeout 120 \
    --access-logfile - --error-logfile -
Restart=on-failure
RestartSec=5
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/stmnext

[Install]
WantedBy=multi-user.target
ss -tlnp | grep :8099  # must be empty before enabling
systemctl daemon-reload
systemctl enable --now stmnext.service
systemctl status stmnext.service --no-pager

1.8 Loopback smoke test

curl -sI http://127.0.0.1:8099/ | head
curl -s  http://127.0.0.1:8099/ | grep -c 'STM Journals'
curl -sI http://127.0.0.1:8099/admin/login/
Phase 2

Staging URL + data sync — zero downtime

0 / 4▸

2.1 Staging subdomain in WHM

Add A record stmnext-staging.celnet.in → 67.225.224.49 (grey-cloud so we hit origin directly).

# /etc/apache2/conf.d/userdata/std/2_4/<cpaneluser>/<site>/stmnext-staging.conf
<IfModule proxy_module>
    ProxyPass        / http://127.0.0.1:8099/
    ProxyPassReverse / http://127.0.0.1:8099/
    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto https
</IfModule>
/usr/local/cpanel/scripts/rebuildhttpdconf
systemctl reload httpd

2.2 Pull Postgres snapshot from VPS

# On VPS:
sudo -u claw bash -c '
cd /home/claw/claudeclaw-home/stm-next
set -a; . .env.web; . .env.db; set +a
PGPASSWORD=$DJANGO_DB_PASSWORD pg_dump -h 127.0.0.1 -U $DJANGO_DB_USER \
  $DJANGO_DB_NAME -F c -Z 9 -f /tmp/stmnext.dump
ls -lh /tmp/stmnext.dump
'
# Copy to Celnet, then restore:
scp -i ~/.ssh/journals_server -P 522 \
  [email protected]:/tmp/stmnext.dump /tmp/stmnext.dump
sudo -u stmnext bash -c '
cd /home/stmnext/app
set -a; . .env.db; set +a
PGPASSWORD=$DJANGO_DB_PASSWORD pg_restore \
  -h 127.0.0.1 -U stmnext -d stmnext --clean --if-exists /tmp/stmnext.dump
'
systemctl restart stmnext.service

2.3 Rsync fulltext PDFs

rsync -av --info=progress2 \
    -e 'ssh -i ~/.ssh/journals_server -p 522' \
    /home/claw/claudeclaw-home/stm-next/fulltext/ \
    [email protected]:/home/stmnext/fulltext/
# On Celnet:
chown -R stmnext:stmnext /home/stmnext/fulltext

2.4 12-URL walk-through

Hit each on https://stmnext-staging.celnet.in/:

Phase 3

Cutover — 5-15 min downtime

0 / 6▸

3.1 Pre-cutover

3.2 VPS maintenance mode

# /etc/caddy/Caddyfile — swap stmjournals.celnet.in block:
stmjournals.celnet.in, articles.stmjournals.com {
    respond "The site is briefly down for a scheduled migration. Back in ~10 minutes." 503
}
# then: systemctl reload caddy

3.3 Final delta sync

# VPS: fresh dump now that writes are frozen
sudo -u claw pg_dump ... -f /tmp/stmnext-final.dump
scp .../stmnext-final.dump [email protected]:/tmp/

# Celnet:
systemctl stop stmnext.service
sudo -u stmnext pg_restore ... --clean --if-exists /tmp/stmnext-final.dump
rsync -av --delete /home/claw/claudeclaw-home/stm-next/fulltext/ \
    -e 'ssh -i ~/.ssh/journals_server -p 522' \
    [email protected]:/home/stmnext/fulltext/
systemctl start stmnext.service

3.4 Production Apache vhost on Celnet

# /etc/apache2/conf.d/userdata/std/2_4/<cpaneluser>/<site>/stmnext-prod.conf
<IfModule proxy_module>
    ProxyPass        / http://127.0.0.1:8099/
    ProxyPassReverse / http://127.0.0.1:8099/
    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto https
</IfModule>
# then:
/usr/local/cpanel/scripts/rebuildhttpdconf
systemctl reload httpd
# AutoSSL pre-stage:
/usr/local/cpanel/bin/autossl_check --user=<cpaneluser>

3.5 Cloudflare DNS flip

In Cloudflare dashboard — change proxied A records:

3.6 Live verification

Phase 4

Monitor + decommission

0 / 3▸

Days 0–7

Day 7+

# On VPS, after Boss OKs:
systemctl stop stmnext.service
systemctl disable stmnext.service
sudo -u claw pg_dump stmnext | gzip > /home/claw/claw-backups/stmnext-vps-final-$(date +%F).sql.gz
sudo -u claw tar czf /home/claw/claw-backups/stmnext-vps-fulltext-$(date +%F).tgz \
    -C /home/claw/claudeclaw-home/stm-next fulltext
# then: remove the stmjournals.celnet.in Caddy block

Rollback

reversible up to Day 7
If Phase 1 fails

Nothing to roll back. VPS untouched. Fix locally and retry.

If Phase 2 fails

systemctl stop stmnext.service on Celnet. Staging DNS stays, no customer impact.

If Phase 3 fails

Cloudflare: flip origin back to 187.127.143.55. VPS Caddyfile: restore original reverse_proxy block. VPS Postgres & fulltext untouched — writes resume as if nothing happened.

Risks

ranked by likelihood × impact
RiskLikelihoodMitigation
psycopg2 build fails (missing libpq-devel)mediumPre-install libpq-devel. Fallback: psycopg[binary] wheel.
ACME challenge fails on first AutoSSL passmediumPre-stage with autossl_check --user=<cpaneluser>; cert lands in 5–10 min.
Fulltext hardcoded paths (/home/claw/...)mediumGrep repo pre-cutover; env-var any absolute paths found.
Port 8099 already in use on CelnetlowCheck ss -tlnp; shift to 8599 if conflict.
Admin PBKDF2 cost mismatch between Django versionslowPin requirements.txt; same Django version both sides.
CloudLinux LVE applies to system userlowuseradd -r → uid < 1000 → outside LVE. Verify lvectl list.
Imunify360 flags gunicorn / Postgres as unknownlowWhitelist stmnext user; verify no process-kill events.
cPanel vhost needs mod_proxy enabledlowWHM → EasyApache → confirm proxy, proxy_http modules.
Googlebot sees 503 during cutoverlowKeep cutover under 15 min; CF cache keeps most pages warm. Monitor Search Console.
CF caches old origin stucklowProxied record flip is instant; purge zone cache if any page is stuck.
Secrets accidentally reusedlowRotate DJANGO_SECRET_KEY on Celnet; keep same email/razorpay creds.

Related

memory + source doc
  • Full markdown: ~/Desktop/projects/stm-next-migration/MIGRATION-PLAN.md
  • Memory: conwizamit-vps (source), celnet-server-fleet (destination), journals-stmjournals-com-site (what this replaces)
  • Prior lessons: celnet-lve-reseller-cap, celnet-opcache-starvation