Local Node Development with Custom Subdomains & SSL (on OSX 10.15 Catalina)

Hi, I’m Pete, and I’m sharing this because everything I needed to know was spread all over the place. Hopefully this helps you out.

Here are my constraints:

  • Visiting https://exhibits.localhistorymuseum.com points to a react app on port 3040.
  • Visiting https://tickets.localhistorymuseum.com points to another react app on port 3050.
  • Requests to https://api.localhistorymuseum.com point to example express app on 3060.
  • I don’t want to see port numbers on the URLs.
  • I want to minimize security alerts in the browser.

Here’s a summary of the work to be done:

  1. Generate self-signed cert
  2. Add it to the OSX keychain
  3. Set up your apps (with bonus CORS content)
  4. Update /etc/hosts
  5. Set up Nginx

Let’s get started!

Generate your key/CA

We’re acting as our own certificate authority in this case. We’ll use that key to generate a certificate. If you want to get into the nitty-gritty, I learned a ton from this guide.

openssl genrsa -des3 -out host.key 2048
Image for post
Image for post
looking histerrific

Generate your cert

Now we’ll use that key to sign a certificate. You’ll need your handy passphrase on hand (antelope). In the localhistorymuseum.com directory run:

openssl req -x509 -new -nodes -key host.key -sha256 -days 1024 -out host.pem
Image for post
Image for post
Image for post
Image for post
Now we have our cert!

Add it as a trusted cert in the OSX keychain

Open up Keychain Access in OSX and you’ll see something like this (if you click on System):

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post
This will pop open a dialog. You’ll have to expand the “Trust” section.
Image for post
Image for post
set this to Always Trust (this will auto-set all drop-downs). We want that.
Image for post
Image for post
We’re all set!

Set up your apps

I’m going to limit the amount of boilerplate I provide here because this stuff is really well documented elsewhere. Please comment and ask if I missed something you would really find helpful. 🙏

➜ create-react-app exhibits-web
...
➜ create-react-app tickets-web
...
➜ mkdir api
➜ yarn init
...
➜ touch index.js
➜ touch .env
➜ et cetera...
Image for post
Image for post

Configure our apps to use custom (sub)domains

In your .env files for the frontend apps, you may want to add references to your local subdomains for URL construction config.

# create-react-app automatically pulls in env vars prefixed 
# with REACT_APP_ to process.env
REACT_APP_API_HOST=https://api.localhistorymuseum.com
REACT_APP_TICKETS_HOST=https://tickets.localhistorymuseum.com/
REACT_APP_BASE_URL=https://exhibits.localhistorymuseum.com/

Configure our apps for HTTPS

For exhibits-web and tickets-web (via create-react-app) this is easy. We just set an env var inline when we run it like this: HTTPS=true yarn start.

const https = require('https');
const cors = require('cors');
...
// configure allowed list via .env
export const ALLOWED_ORIGINS = [
process.env.TICKET_CORS_DOMAIN,
process.env.EXHIBITS_CORS_DOMAIN
];
const defaultCorsOptions = {
optionsSuccessStatus: 200,
allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
exposedHeaders: true,
credentials: true
};
const corsConfigDelegate = (req, callback) => {
if (ALLOWED_ORIGINS.includes(req.headers['origin'])) {
// yes CORS
return callback(null, { ...defaultCorsOptions, origin: true });
}
return callback(null, { origin: false }); // no CORS
};
...app.options('*', cors(corsConfigDelegate));
app.use('*', cors(corsConfigDelegate));
...app.get('/data', (req, res) => res.json({ exhibits: [], tickets: [] }));...if (process.env.NODE_ENV === 'development') {
https
.createServer(
{
key: fs.readFileSync(`/path/to/${process.env.KEY_NAME}`),
cert: fs.readFileSync(`/path/to/${process.env.CERT_NAME}`),
passphrase: process.env.CERT_PASSPHRASE
},
app
)
.listen(PORT, function() {
console.log('>> HTTPS << server listening on ' + PORT);
});
}
  1. set up CORS delegate
  2. assign CORS delegate to all requests
  3. define GET request to /data that returns a little sample JSON
  4. start the HTTPS server w/cert

Loop back your custom URLs to localhost

We’re going to modify /etc/hosts on OSX, which will point our host names to IP addresses. Namely, *.localhistorymusuem.com to localhost. We don’t want our traffic escaping into the real world!

sudo /etc/hosts
127.0.0.1       exhibits.localhistorymuseum.com
127.0.0.1 tickets.localhistorymuseum.com
127.0.0.1 api.localhistorymuseum.com

Set up Nginx

I chose Nginx because it’s the tool I’m most familiar with. I’m sure there are parallels in HAProxy, lighttpd, or whatever flavor of web server you enjoy. I installed via homebrew.

# Note: I omitted logs to keep this reasonably readable...
# see this full example on nginx.org for more info
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# EXHIBITS
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name exhibits.localhistorymuseum.com;
ssl_certificate /etc/ssl/certs/localhistorymuseum.com/host.pem;
ssl_certificate_key /etc/ssl/certs/localhistorymuseum.com/host.key;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass https://0.0.0.0:3040;
}
}
# TICKETS
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name tickets.localhistorymuseum.com;
ssl_certificate /etc/ssl/certs/localhistorymuseum.com/host.pem;
ssl_certificate_key /etc/ssl/certs/localhistorymuseum.com/host.key;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass https://0.0.0.0:3050;
}
}
# API
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name api.localhistorymuseum.com;
ssl_certificate /etc/ssl/certs/localhistorymuseum.com/host.pem;
ssl_certificate_key /etc/ssl/certs/localhistorymuseum.com/host.key;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass https://0.0.0.0:3060;
}
}
}

Pulling it all together

OK, so by now we have:

  • 3 apps (2 frontend, 1 api), running on ports 3040, 3050, 3060
  • an updated /etc/hosts file
  • a configured nginx instance running
const response = await fetch('https://api.localhistorymuseum.com/data');
const data = await response.json();
console.log(JSON.stringify(data)); // { exhibits: [], tickets: [] }

Godspeed

I distilled this from my existing solution, so there may be a rough edge here and there. If this blows up, I’ll put together a repo with a working example, otherwise, feel free to ask questions 🙏

Resources

Professional typist — retired dungeon master 🐉— 👨🏻❤️🍕 — podcast about books at www.take.fyi

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store