How I Replaced Localhost with a Custom Domain for Local Development

August 2026
7 min read
Milan Praz

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 mkcert installation 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.