Using a Custom Local Domain for Local Development on Windows
I ran into this problem while working on a web application that had a frontend running locally and a backend running on a remote server.
My frontend was running on:
http://localhost:3003
The backend was already deployed and was setting authentication cookies for its parent domain, something like:
Domain=.example.com
That worked fine for deployed applications such as:
https://app.example.com
https://admin.example.com
But it did not behave the same way with:
http://localhost:3003
The reason is simple: localhost is not a subdomain of example.com.
I did not want to change the backend cookie logic just to make local development work. I wanted my local environment to behave more like the real deployed environment.
The solution was to give my local application a custom domain such as:
local.example.com
while still making it point to my own machine.
This post explains how that works and how I set it up on Windows.
The basic idea
Normally, when we enter a URL such as:
https://facebook.com
the browser needs to find the IP address for facebook.com.
At a simplified level, the process looks like this:
Browser
β
"What IP belongs to facebook.com?"
β
Check local information
β
If not found, ask DNS
β
DNS returns an IP address
β
Browser connects to that server
Your computer also has a local file called the hosts file.
The hosts file lets us manually say:
this hostname = this IP address
For example:
127.0.0.1 local.example.com
127.0.0.1 means the current computer, just like localhost.
So after adding that entry, when I open:
https://local.example.com:3003
Windows does not need to resolve that hostname using public DNS. It already has a local answer:
local.example.com
β
127.0.0.1
β
my own computer
β
port 3003
β
local Next.js application
This does not expose the local application to the internet.
The mapping only exists on my computer.
Step 1: Add the custom domain to the Windows hosts file
On Windows, the hosts file is located at:
C:\Windows\System32\drivers\etc\hosts
Open PowerShell as Administrator and run:
notepad C:\Windows\System32\drivers\etc\hosts
Then add:
127.0.0.1 local.example.com
Save the file.
After that, flush the Windows DNS cache:
ipconfig /flushdns
Now test the mapping:
ping -4 local.example.com
The important part of the output should look like:
Pinging local.example.com [127.0.0.1]
If it shows 127.0.0.1, the hosts mapping is working.
Step 2: Run the local app using that hostname
Suppose the Next.js application normally runs on port 3003.
Without HTTPS, I could already run the application locally and open:
http://local.example.com:3003
At this point the hostname mapping is already working.
The browser thinks the hostname is:
local.example.com
while Windows sends the request to:
127.0.0.1
That was the main part I needed for domain-based development.
Why I also needed HTTPS
In my case, authentication cookies were marked as:
Secure
A typical cookie might look conceptually like this:
Set-Cookie: auth_token=...;
Domain=.example.com;
Secure;
HttpOnly;
A Secure cookie is meant to be used over HTTPS.
So instead of developing with:
http://local.example.com:3003
I wanted:
https://local.example.com:3003
That meant I needed a certificate trusted by my local browser.
For this, I used mkcert.
Step 3: Install and initialize mkcert
After installing mkcert on Windows, run this once from PowerShell:
mkcert -install
This creates a local certificate authority and adds it to the local trust store.
In simple terms, it tells Windows and supported browsers:
Certificates created by this local
mkcertinstallation can be trusted on this machine.
This is normally a one-time setup per computer.
Step 4: Generate a certificate for the local domain
From the project directory:
mkdir certs
Then generate a certificate:
mkcert -key-file certs/local.example.com-key.pem -cert-file certs/local.example.com.pem local.example.com
That creates two files:
certs/
βββ local.example.com-key.pem
βββ local.example.com.pem
The .pem file is the certificate, and the -key.pem file is its private key.
These files should generally stay local and should not be committed to a public repository.
A useful .gitignore entry is:
certs/
Step 5: Run Next.js with HTTPS
I then started Next.js using the generated certificate:
pnpm next dev --hostname 127.0.0.1 --port 3003 --experimental-https --experimental-https-key .\certs\local.example.com-key.pem --experimental-https-cert .\certs\local.example.com.pem
Now I could open:
https://local.example.com:3003
The full request flow became:
Browser
β
https://local.example.com:3003
β
Windows hosts file
β
127.0.0.1
β
Port 3003
β
Next.js development server
The browser still sees local.example.com, which is the important part.
Why this helps with cookies
Imagine the backend is running at:
https://api.example.com
and it creates a cookie for:
.example.com
My local frontend is now:
https://local.example.com:3003
Both hostnames belong under the same parent domain:
example.com
βββ api.example.com
βββ local.example.com
That makes the local environment much closer to the real deployed setup than using plain localhost.
Depending on the authentication setup, I may also need to send requests with credentials.
Using fetch:
fetch('https://api.example.com/me', {
credentials: 'include',
});
Or with Axios:
axios.get('https://api.example.com/me', {
withCredentials: true,
});
The backend also needs to allow the local frontend origin when CORS is involved, for example:
https://local.example.com:3003
An important thing I misunderstood at first
At first, mapping a domain to 127.0.0.1 felt like I was somehow exposing my local application to the internet.
That is not what happens.
This entry:
127.0.0.1 local.example.com
exists only in my own hosts file.
Someone elseβs computer does not know about that mapping.
On my machine:
local.example.com β 127.0.0.1
On another personβs machine, the same hostname would follow their normal DNS resolution process.
The hosts file is essentially a small local DNS override.
Checking that everything is working
There are a few useful PowerShell commands.
Check the hosts entry:
Get-Content "$env:SystemRoot\System32\drivers\etc\hosts" | Select-String "local.example.com"
Expected result:
127.0.0.1 local.example.com
Check hostname resolution:
ping -4 local.example.com
Expected:
Pinging local.example.com [127.0.0.1]
Check that the certificate files exist:
Test-Path .\certs\local.example.com-key.pem
Test-Path .\certs\local.example.com.pem
Both should return:
True
Then start the application and open:
https://local.example.com:3003
The mental model that made it click for me
I now think about the setup as three separate pieces.
Hosts file
The hosts file answers:
Where should local.example.com go?
Answer:
127.0.0.1
Port
The port answers:
Which application on my computer should receive the request?
Answer:
3003
Certificate
The certificate answers:
Can the browser trust HTTPS for local.example.com?
Answer:
Yes, through the local mkcert certificate.
Put together:
https://local.example.com:3003
β
βββ local.example.com β 127.0.0.1
β
βββ :3003 β Next.js
β
βββ HTTPS β local mkcert certificate
Final thoughts
This ended up being much simpler than changing authentication logic specifically for localhost.
Instead of teaching the backend about a completely different development environment, I made my local frontend behave more like a real subdomain.
The main pieces were:
Hosts file β map a custom hostname to 127.0.0.1
mkcert β create trusted local HTTPS
Next.js β run the app on the desired local port
It is a useful setup whenever local development needs to closely reproduce domain-specific behavior such as cookies, subdomains, HTTPS, redirects, or authentication flows.
And the part I found most useful to understand was this:
A domain name does not have to come from public DNS. Your own computer can override where a hostname points before a public DNS lookup is needed.
Once that clicked, the rest of the setup made a lot more sense.