Custom Domain Mapping
Overview
Clients often want their Merciglobal-hosted application to open on their own domain instead of the default Merciglobal URL — for example https://expo.aufinja.com instead of https://erp.merciglobal.com/aufinja/ein/. This is called domain mapping.
The work splits cleanly in two: the client adds one DNS record at their domain provider, and Merciglobal adds a server block plus an SSL certificate. No proxy, no second server and no code duplication is required — the mapped domain serves the same application from the same host.
Who Does What
| Task | Owner | Typical time |
|---|---|---|
Add DNS A record for the sub-domain |
Client IT / domain provider | 5 minutes |
| DNS propagation | — | 15 min – 24 hrs |
| Create nginx server block | Merciglobal | 10 minutes |
| Issue SSL certificate | Merciglobal | 2 minutes |
| Application URL / session / callback checks | Merciglobal | 30 – 60 minutes |
Prerequisites
- Server public IP of the host running the application (
curl -4 ifconfig.meon the box). - Client must control DNS for the parent domain, or be able to instruct whoever does.
- Root / sudo access on the application server.
- Port
80open to the internet — Certbot's HTTP-01 challenge fails without it.
!!! warning "Confirm the IP before sending it to the client" Always read the IP off the server itself rather than from an old email or ticket. A single wrong digit sends the client's domain to a stranger's network and produces a Certbot failure with no obvious cause.
Part 1 — Client Side: Add the DNS Record
Send the client these values. Everything except the record type is client-specific.
| Type | Name / Host | Value / Points to | TTL |
|---|---|---|---|
A |
<subdomain> |
<server public IP> |
3600 (or Auto) |
Worked example — mapping expo.aufinja.com to a server at 92.4.72.123:
| Type | Name / Host | Value / Points to | TTL |
|---|---|---|---|
A |
expo |
92.4.72.123 |
3600 |
The Name / Host field
Most DNS panels expect only the sub-domain part — the client types expo and the panel completes it to expo.aufinja.com. A few panels expect the fully qualified name instead. Tell the client to check what the panel displays after saving; if it shows expo.aufinja.com.aufinja.com, the field was over-filled.
Never instruct a client to change @ or www — those carry their main website.
Cloudflare clients
!!! warning "Set the record to DNS only during setup"
If the client uses Cloudflare, the orange cloud beside the record must be clicked so it turns grey (DNS only). With the proxy on, Certbot validates against Cloudflare's edge instead of the origin and issuance fails. The proxy can be re-enabled after the certificate is live, provided Cloudflare SSL mode is set to Full (strict).
Verify propagation before touching the server
dig +short expo.aufinja.com
# expected: 92.4.72.123
# or, from a Windows client machine
nslookup expo.aufinja.com
Do not proceed to Part 2 until this returns the correct IP. dnschecker.org is a useful second opinion when the client insists the record is saved but local resolvers disagree.
Part 2 — Merciglobal Side: Server Configuration
Step 1 — Create the server block
Create /etc/nginx/sites-available/expo.aufinja.com with HTTP only. Certbot will add the TLS block itself.
server {
listen 80;
listen [::]:80;
server_name expo.aufinja.com;
root /var/www/devx/aufinja/ein;
index cart_login.php index.php;
access_log /var/log/nginx/expo.aufinja.com.access.log;
error_log /var/log/nginx/expo.aufinja.com.error.log;
# keep legacy deep links working after the domain change
rewrite ^/aufinja/ein/(.*)$ /$1 last;
location = / { return 302 /cart_login.php; }
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SERVER_NAME $host;
}
location ~ /\. { deny all; }
}
If the application pulls shared assets from above its own folder, alias them explicitly rather than moving files:
location ^~ /common/ { alias /var/www/devx/common/; }
location ^~ /assets/ { alias /var/www/devx/assets/; }
Step 2 — Enable and test
sudo ln -s /etc/nginx/sites-available/expo.aufinja.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Load http://expo.aufinja.com in a browser before requesting a certificate. A working page over plain HTTP confirms both DNS and the server block are correct, which isolates any later failure to Certbot alone.
Step 3 — Issue the SSL certificate
sudo certbot --nginx -d expo.aufinja.com
Certbot writes the 443 block, the HTTP→HTTPS redirect, and the certificate paths into the same file. Renewal is picked up automatically by the existing systemd timer.
Step 4 — Confirm renewal will succeed
sudo certbot renew --dry-run
sudo systemctl list-timers | grep certbot
!!! tip
Run certbot certificates after adding each mapped domain and keep the expiry list in the client's handover notes. A mapped domain that silently fails renewal 90 days later is the single most common post-go-live incident.
Part 3 — Application Checklist
DNS and nginx are the easy half. Most failures after a domain mapping come from the application still assuming it lives at its original path. Work through all of these before handover.
Hardcoded absolute URLs
grep -rn "devx.merciglobal.com" /var/www/devx/aufinja/ein/
Form actions, header("Location: ...") redirects, <img src> tags and JavaScript fetch() calls carrying the full original URL will bounce the user back to the Merciglobal domain mid-session. Replace with relative paths, or derive the base from the request:
🚫 Less Efficient Version
$base = "https://devx.merciglobal.com/aufinja/ein/";
header("Location: " . $base . "cart_confirm.php");
✅ Optimized Version
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$base = $scheme . '://' . $_SERVER['HTTP_HOST'] . '/';
header("Location: " . $base . "cart_confirm.php");
Session cookies
If session.cookie_domain is pinned to .merciglobal.com anywhere in php.ini, an .htaccess, or a bootstrap file, logins on the mapped domain will appear to succeed and then immediately drop the user back to the login screen. Leave the value unset so PHP defaults to the requesting host.
// avoid
ini_set('session.cookie_domain', '.merciglobal.com');
Payment gateway callbacks
Razorpay (or any gateway) return URLs and webhook endpoints must be updated to the mapped domain, and the new domain added to the allowed/whitelisted origins in the gateway dashboard. A stale callback URL produces a successful payment with an unconfirmed order — the hardest failure to diagnose after the fact.
Other items
| Item | What to check |
|---|---|
| Email templates | Links back into the portal must use the mapped domain |
| CORS headers | Add the mapped origin if any API is called from the browser |
| e-Invoice / IRN callbacks | Any registered return URL pointing at the old host |
| PDF / print templates | Logo and asset URLs resolved absolutely |
robots.txt / sitemap |
Regenerate if the portal is publicly indexed |
Recommended Pattern: Resolve the Tenant from the Host
Mapping the second and third client is only cheap if the application derives its tenant from the hostname rather than the URL path. Resolve pj_<client>_ at bootstrap instead of parsing the request path:
// bootstrap.php — host to project resolution
$hostMap = [
'expo.aufinja.com' => 'aufinja',
'devx.merciglobal.com' => null, // path-based legacy access
];
$host = strtolower($_SERVER['HTTP_HOST'] ?? '');
$project = $hostMap[$host] ?? null;
if ($project === null) {
// fall back to the legacy /<project>/ path segment
$project = explode('/', trim($_SERVER['REQUEST_URI'], '/'))[0] ?? '';
}
define('PROJECT', $project);
define('TBL_PREFIX', 'pj_' . PROJECT . '_');
With this in place, onboarding a new mapped domain reduces to: one DNS record, one server block, one certbot run, one row in the host map.
!!! tip "Beyond a dozen mapped domains" Per-client certificate management stops scaling. At that point evaluate Cloudflare SSL for SaaS (custom hostnames), which handles the client CNAME and certificate issuance while Merciglobal keeps a single origin.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Domain does not resolve | Record not saved, or still propagating | dig +short <domain>; wait out the TTL |
| Resolves to the wrong IP | Host field over-filled, or an old A record still present |
Check for a duplicate record on the same name |
| Merciglobal default site loads instead | No server_name match — nginx served the default block |
Confirm the symlink exists and nginx -t passed |
| Certbot: challenge failed | Cloudflare proxy on, or port 80 blocked | Grey-cloud the record; check the OCI security list and ufw |
| Certbot: too many certificates | Let's Encrypt rate limit hit from repeated attempts | Wait for the weekly window; use --dry-run while debugging |
| Login loops back to the login page | session.cookie_domain pinned to the old domain |
Unset it and restart PHP-FPM |
| Assets 404 on the new domain | Shared folders sit above the docroot | Add alias blocks for those paths |
| Mixed-content warnings | Assets referenced over http:// |
Switch to protocol-relative or https:// URLs |
Removing a Mapped Domain
sudo rm /etc/nginx/sites-enabled/expo.aufinja.com
sudo nginx -t && sudo systemctl reload nginx
sudo certbot delete --cert-name expo.aufinja.com
Ask the client to remove the A record as well. A record left pointing at a re-used IP will serve whatever occupies that address next.
Notes & Caveats
- One
Arecord per mapped sub-domain. Wildcards are not used, so each new sub-domain needs its own record and server block. - A
CNAMEto the Merciglobal hostname is an acceptable alternative to anArecord and survives IP changes, but many providers rejectCNAMEon a root domain. - Root-domain mapping (
aufinja.comwith no sub-domain) requires anArecord and interacts with the client's existing website — confirm in writing before proceeding. - If the server IP is ever changed, every mapped client must be notified in advance. Keep the mapped-domain list current.