DEV Community

Theodor Heiselberg
Theodor Heiselberg

Posted on

(4)Creating the Pinnacle of Niche Software: Abandoning localhost:1234 - locally!

About

The concrete implementation was made on MacOS, but it should be quite easy to alter and adopt this example to both Windows and Linux.

Exclusively tested with Vite 6+!

In this article we will implement the ideas presented here - Production Parity: Routing Local Traffic with a Reverse Proxy

The main idea is to achieve parity between our development setup and production.

You might think: 'Why would this be necessary?' - The argumentation for achieving environment parity is covered here: the-red-pill-of-software-delivery-unmasking-magic-code-and-building-for-reality

This article is a continuation of creating-the-pinnacle-of-niche-software-using-vite-plugin-elm-watch and the example created there will be extended in the branch named feature/use-custom-domain - Link!

Goals

  1. Use a custom domain name and completely ditch the usage of localhost:port when working on our solution from the browser
  2. Develop from a devcontainer
  3. Enable HTTPS

Now let's go through how we achieve this fine objective :)

The moving parts

Next up we will setup the following:

  • Add nginx to the solution
  • Configure our hosts file
  • Configure nginx
  • Create and trust certificates

Reverse Proxy

First of all we need to add a reverse proxy like nginx to handle routing from a url/domain to the vite server. This won't break HRL and will bring all the benefits discussed earlier. I'll use nginx throughout this article.

If you are following along from the previous article, it should be quite straight forward to extend the the code in the following. A working example can be found here: Link! in the branch use-custom-domanin

Add nginx
First we'll add nginx to the docker-compose.yml

name: elm-first

services:
  dev: ..

  nginx:
    image: nginx:alpine
    container_name: elm-first-nginx
    ports:
      - "80:80"
      - "443:443"
    networks:
      - internal
    # volumes:
      # - ./nginx.conf:/etc/nginx/nginx.conf:ro
      # - ./ssl:/etc/nginx/certs:ro
    depends_on:
      - dev
volumes:
  elm-first-vite-example:

networks:
  internal:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Add endpoint to your hosts file

On mac it's simply done by:
> sudo vi /etc/hosts and add

127.0.0.1       happy.now www.happy.now
127.0.0.1       happy.dev
Enter fullscreen mode Exit fullscreen mode

Save the file an restart the devcontainer.
I had some troubles with when the mappings in the hosts file were picked up by the browsers.

But now we can access the default nginx site by simply using: happy.now

Observe that using happy.dev will redirect to https://happy.dev

Next we'll route to our vite server running on port 3456

Serve the running vite sever

  1. Add a custom nginx.conf touch .d evcontainer/nginx.conf
events {
    worker_connections 1024;
}

