Netcup RS 4000 G12
- 12 dedicated cores (AMD EPYC 9645, 2024)
- 32 GB DDR5 ECC RAM
- 1 TB NVMe SSD
- 2.5 Gbit/s network
- €27.08/mo
This document outlines the migration plan for the Magic e-VERSE platform from the current local server (Intel i7-9700KF, 16 GB RAM) to a cloud-hosted setup using Netcup RS 4000 G12 as the primary server, Cloudflare R2 for product image storage, and Hetzner Storage Box for off-site backups.
| Component | Spec |
|---|---|
| CPU | Intel Core i7-9700KF @ 3.60 GHz (8 cores) |
| RAM | 16 GB DDR4 (18 GB in swap — critical) |
| System Disk | 228 GB SSD (/dev/sda2) — 77% full |
| Data Disk | 932 GB HDD (/dev/sdb1 → /mnt/data) — 64% full |
| NVMe | 1.8 TB (/dev/nvme0n1p1 → /mnt/nvme) — 52% full |
| NAS Backup | 3.5 TB + 2.7 TB (SMB mounts) |
| Docker Containers | 53 running |
| Docker Images | 697 GB (688 GB reclaimable) |
| Actual container RAM usage | ~2.2 GB |
| Total platform data | ~190 GB (excl. product images) |
| Product images | ~220 GB (/mnt/data/pim_data/) |
Netcup RS 4000 G12
Cloudflare R2
Hetzner Storage Box BX11
Total Monthly Cost
Best price/performance ratio across all EU providers. Comparison at time of evaluation (March 2026):
| Provider | Product | Cores | RAM | Storage | Network | Price/mo |
|---|---|---|---|---|---|---|
| Netcup | RS 4000 G12 | 12 | 32 GB DDR5 | 1 TB NVMe | 2.5 Gbit/s | €27.08 |
| Hetzner | CCX33 (Cloud) | 8 vCPU | 32 GB | 240 GB | 1 Gbit/s | €48.49 |
| Hetzner | AX42 (Dedicated) | 8 | 64 GB | 1 TB | 1 Gbit/s | €46.00 |
| Contabo | VDS M | 4 | 32 GB | 240 GB | 500 Mbit/s | €35.84 |
| OVHcloud | Advance-1 | 6 | 32 GB | NVMe | 1 Gbit/s | €110+ |
| Feature | Cloudflare R2 | Firebase Storage | Hetzner Object Storage |
|---|---|---|---|
| Free storage | 10 GB | 5 GB | 0 |
| Egress cost | Free | €0.12/GB | €1/TB |
| 250 GB cost | ~€3.50/mo | ~€8-11/mo | €6.49/mo |
| CDN | Global edge | Google CDN | No CDN |
| S3 compatible | Yes | No (proprietary SDK) | Yes |
| Service | Container(s) | Current RAM |
|---|---|---|
| 8 Commerce Tenants | backend + storefront + redis + meilisearch (×8) | ~1.6 GB |
| Magic PIM | backend + redis | ~130 MB |
| Spranz | backend + storefront + redis + meilisearch | ~60 MB |
| Master Magic | backend + redis | ~40 MB |
| PostgreSQL (shared) | magic_pim_postgres_dev | ~90 MB |
| MySQL | mysql | ~10 MB |
| n8n (×3) | magic_n8n, magic_n8n_2, magic_n8n_3 | ~120 MB |
| Image Services | rembg-service, preflight-service | ~15 MB |
| Sshwifty | magic_terminal_sshwifty | ~4 MB |
| Vaultwarden | vaultwarden | ~8 MB |
| Magic Portal | portal (Vite + React) | ~30 MB |
| Dashboard | dashboard API | ~20 MB |
| DevDocs | Starlight static site | ~10 MB |
| Total | ~48 containers | ~2.2 GB |
| Data | Size | Bucket |
|---|---|---|
/mnt/data/pim_data/spranz/ | Product images | magic-pim-images |
/mnt/data/pim_data/langenberg/ | Product images | magic-pim-images |
/mnt/data/pim_data/svg/ | SVG logos | magic-pim-images |
| Total | ~220 GB |
| Service | Reason |
|---|---|
| Omada Controller (TP-Link) | Needs LAN access for network hardware |
| Nextcloud + OnlyOffice | Optional — can move later or use cloud alternative |
| Item | Reason |
|---|---|
| Old Docker images (688 GB) | Reclaimable, rebuild fresh on new server |
| Windows SMB shares | Local storage, not platform-related |
| NAS backups | Replaced by Hetzner Storage Box |
ip (IPv4) + iv (IPv6) + 12M (12-month contract)5161nc17735292121 (1 month free)magic-pim-imagesimages.magiceverse.online)uXXXXXX), host (uXXXXXX.your-storagebox.de)# SSH into new Netcup server (IP from Netcup SCP panel)ssh root@<netcup-ip>
# Set hostnamehostnamectl set-hostname magic-cloud
# Update systemapt update && apt upgrade -yapt install -y curl git htop rsync rclone unzip
# Set timezonetimedatectl set-timezone Europe/Amsterdam# Install Docker Engine + Compose plugincurl -fsSL https://get.docker.com | shapt install -y docker-compose-plugin
# Verifydocker --versiondocker compose version
# Enable Docker on bootsystemctl enable dockerufw default deny incomingufw default allow outgoingufw allow 22/tcp # SSHufw allow 80/tcp # HTTPufw allow 443/tcp # HTTPSufw allow 8000/tcp # Coolify dashboardufw enable# Match current server structuremkdir -p /mnt/data/magic_omniverse/magic_commercemkdir -p /mnt/data/magic_pimmkdir -p /mnt/data/vaultwardenmkdir -p /opt/scriptsmkdir -p /opt/backups/databases# Generate SSH key on new serverssh-keygen -t ed25519 -C "magic-cloud"
# Copy to old server so we can pull datassh-copy-id adminmichiel1@<old-server-ip># On the CURRENT (old) serverrclone config
# Enter these settings:# name> r2# Storage> s3# provider> Cloudflare# env_auth> false# access_key_id> <your R2 access key># secret_access_key> <your R2 secret key># region> auto# endpoint> https://<account-id>.r2.cloudflarestorage.com# acl> (leave blank)# Edit advanced config?> n# Dry run first to verifyrclone sync /mnt/data/pim_data/ r2:magic-pim-images/ --dry-run --progress
# If looks good, do the real sync (run in tmux/screen!)tmux new -s r2syncrclone sync /mnt/data/pim_data/ r2:magic-pim-images/ --progress --transfers=8
# Verify file countrclone size r2:magic-pim-images/# In Cloudflare Dashboard:# 1. Go to R2 → magic-pim-images → Settings# 2. Enable "Public Access" via custom domain# 3. Add domain: images.magiceverse.online# 4. Cloudflare handles SSL automatically
# Test access:curl -I https://images.magiceverse.online/spranz/5325-00.001.jpgFiles to update (in magic_development, then sync to all tenants):
| File / Area | Current | New |
|---|---|---|
| PIM backend image serving | /mnt/data/pim_data/{supplier}/{file} | https://images.magiceverse.online/{supplier}/{file} |
Storefront <Image> components | Relative paths via backend proxy | Direct R2 URL |
| Spranz Designer SVG paths | /mnt/data/pim_data/svg/spranz/ | https://images.magiceverse.online/svg/spranz/ |
| APLT product thumbnails | Local file read | R2 URL |
| Docker volume mounts | /mnt/data/pim_data:/mnt/data/pim_data:ro | Remove (no longer needed) |
# PostgreSQL — all tenant databases + PIMdocker exec magic_pim_postgres_dev pg_dumpall -U postgres > /tmp/postgres_full_$(date +%Y%m%d).sql
# Check sizels -lh /tmp/postgres_full_*.sql
# MySQL — portal, workflow datadocker exec mysql mysqldump -uroot -p<your-db-password> --all-databases > /tmp/mysql_full_$(date +%Y%m%d).sqltmux new -s migration
# Replace <netcup-ip> with actual IP throughout
# 1. Commerce tenants (the big one)rsync -avz --progress \ --exclude='node_modules' \ --exclude='.next' \ --exclude='dist' \ /mnt/data/magic_omniverse/ root@<netcup-ip>:/mnt/data/magic_omniverse/
# 2. PIM (code only — images are in R2 now)rsync -avz --progress \ --exclude='node_modules' \ --exclude='.next' \ --exclude='uploads' \ /mnt/data/magic_pim/ root@<netcup-ip>:/mnt/data/magic_pim/
# 3. Vaultwarden datarsync -avz --progress /mnt/data/vaultwarden/ root@<netcup-ip>:/mnt/data/vaultwarden/
# 4. rembg + preflight servicesrsync -avz --progress /home/adminwayne/rembg-service/ root@<netcup-ip>:/opt/services/rembg-service/rsync -avz --progress /home/adminwayne/preflight-service/ root@<netcup-ip>:/opt/services/preflight-service/
# 5. Database dumpsscp /tmp/postgres_full_*.sql root@<netcup-ip>:/tmp/scp /tmp/mysql_full_*.sql root@<netcup-ip>:/tmp/
# Note: n8n dirs (magic_n8n, magic_n8n_2, magic_n8n_3) are included# in step 1 since they live under /mnt/data/magic_omniverse/Right before you switch DNS, do a final delta sync to catch any changes:
# Quick delta sync — only transfers changed filesrsync -avz --progress --exclude='node_modules' --exclude='.next' --exclude='dist' \ /mnt/data/magic_omniverse/ root@<netcup-ip>:/mnt/data/magic_omniverse/
# Fresh database dumpdocker exec magic_pim_postgres_dev pg_dumpall -U postgres > /tmp/postgres_final_$(date +%Y%m%d_%H%M).sqldocker exec mysql mysqldump -uroot -p<your-db-password> --all-databases > /tmp/mysql_final_$(date +%Y%m%d_%H%M).sqlscp /tmp/postgres_final_*.sql /tmp/mysql_final_*.sql root@<netcup-ip>:/tmp/All commands below run on the new Netcup server.
# One-line Coolify installercurl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
# This installs:# - Docker (if not already installed)# - Traefik (reverse proxy + auto-SSL)# - Coolify dashboard# - PostgreSQL (for Coolify's own data)
# Coolify dashboard will be available at:# http://<netcup-ip>:8000http://<netcup-ip>:8000 in your browser*.magiceverse.onlinemagic-commerce repository (created in Phase 5B)For the initial migration, we deploy using docker-compose through Coolify while keeping the current code structure.
In Coolify dashboard, create Project: “Shared Infrastructure”:
PostgreSQL:
MySQL:
# Find the PostgreSQL container Coolify createddocker ps | grep postgres
# Restore PostgreSQL (use the actual container name)docker exec -i <coolify-postgres-container> psql -U postgres < /tmp/postgres_final_*.sql
# Verify databasesdocker exec <coolify-postgres-container> psql -U postgres -c "\l"
# Restore MySQLdocker exec -i <coolify-mysql-container> mysql -uroot -p<password> < /tmp/mysql_final_*.sqlExpected databases:
magic_pimmagic_b2b_development, magic_b2b_brinxx, magic_b2b_defaultmagic_b2b_logohorloge, magic_b2b_bovisales, magic_b2b_demomagic_b2b_desluis, magic_b2b_jodasignFor each tenant, create a Coolify Project:
Example: Project “Brinxx”
/mnt/data/magic_omniverse/magic_commerce/magic_brinxx/admin-brinxx.magiceverse.onlinebrinxx.magiceverse.onlineRepeat for all 8 tenants:
| Coolify Project | Backend Domain | Storefront Domain |
|---|---|---|
| Development | admin-development.magiceverse.online | development.magiceverse.online |
| Brinxx | admin-brinxx.magiceverse.online | brinxx.magiceverse.online |
| Default | admin-default.magiceverse.online | default.magiceverse.online |
| Logohorloge | admin-logohorloge.magiceverse.online | logohorloge.magiceverse.online |
| Bovisales | admin-bovisales.magiceverse.online | bovisales.magiceverse.online |
| Demo | admin-demo.magiceverse.online | demo.magiceverse.online |
| De Sluis | admin-desluis.magiceverse.online | desluis.magiceverse.online |
| Jodasign | admin-jodasign.magiceverse.online | jodasign.magiceverse.online |
PIM — Project: “Magic PIM”
/mnt/data/magic_pim/pim.magiceverse.onlineSpranz — Project: “Spranz”
/mnt/data/magic_omniverse/magic_commerce/magic_spranz/n8n — Project: “Automation”
/mnt/data/magic_omniverse/magic_n8n/, magic_n8n_2/, magic_n8n_3/n8n.magiceverse.online, n8n2.magiceverse.online, n8n3.magiceverse.onlineVaultwarden — Project: “Tools”
vaultwarden/server:latest/mnt/data/vaultwarden:/data/vault.magiceverse.onlineSshwifty — same project “Tools”
/mnt/data/magic_omniverse/magic_terminal/terminal.magiceverse.onlinerembg + preflight — same project “Tools”
/opt/services/rembg-service/ and /opt/services/preflight-service/Magic Portal — Project: “Portal”
/mnt/data/magic_omniverse/magic_portal/portal.magiceverse.onlineDashboard — same project “Portal”
/mnt/data/magic_omniverse/magic_dashboard/dashboard.magiceverse.onlineDevDocs (Starlight) — same project “Portal”
/mnt/data/magic_omniverse/magic_docs/starlight/npm run build → serve dist/ directorydevdocs.magiceverse.onlineNextcloud + OnlyOffice (optional) — Project: “Office”
office.magiceverse.onlineIn the Coolify dashboard, check that all services show green/healthy status:
Shared Infrastructure ├── PostgreSQL 16 ✅ Healthy └── MySQL 8 ✅ Healthy
Brinxx ├── Backend ✅ Running → admin-brinxx.magiceverse.online ├── Storefront ✅ Running → brinxx.magiceverse.online ├── Redis ✅ Healthy └── Meilisearch ✅ Healthy
... (repeat for all tenants)
Magic PIM ├── Backend ✅ Running → pim.magiceverse.online └── Redis ✅ Healthy
Automation ├── n8n ✅ Running → n8n.magiceverse.online ├── n8n_2 ✅ Running → n8n2.magiceverse.online └── n8n_3 ✅ Running → n8n3.magiceverse.online
Portal ├── Magic Portal ✅ Running → portal.magiceverse.online ├── Dashboard API ✅ Running → dashboard.magiceverse.online └── DevDocs (Starlight) ✅ Running → devdocs.magiceverse.online
Tools ├── Vaultwarden ✅ Running → vault.magiceverse.online ├── Sshwifty ✅ Running → terminal.magiceverse.online ├── rembg-service ✅ Running └── preflight-service ✅ RunningOnce Phase 5A is stable and DNS is switched, transition to the monorepo architecture for git-push deploys and preview environments.
# Create new GitHub repo: magic-commerce (private)gh repo create magic-commerce --private
# Structure (see Monorepo + Coolify Plan for full details):magic-commerce/├── backend/ # Single Medusa codebase for all tenants├── storefront/ # Single Next.js codebase for all tenants├── tenants/ # Reference configs per tenant (tenant.json)└── docker-compose.yml # Local developmentmagic_development/backend/ as the single backend codebasebackend/src/features/)storefront/src/themes/)main branchFor each tenant, switch the source from local docker-compose to the GitHub monorepo:
magic-commerce repo → Build Path: /backendTENANT_ID=brinxxTENANT_TIER=enterpriseTENANT_FEATURES=technique_pricing,brand_wizardDATABASE_URL=postgres://postgres:xxx@postgres:5432/magic_b2b_brinxxMEDUSA_BACKEND_URL=https://admin-brinxx.magiceverse.onlineIMAGE_BASE_URL=https://images.magiceverse.onlinemainRepeat for all tenants — same repo, different env vars.
*.preview.magiceverse.online → <netcup-ip>Developer opens PR #42 ↓Coolify auto-builds preview ↓Available at: pr-42.preview.magiceverse.online ↓PR merged → preview auto-destroyedPR closed → preview auto-destroyedDeveloper pushes to main ↓Coolify detects push (GitHub webhook) ↓Builds new Docker images for each tenant ↓Zero-downtime container replacement ↓Database migrations run automatically ↓✅ Live on production domainsOn your local machine, temporarily point domains to the new server:
# Add to /etc/hosts (your laptop/desktop)<netcup-ip> admin-development.magiceverse.online<netcup-ip> admin-brinxx.magiceverse.online<netcup-ip> brinxx.magiceverse.online<netcup-ip> pim.magiceverse.online
# Test in browser — verify everything works# Remove /etc/hosts entries after testingIn Cloudflare DNS dashboard:
# On old server — last sync before switchdocker exec magic_pim_postgres_dev pg_dumpall -U postgres > /tmp/postgres_final.sqldocker exec mysql mysqldump -uroot -p<your-db-password> --all-databases > /tmp/mysql_final.sqlscp /tmp/postgres_final.sql /tmp/mysql_final.sql root@<netcup-ip>:/tmp/
# Restore on new server (use actual Coolify container names from `docker ps`)ssh root@<netcup-ip>docker exec -i <coolify-postgres-container> psql -U postgres < /tmp/postgres_final.sqldocker exec -i <coolify-mysql-container> mysql -uroot -p<password> < /tmp/mysql_final.sqlIn Cloudflare DNS dashboard, update all A records:
| Record | Old Value | New Value |
|---|---|---|
*.magiceverse.online | <old-server-ip> | <netcup-ip> |
portal.magiceverse.online | <old-server-ip> | <netcup-ip> |
pim.magiceverse.online | <old-server-ip> | <netcup-ip> |
Coolify/Traefik automatically provisions Let’s Encrypt SSL certificates once DNS points to the server. Check the Coolify dashboard to confirm all domains show a valid certificate.
After confirming everything works (give it 1 hour):
# Run this verification script on the new serverecho "=== Container Status ==="docker ps --format "table {{.Names}}\t{{.Status}}" | sort
echo ""echo "=== Memory Usage ==="free -h
echo ""echo "=== Disk Usage ==="df -h /
echo ""echo "=== Container Count ==="echo "Running: $(docker ps -q | wc -l)"echo "Stopped: $(docker ps -aq --filter 'status=exited' | wc -l)"| Service | URL | What to Check |
|---|---|---|
| Development Admin | https://admin-development.magiceverse.online | Login works, products load |
| Brinxx Admin | https://admin-brinxx.magiceverse.online | Login, orders visible |
| Brinxx Storefront | https://brinxx.magiceverse.online | Products display, images load from R2 |
| All other tenants | https://admin-{tenant}.magiceverse.online | Basic login test |
| PIM | https://pim.magiceverse.online | Product list loads |
| n8n | https://n8n.magiceverse.online | Workflows visible |
| Vaultwarden | https://vault.magiceverse.online | Login works |
| Terminal | https://terminal.magiceverse.online | SSH connection works |
| Portal | https://portal.magiceverse.online | Dashboard loads, tenant list visible |
| Dashboard | https://dashboard.magiceverse.online | API responds, dev projects accessible |
| DevDocs | https://devdocs.magiceverse.online | Pages load |
| Product images | https://images.magiceverse.online/spranz/5325-00.001.jpg | Image loads |
# Install monitoringapt install -y glances
# Watch liveglances
# Check Docker logs for errorsdocker ps -q | xargs -I {} docker logs {} --tail=5 2>&1 | grep -i "error\|fatal\|crash"# Clean up Docker build cachedocker builder prune -f
# Remove dangling imagesdocker image prune -f
# Check final disk usagedf -h /On your local machine (~/.ssh/config):
Host magic-cloud HostName <netcup-ip> User root IdentityFile ~/.ssh/id_ed25519 StrictHostKeyChecking noUpdate the server documentation to reflect the new infrastructure — IP addresses, paths, and connection details.
# Daily database backup script — /opt/scripts/backup-databases.sh#!/bin/bashBACKUP_DIR="/opt/backups/databases"DATE=$(date +%Y%m%d_%H%M)mkdir -p $BACKUP_DIR
# PostgreSQLdocker exec magic_pim_postgres_dev pg_dumpall -U postgres | gzip > $BACKUP_DIR/postgres_$DATE.sql.gz
# MySQLdocker exec mysql mysqldump -uroot -p<your-db-password> --all-databases | gzip > $BACKUP_DIR/mysql_$DATE.sql.gz
# Keep last 7 daysfind $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
echo "Backup completed: $DATE"# Rsync to Hetzner Storage Box — /opt/scripts/backup-remote.sh#!/bin/bashSTORAGEBOX="uXXXXXX@uXXXXXX.your-storagebox.de"
# Sync database backupsrsync -avz --delete /opt/backups/databases/ $STORAGEBOX:backups/databases/
# Sync docker-compose files and configsrsync -avz --delete /mnt/data/magic_omniverse/magic_commerce/*/docker-compose*.yml $STORAGEBOX:backups/compose/rsync -avz --delete /opt/coolify/ $STORAGEBOX:backups/coolify/
echo "Remote backup synced: $(date)"# /etc/crontab additions# Local DB backup — every 6 hours0 */6 * * * root /opt/scripts/backup-databases.sh >> /var/log/backup.log 2>&1
# Remote sync — daily at 3 AM0 3 * * * root /opt/scripts/backup-remote.sh >> /var/log/backup-remote.log 2>&1Product images in Cloudflare R2 are the source of truth. R2 has built-in durability (99.999999999%). For extra safety:
# Weekly sync of R2 to Hetzner Storage Box0 4 * * 0 root rclone sync r2:magic-pim-images/ /tmp/r2-mirror/ && rsync -avz --delete /tmp/r2-mirror/ $STORAGEBOX:backups/images/ && rm -rf /tmp/r2-mirror/| When | Action | New Specs | Price |
|---|---|---|---|
| Current | RS 4000 G12 | 12 cores, 32 GB, 1 TB | €27/mo |
| Need more disk | Add Netcup Block Storage | +500 GB NVMe | +€5.95/mo |
| Need more RAM/CPU | Migrate to RS 8000 G12 | 16 cores, 64 GB, 2 TB | €58/mo |
| High traffic | Add Cloudflare CDN (free) | Cache storefronts at edge | €0 |
| Need HA/scaling | Migrate to Hetzner Cloud | CCX series + load balancer | Variable |