Custom Local Domain over HTTPS

Note

This is a documention for my own reference, so I can come back to it in the future either for setting up a new custom local domain over HTTPS using docker and nginx, or reverting back to the original state.

Steps

mkcert

Install mkcert:

# MacOS
$ brew install mkcert

# Create and install a local certificate authority (CA), trusted locally on your device.
$ mkcert -install

# List the root CA files.
$ ls "$(mkcert -CAROOT)"
rootCA-key.pem rootCA.pem

# Create a trusted certificate.
$ mkdir potato_cld && cd potato_cld
$ mkcert potato.cld "*.potato.cld"

Created a new certificate valid for the following names 📜
 - "potato.cld"
 - "*.potato.cld"

Reminder: X.509 wildcards only go one level deep, so this won\'t match a.b.potato.cld

The certificate is at "./potato.cld+1.pem" and the key at "./potato.cld+1-key.pem" ✅

It will expire on 11 May 2026 🗓

The command mkcert potato.cld "*.potato.cld" above does two things:

  1. Generates a certificate for the hostname you've specified
  2. Lets mkcert (that you've added as a local CA) sign this certificate.

Now, the certificate is ready and signed by a certificate authority that the browser trusts locally.

Configure DNS

1. Modify /etc/hosts

The simplest way to go about this is to update the /etc/hosts as shown below,

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	      localhost
255.255.255.255	broadcasthost
::1             localhost

127.0.0.1       potato.cld api.potato.cld api2.potato.cld

But that is too simple. Let's crank up the complexity so I can learn something new - dnsmasq.

2. Install, configure and start the dnsmasqserver

dnsmasq is a lightweight DNS, TFTP, PXE, router advertisement and DHCP server. It is intended to provide coupled DNS and DHCP service to a LAN.

Dnsmasq accepts DNS queries and either answers them from a small, local, cache or forwards them to a real, recursive, DNS server. It loads the contents of /etc/hosts so that local hostnames which do not appear in the global DNS can be resolved and also answers DNS queries for DHCP configured hosts. It can also act as the authoritative DNS server for one or more domains, allowing local names to appear in the global DNS. ... source: man dnsmasq

# Install
$ brew install dnsmasq
$ brew list dnsmasq
/opt/homebrew/Cellar/dnsmasq/2.90/.bottle/etc/dnsmasq.conf
/opt/homebrew/Cellar/dnsmasq/2.90/homebrew.dnsmasq.service
/opt/homebrew/Cellar/dnsmasq/2.90/homebrew.mxcl.dnsmasq.plist
/opt/homebrew/Cellar/dnsmasq/2.90/sbin/dnsmasq
/opt/homebrew/Cellar/dnsmasq/2.90/share/man/man8/dnsmasq.8

# Create a new directory and a `.conf` file:
$ mkdir -p /Users/bsm/Development/dnsmasq.d
$ touch /Users/bsm/Development/dnsmasq.d/dnsmasq.conf

We will now uncomment and update the value of conf-dir= in /opt/homebrew/etc/dnsmasq.conf to point to dnsmasq.conf file we created above.

% cat $(brew --prefix)/etc/dnsmasq.conf | grep conf-dir
#conf-dir=/opt/homebrew/etc/dnsmasq.d
conf-dir=/Users/bsm/Development/dnsmasq.d/,*.conf
#conf-dir=/opt/homebrew/etc/dnsmasq.d,.bak
#conf-dir=/opt/homebrew/etc/dnsmasq.d/,*.conf

Next, edit /Users/bsm/Development/dnsmasq.d/dnsmasq.conf and add the line address=/potato.cld/127.0.0.1.

$ cat /Users/bensoorajmohan/Development/dnsmasq.d/dnsmasq.conf 
address=/potato.cld/127.0.0.1

Now,

# start the `dnsmasq` service:
$ sudo brew services start dnsmasq

Warning: Taking root:admin ownership of some dnsmasq paths:
  /opt/homebrew/Cellar/dnsmasq/2.90/sbin
  /opt/homebrew/Cellar/dnsmasq/2.90/sbin/dnsmasq
  /opt/homebrew/opt/dnsmasq
  /opt/homebrew/opt/dnsmasq/sbin
  /opt/homebrew/var/homebrew/linked/dnsmasq

This will require manual removal of these paths using `sudo rm` on
brew upgrade/reinstall/uninstall.
==> **Successfully started `dnsmasq` (label: homebrew.mxcl.dnsmasq)**

# verify that the service is running:
$ sudo brew services
**Name  Status  User File**
black   none         
dnsmasq started root /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
unbound none

3. Configure resolv.conf

Configure the DNS server to be used for the domain potato.cld:

# Make a new folder called /etc/resolver/, if it doesn't exist
$ sudo mkdir -p /etc/resolver/
# Create a new file with the name of the domain you want custom DNS settings for
$ touch /etc/resolver/potato.cld
# Set the nameserver that the domain should resolve to
$ sudo tee /etc/resolver/potato.cld >/dev/null <<EOF
nameserver 127.0.0.1
EOF

# restart local dnsmasq service
$ sudo brew services restart dnsmasq
Stopping `dnsmasq`... (might take a while)

==> **Successfully stopped `dnsmasq` (label: homebrew.mxcl.dnsmasq)**
Warning: Taking root:admin ownership of some dnsmasq paths:
  /opt/homebrew/Cellar/dnsmasq/2.90/sbin
  /opt/homebrew/Cellar/dnsmasq/2.90/sbin/dnsmasq
  /opt/homebrew/opt/dnsmasq
  /opt/homebrew/opt/dnsmasq/sbin
  /opt/homebrew/var/homebrew/linked/dnsmasq
This will require manual removal of these paths using `sudo rm` on
brew upgrade/reinstall/uninstall.
==> **Successfully started `dnsmasq` (label: homebrew.mxcl.dnsmasq)**

Verify

$ dscacheutil -q host -a name potato.cld

name: potato.cld
ip_address: 127.0.0.1

# Verify the new resolver was picked up
$ scutil --dns
DNS configuration

resolver #8
  domain        : potato.cld
  nameserver[0] : 127.0.0.1
  flags         : Request A records, Request AAAA records
  reach         : 0x00030002 (Reachable,Local Address,Directly Reachable Address)

Docker and Nginx

Clone the git repository and start the docker containers:

$ git clone https://github.com/bensooraj/blog-artefact-custom-local-domain.git
$ docker-compose up -d

$ curl https://api.potato.cld/ok 
200 OK: api.potato.cld

$ curl https://api2.potato.cld/ok
200 OK: api2.potato.cld

$ docker ps -a

CONTAINER ID   IMAGE                   COMMAND                  CREATED         STATUS         PORTS                                      NAMES
ef88c504a751   nginx:latest            "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx
39651f26920c   one_tls_whoami-goapi    "./server"               2 minutes ago   Up 2 minutes   8080/tcp, 0.0.0.0:8080->443/tcp            one_tls_whoami-goapi-1
a98caad7d4cb   one_tls_whoami-goapi2   "./server"               2 minutes ago   Up 2 minutes   8080/tcp, 0.0.0.0:8081->443/tcp            one_tls_whoami-goapi2-1

$ docker logs nginx --tail=2
192.168.65.1 - - [15/Feb/2024:09:54:17 +0000] "GET /ok HTTP/1.1" 200 23 "-" "curl/8.1.2" "-"
192.168.65.1 - - [15/Feb/2024:09:54:18 +0000] "GET /ok HTTP/1.1" 200 24 "-" "curl/8.1.2" "-"

$ docker logs one_tls_whoami-goapi-1 --tail=2
2024/02/15 09:54:09 INFO server started on localhost. protocol=https port=443
2024/02/15 09:54:17 INFO Header:  X-API-ServerName=api.potato.cld

$ docker logs one_tls_whoami-goapi2-1 --tail=2                       
2024/02/15 09:54:09 INFO server started on localhost. protocol=https port=443
2024/02/15 09:54:18 INFO Header:  X-API-ServerName=api2.potato.cld

Explore the nginx configuration files:

$ cat nginx/conf/api2_potato_cld.conf

server {
	listen 80;
	listen [::]:80;
	server_name api2.potato.cld;

	return 301 https://api2.potato.cld$request_uri;
}

server {
	listen 443 ssl;
	listen [::]:443 ssl;

	server_name api2.potato.cld;

	ssl_certificate /etc/nginx/ssl/potato.cld.pem;
	ssl_certificate_key /etc/nginx/ssl/potato.cld-key.pem;

	# If they come here using HTTP, bounce them to the correct scheme
	error_page 497 https://$server_name:$server_port$request_uri;

	error_log /dev/stderr;
	access_log /dev/stdout main;

	location / {
		proxy_set_header X-API-ServerName $server_name;
		proxy_set_header X-Forwarded-For $remote_addr;
		proxy_set_header Host $http_host;
		proxy_pass https://host.docker.internal:8081;
	}
}

References

  1. How to use HTTPS for local development
  2. macOS: Using Custom DNS Resolvers
  3. Mastering NGINX Logs in Docker: From Novice to Ninja
  4. How to create local wildcard domains in MacOS using dnsmasq?
  5. Gist: Never touch your local /etc/hosts file in OS X again
  6. OS/X "etc/resolver/dev" isn't working - why not?
  7. Using Dnsmasq for local development on OS X
  8. Local domains with HTTPS and Docker compose
  9. Docker-Powered Web Development Utilizing HTTPS and Local Domain Names
  10. Setting up a custom domain for your local apps (Mac OS & Linux)
  11. Configure NGINX as a Reverse Proxy with Docker Compose file
  12. How to Setup a Local DNS Server Using DNSMasq