Instantly and automatically create subdomains for local dev servers.
Go to file
Joshua Reusch ca165d33a9 fix #1 - also set supplementary groups when dropping privileges 2023-11-30 14:59:50 +01:00
docs update readme 2023-09-01 17:08:00 +02:00
probe fix #1 - also set supplementary groups when dropping privileges 2023-11-30 14:59:50 +01:00
static WIP: rescan first try 2023-08-31 23:23:36 +02:00
templates WIP: rescan first try 2023-08-31 23:23:36 +02:00
.gitignore test websockets 2023-11-05 23:04:21 +01:00
.gitlab-ci.yml fix release assset links 2023-09-01 17:21:41 +02:00
LICENSE fix name in LICENSE 2023-11-21 21:51:09 +01:00
README.md ignore spotify 2023-11-30 14:24:51 +01:00
TODO WIP: fix docker stuff 2023-11-22 01:53:49 +01:00
ca.conf HTTPS support. 2023-11-05 18:29:20 +01:00
cert.go HTTPS support. 2023-11-05 18:29:20 +01:00
docker_watcher.go docker: fix ignoring certain ports #2 2023-11-30 14:22:24 +01:00
event_dispatcher.go Docker watcher, better handling of duplicates, sanitize domain name 2023-08-30 22:04:35 +02:00
flake.lock fix nixos build 2023-11-21 14:55:18 +01:00
flake.nix update README 2023-11-22 01:14:48 +01:00
go.mod .localhostd config files 2023-11-10 19:25:08 +01:00
go.sum .localhostd config files 2023-11-10 19:25:08 +01:00
localhostd.go .localhostd config files 2023-11-10 19:25:08 +01:00
localhostd.service example systemd service 2023-09-01 16:28:01 +02:00
logger.go refactor: extract process_watcher from localhostd 2023-08-28 20:10:58 +02:00
main.go fix #1 - also set supplementary groups when dropping privileges 2023-11-30 14:59:50 +01:00
process_watcher.go ignore spotify 2023-11-30 14:24:51 +01:00
ui.go always update before serving the UI 2023-09-06 13:10:10 +02:00
usage.txt blacklist, processes, docker options 2023-11-22 01:00:29 +01:00
utils.go docker configuration 2023-11-10 21:45:14 +01:00

README.md

localhostd is a simple reverse proxy server. It watches all your processes and docker containers, checks if they start an HTTP server, and then proxies from a subdomain derived from their working directory to them.

So instead of having http://localhost:8080, http://localhost:3000, http://localhost:1234, etc. you can run localhostd on port 80, and then use "real" domain names, like http://webrtc-share.localhost or http://serverless-htmx.localhost!

No more remembering port numbers, making sure they don't collide, or weird bugs because you re-used some of the ports between different projects!

Features

  • automatically create reverse proxies for local processes
  • automatically create reverse proxies for docker containers without publishing the port
  • automatically create HTTPS certificates for proxied domains
  • fuzzy match on subdomains
  • creates multiple subdomains each, based on the CWD and process name
  • combine with a local dnsmasq to use arbitrary TLDs
  • a simple UI that lists forwarded domains

screenshot

Installation

If you are using Nix/NixOS, you can use the flake in this repository directly. See below for instructions on how to use the flake.

localhostd ships a single, fully self-contained binary. Go to Releases to download the latest version!

While you can run localhostd just from your downloads folder, it is common to put binaries in /usr/local/bin or /opt/localhostd/bin for example.

You can also create a simple systemd service, to start localhostd by default. Put the following into a file called /etc/systemd/system/localhostd.service:

[Unit]
After=network-online.target
Description=localhostd

[Service]
Restart=always
# Change the path and add custom flags here!
ExecStart=/usr/local/bin/localhostd

Afterwards, you can enable and start the service using systemctl:

sudo systemctl enable localhostd.service
sudo systemctl start localhostd.service

NixOS / Flakes

If you are using NixOS, you can use the flake in this repository instead. It also provides a module that configures the systemd service for you.