http {

    # Define resolver to handle host.docker.internal
    resolver 127.0.0.11;

    server {
        listen 80;
        server_name www.happy.dev happy.dev www.happy.now happy.now;
        # Proxy frontend requests to Elm dev server
        location / {
            set $frontend_upstream host.docker.internal:3456;
            proxy_pass http://$frontend_upstream;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
            sub_filter '</head>' '<meta name="environment-name" content="local" /></head>';
            sub_filter_once on;
            # Only apply sub_filter to index.html
            # sub_filter_types text/html;
        }
    }
Enter fullscreen mode Exit fullscreen mode
  1. Uncomment in the docker-compose.yml
 volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
Enter fullscreen mode Exit fullscreen mode
  1. From the host machine! run: docker exec elm-first-nginx nginx -t
  2. It the config is ok now run: docker exec elm-first-nginx nginx -s reload
  3. Start the vite server by running: npm run dev
  4. The vite server will bounce access from anything but localhost so update the allowedHosts whith:
allowedHosts: ["host.docker.internal", "www.happy.now", "happy.now", "www.happy.dev", "happy.dev"],
Enter fullscreen mode Exit fullscreen mode
  1. Reload the page and be a happy developer now!

Finally we will enable HTTPS.

Enable HTTPS

First we'll need to rearrage the project a bit. Start with stopping the devcontainer.

Rearrage nginx config

Add the missing nginx folder and add files.

project-root/
├── .devcontainer/
│   ├── ssl/
│   ├── devcontainer.json
│   ├── docker-compose.yml
│   ├── Dockerfile.devmachine
│       └── nginx/
|           ├── nginx.conf
|           └── conf.d/
|               └── proxy_elm_env.inc <-- NEW
Enter fullscreen mode Exit fullscreen mode

The new configs should look like this:
nginx.conf

events {
    worker_connections 1024;
}

http {

    # Define resolver to handle host.docker.internal
    resolver 127.0.0.11;

    server {
        listen 80;
        server_name www.happy.dev happy.dev www.happy.now happy.now;
        # Proxy frontend requests to Elm dev server
        location / {
            include /etc/nginx/conf.d/proxy_elm_env.inc;
        }
    }

    server {
        listen 443 ssl;
        server_name www.happy.dev happy.dev;

        ssl_certificate     /etc/nginx/certs/happy.dev.crt;
        ssl_certificate_key /etc/nginx/certs/happy.dev.key;

        location / {
            include /etc/nginx/conf.d/proxy_elm_env.inc;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

proxy_elm_env.inc

proxy_pass http://$frontend_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
sub_filter '</head>' '<meta name="environment-name" content="local" /></head>';
Enter fullscreen mode Exit fullscreen mode

Certificates

We need a certificate in order to use HTTPS. Here we'll create a self signed cert.

Let's use happy as our domain name.

Since we will be using squidex in the next article we create a cert which can handle both happy.dev and squidex.happy.dev.

Notes on TLD's
It's important to notice that some TLD's are special.
dev, app, and page will all have HTTPS enforced by the browser if used. Unlike test, localhost or local which, at least on MacOS, will be treated as safe

Method 1 (Not so good)

Using this method will require a cert for each domain name.
Eg. one for both happy.dev and squidex.happy.dev

cd /workspace/.devcontainer/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout happy.dev.key -out happy.dev.crt \
  -subj "/CN=happy.dev" \
  -addext "subjectAltName=DNS:happy.dev,DNS:*.happy.dev"

# Set proper permissions
chmod 644 squidex.happy.dev.crt
chmod 600 squidex.happy.dev.key
Enter fullscreen mode Exit fullscreen mode

Repeat for the happy.dev domain. But I prefer to just use method 3.

Method 2 (Better)

Using this method will make you only need one certificate.

Create a req.cnf file in eg. the .devcontainer/ssh folder.

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
C = DK
ST = Central Denmark Region
L = Tilst
O = Kompromisløs
OU = IT
CN = happy.dev

[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = happy.dev
DNS.2 = squidex.happy.dev
Enter fullscreen mode Exit fullscreen mode

Generate Private key: openssl genrsa -out happy.dev.key 2048
Generate Self-Signed Certificate: openssl req -x509 -new -key happy.dev.key -sha256 -days 365 -out happy.dev.crt -config req.cnf -extensions v3_req

chmod 644 happy.dev.crt
chmod 600 happy.dev.key
Enter fullscreen mode Exit fullscreen mode

Trust the Certificate in macOS Keychain

To make browsers on your Mac trust your self-signed certificate, you need to add the certificate to your macOS system keychain and then explicitly set its trust level.

Most web browsers on macOS, including Chrome, Safari, and Firefox (by default), rely on the certificates stored in the system's Keychain Access utility.

Here is the step-by-step guide to get your browser to trust the happy.dev certificate:

Step 1: Open Keychain Access
Open Spotlight Search by pressing Cmd + Space.

Type Keychain Access and press Enter.

Step 2: Import the Certificate
In the Keychain Access application, make sure you have the login or System keychain selected on the left sidebar.

Go to the menu bar and select File > Import Items....

Navigate to the location of your happy.dev.crt file, select it, and click Open.

You may be prompted to enter your administrator password to add the certificate to the keychain.

Step 3: Set the Trust Level
In Keychain Access, find your imported certificate. It should be listed under its common name, which is likely happy.dev or a similar identifier.

Double-click the certificate to open its details window.

Expand the Trust section.

You will see a dropdown menu labeled "When using this certificate:". By default, it will say "Use System Defaults."

Change this dropdown to "Always Trust."

Close the window. You will be prompted to enter your administrator password again to save the changes.

Step 4: Restart Your Browser
After you've set the trust level, it's a good practice to completely quit and restart your web browser (e.g., Safari, Chrome, or Firefox). This ensures that the browser reloads the certificate trust settings from the system keychain.

Now, when you navigate to https://happy.dev, your browser should recognize the certificate as valid, and you will no longer see the "Your connection is not private" or security warning pages.

Update your hostfile

sudo vi /etc/hosts

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
....
127.0.0.1       happy.dev www.happy.dev        <-- ADD
Enter fullscreen mode Exit fullscreen mode

Use a script to add cert

All the above steps can be automated using this script:

#!/bin/bash

# Example
# bash> setup-certs.sh happy.dev 365

# Default values
CERT_NAME="${1:-localhost}"
DAYS="${2:-365}"
CERT_DIR="${3:-.}"

# Generate private key
openssl genrsa -out "$CERT_DIR/$CERT_NAME.key" 2048

# Generate self-signed certificate
openssl req -x509 -new -key "$CERT_DIR/$CERT_NAME.key" -sha256 -days $DAYS \
  -out "$CERT_DIR/$CERT_NAME.crt" -config req.cnf -extensions v3_req

# Set proper permissions
chmod 644 "$CERT_DIR/$CERT_NAME.crt"
chmod 600 "$CERT_DIR/$CERT_NAME.key"

# Add certificate to macOS Keychain (macOS only)
if [[ "$OSTYPE" == "darwin"* ]]; then
  echo "Adding certificate to macOS Keychain..."
  sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$CERT_DIR/$CERT_NAME.crt"

  # Add domains to /etc/hosts
  echo "Adding domains to /etc/hosts..."
  HOSTS_ENTRY="127.0.0.1 $CERT_NAME squidex.$CERT_NAME"
  if ! grep -q "$CERT_NAME" /etc/hosts; then
    echo "$HOSTS_ENTRY" | sudo tee -a /etc/hosts > /dev/null
    echo "Added: $HOSTS_ENTRY"
  else
    echo "$CERT_NAME already in /etc/hosts"
  fi
fi

echo "✅ Certificate created!"
echo "Certificate: $CERT_DIR/$CERT_NAME.crt"
echo "Key: $CERT_DIR/$CERT_NAME.key"
Enter fullscreen mode Exit fullscreen mode

If the file doesn't have execute rights:
bash setup-certs.sh happy.dev 365

I ran it from .devcontainer/ssh/

Notes for Linux users

host.docker.internal doesn't work out of the box like it does on macOS. You need to add this to the docker-compose.yml under the nginx service:

extra_hosts:
  - "host.docker.internal:host-gateway"
Enter fullscreen mode Exit fullscreen mode

Conclusion

We now have a professional-grade development setup that eliminates the need for localhost entirely. By routing traffic through a reverse proxy with valid SSL handling, we have achieved production parity on our local machines.

Is this more effort than simply running a dev server on a random port? Yes. But the trade-off is significant:

Zero "Magic" Code: You no longer need if (development) { ... } blocks to handle URLs or protocol differences.

Security First: You catch CORS, HSTS, and SSL issues during development rather than at 2 AM during a production deployment.

Skill Mastery: You've moved beyond being a "local-only" developer to understanding the networking layers that power modern web applications.

Top comments (0)