Consolidating TechsFree Platform from 2 Servers to 1
Running a 9-node home cluster means you'll eventually lose track of which service is running where. Today I did a full audit and migrated everything from the infra server to the web server in one shot.
Background: Chaos Spread Across Two Servers
Our rough infra layout:
- infra server (192.168.x.x): T440, message bus, dev environments, and "temporary" service storage
- web server (192.168.x.x): CentOS with aaPanel, Nginx + SSL, production web server
The problem: the infra server became a permanent "temporary service storage." After auditing its ports, this is what I found:
| Port | Service | Status |
|---|---|---|
| :3001 | ERP API Backend (ex-online-shop) | PM2, restarted 127 times |
| :5174 | Shop Frontend (vite preview) | PM2 managed |
| :5175 | ERP Frontend (vite dev) | Unmanaged process |
| :3400–3409 | 10 linebot experiment processes | Unmanaged |
| :8080–8081 | likeshop Docker | Running on both servers |
| :8530 | techsfree-homepage python http.server | Left running "temporarily" |
python3 -m http.server 8530 acting as a production server is... not ideal.
Step 1: Clean Up the Infra Server
First, killed all unnecessary processes:
# Kill 10 linebot experiment processes
ps aux | grep linebot | awk '{print $2}' | xargs kill
# Stop likeshop Docker containers on both servers
docker stop nginx node php mysql redis
docker stop php redis # web server side
# Stop python http.server
pkill -f "http.server 8530"
PM2 with restart: 127 is funny in retrospect, but it's proof of how long it was left unattended.
Step 2: Full Migration to Web Server
The TechsFree platform consists of:
- Admin (ERP Dashboard): React SPA
- Shop-PC: Nuxt3 SSR
- Shop-H5: Vue3 SPA (mobile)
- Backend: Node.js (Express + Prisma)
- DB: MySQL (Docker managed)
All scattered between infra and web. Today I consolidated everything to the web server.
DB Migration
The first challenge was MySQL. The infra DB was buried inside a Docker bind mount from likeshop:
/path/to/likeshop/docker/data/mysql5.7.29/lib/techsfree_platform/
Direct copy doesn't work due to Docker's internal format. Used a temporary container for a clean dump:
# Spin up a temporary MySQL container to dump
docker run --name tmp-mysql-dump \
-v /path/to/mysql:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=<DB_PASSWORD> \
-p 13307:3306 mysql:5.7 -d
mysqldump -h 127.0.0.1 -P 13307 -u root -p techsfree_platform > dump.sql
# Transfer and import to web server
scp dump.sql user@web-server:/tmp/
ssh user@web-server "mysql -h 127.0.0.1 -P 3308 -u root -p techsfree_platform < /tmp/dump.sql"
37 tables, import successful.
Update Backend DB Connection
Copied the .env and updated only the DB host:
DB_HOST=127.0.0.1
DB_PORT=3308
DB_NAME=techsfree_platform
DB_USER=root
DB_PASSWORD=<DB_PASSWORD>
Fired up with PM2:
✅ DB connection successful
✅ DB tables synced
🚀 TechsFree Platform Backend running: http://0.0.0.0:3210
Frontend Build & Nginx Config
The Admin SPA had the old server IP hardcoded in VITE_API_URL. Fixed and rebuilt, then synced with rsync and updated Nginx:
# erp.techsfree.com.conf
location /api/ {
proxy_pass http://127.0.0.1:3210/api/;
}
location / {
root /www/wwwroot/erp.techsfree.com;
try_files $uri $uri/ /index.html;
}
Admin Password Reset
After migration, the admin password was unknown (had the bcrypt hash but not the original). Used bcryptjs to generate a new hash and updated directly in the DB:
node -e "
const bcrypt = require('./node_modules/bcryptjs');
console.log(bcrypt.hashSync('new-password', 10));
"
# → $2a$10$...
mysql -e "UPDATE tf_users SET password='\$2a\$10\$...' WHERE email='admin@techsfree.com'"
Verified login via API — mission complete.
Step 3: Deploy Astro Blog
As a bonus, deployed the TechsFree tech blog (built with Astro). Hit two notable issues:
Pitfall 1: Can't npm install on NFS
The Astro project lives on /mnt/shared/ (NFS mount), which causes issues with symlinks and certain file ops. npm install failed midway.
Fix: copy to local /tmp/ first:
cp -r /mnt/shared/projects/04_techsfree-blog/ /tmp/blog-build/
cd /tmp/blog-build && npm install && npm run build
Pitfall 2: Astro base Config and Subpath Serving
Setting base: '/blog' in Astro config caused internal links to break — subpages returned 404s everywhere.
Conclusion: skip the base config, serve on a dedicated port, and use Nginx proxy_pass:
location /blog/ {
proxy_pass http://127.0.0.1:3300/;
}
Pitfall 3: curl Test Giving Wrong Results
curl http://127.0.0.1/blog/ returned 404 and I panicked — but it was because Host: 127.0.0.1 was being picked up by the default server instead of the right vhost.
# Wrong: goes to default server with Host: 127.0.0.1
curl http://127.0.0.1/blog/
# Right: explicitly set the Host header
curl -H "Host: your-domain.com" http://127.0.0.1/blog/
Spent 30 minutes wondering if it was a Nginx config bug or an Astro build issue. It wasn't either.
Lessons Learned
-
Set expiry dates for temporary setups — never let
python3 -m http.serverbecome production -
Don't
npm installon NFS — copy to/tmp/first -
Mind the Host header in curl tests — in multi-vhost Nginx setups, always use
-H "Host: xxx" - Use temporary containers for DB migration — safer than directly manipulating bind mounts
If you don't periodically audit your infra, you'll end up with zombie processes that have restarted 127 times.
Tags: infra, server-ops, nginx, mysql, astro, nodejs, self-hosted, devops
Top comments (0)