aslan-stg2prod.sh 1.0: Install and Use¶
Copies a WordPress site (database + files) from one site stack on dmz-web
to another, e.g. staging → production. Works for every pair built by
add-wordpress-site.sh: you give the two site names, the script reads
everything else from the sites themselves.
Runs as websvcmaster. Changes nothing outside the two sites' containers
and ~websvcmaster/backups/.
1. Install¶
As root (file uploaded to /root):
install -o websvcmaster -g websvcmaster -m 0755 /root/aslan-stg2prod.sh ~websvcmaster/deploy/aslan-stg2prod.sh
As websvcmaster:
cd ~/deploy
./aslan-stg2prod.sh --version # aslan-stg2prod.sh 1.0
./aslan-stg2prod.sh --help
Commit it to the dmz-web-deploy repository (root):
deploy-commit -m "aslan-stg2prod.sh 1.0: copy a WordPress site between site stacks"
2. What it needs¶
| Item | Expected |
|---|---|
| Containers |
site-<name> and db-<name> of both sites running |
| Web root | a volume mounted at /var/www/html in site-<name> (found automatically) |
| WP-CLI |
wp-cli.phar in the source's web root (the target may not have one yet; the script lends it the source's copy) |
| Database | MariaDB image with mariadb / mariadb-dump (mariadb:11) |
| Table prefix | the same on both sites (otherwise it stops) |
| NPM | a proxy host + certificate for the target's domain, for the final check |
It does not create proxy hosts, certificates or DNS records, and
does not touch nginx-proxy-manager.container or any Quadlet file.
3. Commands¶
aslan-stg2prod.sh <source> <target> --dry-run # read, check, show plan; changes nothing
aslan-stg2prod.sh <source> <target> # asks you to type the target name
aslan-stg2prod.sh <source> <target> -y # no question (scripts only)
aslan-stg2prod.sh <source> <target> --noindex # target: "discourage search engines" on
aslan-stg2prod.sh <source> <target> --keep-noindex # target gets the source's setting
aslan-stg2prod.sh <source> <target> --target-url=https://www.example.com
# address the copy gets (default: target's siteurl)
aslan-stg2prod.sh --restore <target> <backup-dir> # put the target back
Site names are the ones in the unit names: site-staging → staging,
site-mdsdefense → mdsdefense.
What a copy does:
- Reads both sites: URLs (
siteurl,home), database name, user, host,
table prefix, WordPress version, active theme, search engine setting,
published content counts, file sizes. Shows them and the plan. - Backs up the target to
~/backups/stg2prod/<target>-<timestamp>/
(db.sql,files.tar,INFO,SHA256SUMS; mode 700). - Puts the target in maintenance mode.
- Replaces the target's database with the source's.
- Replaces the target's files with the source's; the target's
wp-config.phpstays (it holds the target's DB host and credentials). - Rewrites the source URL to the target URL in the whole database
(serialized data and JSON-escaped\/\/included), sets
siteurl/home, allows search engines (--noindex/--keep-noindex
change that),
flushes transients and cache, and rebuilds the rewrite rules with
plugins and theme loaded (see Troubleshooting). Reports how many times the
source host is still in the database (e.g. e-mail addresses, which are
left alone on purpose). - Ends maintenance mode and checks the target through NPM: home page
HTTP 200 with no source host on it, and every Polylang language root
(/tr/,/ar/...) HTTP 200 without a redirect.
If a step fails, the target stays in maintenance mode and the script
prints the exact --restore command. Every run is logged to
~/.local/state/aslan-stg2prod.log.
Database passwords are never on a command line: they go to the DB
container on stdin.
4. MDS go-live: staging → www.mdsdefense.com ¶
As websvcmaster in ~/deploy:
-
Theme:
./mds-theme.sh statusshows what staging runs. Nothing to
install on production first: the copy brings the theme files along. -
Dry run. The bare domain
mdsdefense.comstill points to the old
site, so production must run as www: give it explicitly, whatever
the fresh install was set up with:./aslan-stg2prod.sh staging mdsdefense --target-url=https://www.mdsdefense.com --dry-runCheck in the output: source
https://staging.mdsdefense.com, target
URLhttps://www.mdsdefense.com, same table prefix, target database is
the production one (db-mdsdefense), counts look right. -
Copy:
./aslan-stg2prod.sh staging mdsdefense --target-url=https://www.mdsdefense.comType
mdsdefensewhen asked. -
Check (browser, private window):
-
https://www.mdsdefense.com/,/tr/,/ar/: home, menu, footer,
language switcher - Projects, Press Room, Contact (form sends), one Standard Page per
language - wp-admin login on www (same users as staging)
- Settings > Reading: "Discourage search engines" off
- Media → Add New shows the 64 MB limit (
.htaccesscame along) -
./mds-theme.sh status: prod shows the same theme version as staging
-
-
From now on: content is edited on production only. Staging is
for theme tests.
5. Later: refresh staging from production¶
The same script works the other way; keep staging out of search engines:
./aslan-stg2prod.sh mdsdefense staging --dry-run --noindex
./aslan-stg2prod.sh mdsdefense staging --noindex
--noindex keeps staging out of search engines whatever production says.
Staging's own wp-config.php stays, so it keeps using its own database.
6. Restore¶
ls -1t ~/backups/stg2prod/ # newest first
./aslan-stg2prod.sh --restore mdsdefense ~/backups/stg2prod/mdsdefense-YYYYMMDD-HHMMSS
It checks the checksums and that the backup belongs to that site, then
puts back database and files (including that site's own wp-config.php).
7. Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
/tr/, /ar/ (or /projects/...) redirect to an unrelated page, e.g. /tr/ → /projects/trauma-simulators/, header x-redirect-by: WordPress
|
WordPress rewrite rules were built while Polylang (or the theme) was not loaded, so the language prefixes are missing; WordPress then "guesses" a post whose slug starts with tr
|
wp-admin > Settings > Permalinks > Save Changes (change nothing), or mds-theme.sh wp <site> rewrite flush. Never flush with --skip-plugins/--skip-themes. The script does this itself since 1.0 and warns if a language root does not answer 200. |
The browser shows mdsdefense.com without www
|
Chrome/Edge hide https:// and www. in the address bar |
Click into the address bar, or right-click > "Always show full URLs". Check the real answer with curl.exe -sv -o NUL https://www.mdsdefense.com/ from a PC: Server: openresty and no location: = dmz-web, no redirect. |
curl https://www.<domain>/ hangs on dmz-web
|
no hairpin NAT: the host cannot reach its own public IP | Test locally through NPM: curl -skI --resolve www.<domain>:444:127.0.0.1 https://www.<domain>:444/
|
| Links point to the bare domain (old site) | the target was installed as https://<domain>
|
Copy with --target-url=https://www.<domain>; check mds-theme.sh wp <site> option get home / siteurl
|
| Final check: HTTP 000 or 502 | NPM has no proxy host / certificate for the target domain, or the site container is down | NPM admin (:8181): proxy host + certificate; podman ps
|
8. Housekeeping¶
Backups are not deleted automatically. Each holds a full database dump
and the whole web root (uploads included). Remove old ones by hand when
the copy has been checked:
du -sh ~/backups/stg2prod/*
rm -r ~/backups/stg2prod/<target>-<timestamp>
Version history¶
| Version | Date | Notes |
|---|---|---|
| 1.0 | 2026-11-05 | First release: copy with backup, maintenance mode, URL rewrite, rewrite rules rebuilt with plugins + theme loaded, NPM check incl. language roots; --dry-run, --noindex, --keep-noindex, --target-url, --restore. |
Updated by Kerem ERSOY about 23 hours ago · 1 revisions