# Note: this example is for illustration purposes,
# not everything is totally correct here!
# You can check out my own NixOS repository for a full example.
{
  # Add localhostd to your inputs:
  inputs = {
    # ...
    localhostd = {
      url = "gitlab:arkandos/localhostd";
      # make sure it uses your systems nixpkgs
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, localhostd, ... } @ inputs:
  {
    nixosConfigurations.default = lib.nixosSystem {
        system = "x86_64-linux";

        # register the module
        modules = [
          # register the module
          localhostd.nixosModule

          # maybe inside configuration.nix?
          {
            # enable the systemd service
            services.localhostd = {
              enable = true;
              # port = 80;
              # user = "usename";
              # domain = "localhost";
              # bindAll = true;
              # https = true;
              # caCert = "";
              # caKey = "";
            };

            # more system configuration...
            system.stateVersion = "23.05";
          }
        ];


    };
  };
}

HTTPS Support

localhostd also supports HTTPS, and can automatically create certificates for all proxied subdomains.

Starting localhostd with --https will start an HTTPS server instead of an HTTP server. If you did not specify a port, localhostd will also listen on port 443 by default, instead of port 80.

By default, localhostd will generate self-signed certificates by default. You also can provide a custom CA (certificate authority) private/public key pair. When doing so, localhostd will sign all generated certificates using the provided CA key pair. When you then also add this CA certificate to your OS configuration, all certificates generated by localhostd will be trusted by default, getting rid of that "This connection is insecure" error.

The full command might then might look like this:

$ localhostd --https --ca-cert rootCA.pem --ca-key rootCA-key.pem

Generating a new CA

The easiest way to generate a new certificate authority is to use mkcert. Mkcert will also automatically add your new CA to your system, such that it will be automatically trusted. If you don't want to use mkcert, you can also use openssl instead, to generate the required files manually:

# Generate the private key file
openssl ecparam -name prime256v1 -genkey -noout -out rootCA-key.pem

# Generate a new root CA certificate, using this private key and the template in this repository
openssl req -x509 -new -sha512 -nodes -key rootCA-key.pem -days 3650 -out rootCA.pem -config ca.conf

The ca.conf file in this repository automatically skips all prompts by openssl for you, setting the proper values on the certificate.

Adding the new CA to your system

If you used mkcert -install, mkcert already did this for you!

Remember that you will need to restart your browser after adding a new certificate.

NixOS

NixOS users can add the path to the public key to security.pki.certificateFiles, or the raw certificate string to security.pki.certificates.

Debian-based Linux

# copy the public key into the system certificate store
sudo cp rootCA.pem /usr/local/share/ca-certificates/localhostd.crt
# update ca-bundle
sudo update-ca-certificates

CentOS / Fedora / Arch Linux

The easiest way is to use the p11-kit scripts:

sudo trust anchor --store rootCA.pem

If you get a no configured writable location error, try adding it manually instead:

# copy the public key into the system certificate store
# Fedora:
sudo cp rootCA.pem /etc/pki/ca-trust/source/anchors/localhostd.pem
# Arch:
sudo cp rootCA.pem /etc/ca-certificates/trust-source/anchors/localhostd.pem

# update ca-bundle (same for both systems)
sudo update-ca-trust

Process configuration

When localhostd detects a new process, it scans the working directory and all parent directories for a file called .config/localhostd.ini or .localhostd.ini. Using this file, you can ignore certain processes, set custom domain names, or configure a custom timeout. The file follows the .ini format. The default section contains configuration for all processes, and additional sections can be added to target specific processes instead.

Once a matching configuration section is found, all other following sections (in the same file or upwards the tree) will be ignored.

You can disable watching and registering non-docker processes entirely by passing --processes=false.

Examples

Ignore all processes started in a directory (or any subdirectory):

# ini with mysql-like syntax for booleans, the value is optional!
# keep in mind that if another file is found closer to the CWD, it will override this.
ignore

Remap different processes to different domain names:

[php]
domain = app-backend

[node]
port = 5173
# multiple domains
domain = app-frontend, app
# on the first request, vite will compile our app
timeout = 60s

[node]
port = 4173
domain = app-preview

# ignore all processes started in the 'scripts' directory.
# you can target all processes by using the DEFAULT section explicitely.
[DEFAULT]
cwd = ./scripts
ignore = true

Different configuration per user:

[DEFAULT]
# only `www-data` is allowed to use localhostd
group = www-data

[DEFAULT]
# arkan is additionally allowed
user = arkan

# if no user matched, ignore everything
[DEFAULT]
ignore

Certain processes are also ignored by default. If you think a program you use should always be ignored, please feel free to open an issue or a pull request!

Internal Blacklist

Some process prefixes are ignored by default. These currently are:

code, steam, wineserver, Discord, spotify, psi, docker-proxy, containerd-shim.

These all open ports that respond correctly to HTTP requests, but we assume that these are never useful to proxy. If you think you know some more programs I should add to this list, or think I some entry is wrong or to broad, please feel free to open an issue!

You can disable the internal blacklist by passing --blacklist=false. To locally ignore some more processes, check out the process configuration above!

Option reference

Name Example Description
port 5173 This section only matches this port.
name node Equivalent to the section name - matches a prefix of the basename of the command.
cmd vite Matches if the command line contains this string
cwd ./tests Only match files in this directory. See below.
user 1001 Matches processes of this user id or name
group www-data Matches processes of this group id or name
ignore true If true, this process/port is ignored.
domain app, dev Configure a custom subdomain for this process/port
timeout 60s Configure a custom timeout for this process/port

cwd

You can use the cwd option to only target processes in a sub-directory. This allows you to hoist the configuration up, or just have a single $HOME/.config/localhostd.ini file. The base directory is the scope for this config file. For .localhostd.ini files, this means just the directory the config file is in. For .config/localhostd.ini files, the entire suffix is removed first - so a file in /var/www/.config/localhostd.ini would be allowed to target any directory in /var/www. This makes sure both config file locations are compatible and interchangeable with each other.

You cannot add configurations that would target a directory outside of this scope - in our example, you could not add a section targeting /home! This makes sure that the server can always reliably find all relevant config files, without having to look through your entire file system.

I encourage you to use the .config/localhostd.ini variant, even in individual projects. Our project root directories are commonly littered with tons of different .eslintrc, package.json, tsconfig.json, Dockerfile, yarn.lock, netlify.toml, and so many more config files. I hope that in the future we will be able to consolidate them all into a single .config directory, or whatever other name folks agree upon. For now, I'm gonna just do it instead!

Docker configuration

Docker containers can be configured using labels. You can set labels manually, or inside a docker-compose.yml file. All labels are prefixed with localhostd/ to avoid collisions.

You can disable watching docker containers entirely by passing --docker=false.

Label Example Description
localhostd/ignore true If set but empty, or set to true, ignore all. Otherwise, ignore the listed ports, separated by ,
localhostd/domain 8080=app,9000=db comma-separated list of domains, optionally with a port=domain to only use this domain for a certain port.
localhostd/timeout 60s Timeout for this container, or port=timeout pairs, separated by ,

So if you run

docker run -l localhostd/domain=webserver nginx

that nginx will be available at http://webserver.localhost!

Troubleshooting

VSCode Dev-Containers

devcontainers come with their own proxy mechanism, forwardPorts. VSCode will inject its own proxy into your container, and will open a local port, using its own process.

Unfortunately, this process is the same one for all open windows, so we cannot differentiate between different workspaces, and we cannot map the ports back to the containers they forward! For these reasons, I decided that I should ignore all ports that VSCode opens.

Instead, all ports that are exposed by docker containers are proxied. You can add an EXPOSE directive to the Dockerfile, use appPort in devcontainers.json, or add ports sections to the service definitions in your docker-compose.yml.

Keep in mind that the way VSCode injects itself into containers also solves a bunch of other issues, mainly that it can make proxied requests inside of the container to localhost. If you expose the ports using dockers own mechanisms, it looks like you try to access your app from a different computer.

To solve this, you need to make sure that your servers inside of docker containers listen on all network interfaces, usually by setting the bind adress to 0.0.0.0.

When using vite, You can pass the --host option:

npx vite dev --host

SvelteKit / CSRF

Some frameworks (like SvelteKit) will complain when submitting forms while using localhostd as a proxy. This is done as a security measure, to make sure that you cannot submit a form from a different domain than you would expect in production.

This option is enabled even on development builds! You can disable it like this:

// svelte.config.js
const config = {
  kit: {
    adapter: adapter(),
    csrf: {
      // only disable csrf check in development
      checkOrigin: process.env.NODE_ENV !== 'development'
    },
  }
}

openssl ecparam -name prime256v1 -genkey -noout -out rootCA-key.pem openssl req -x509 -new -sha512 -nodes -key rootCA-key.pem -out rootCA.pem -config ca.conf

openssl crl2pkcs7 -nocrl -certfile ca-bundle.crt | openssl pkcs7 -print_certs -noout | grep localhost