Skip to content

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.me on 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 80 open 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

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 A record per mapped sub-domain. Wildcards are not used, so each new sub-domain needs its own record and server block.
  • A CNAME to the Merciglobal hostname is an acceptable alternative to an A record and survives IP changes, but many providers reject CNAME on a root domain.
  • Root-domain mapping (aufinja.com with no sub-domain) requires an A record 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.

See Also