|
||
---|---|---|
docs | ||
probe | ||
static | ||
templates | ||
.gitignore | ||
.gitlab-ci.yml | ||
LICENSE | ||
README.md | ||
TODO | ||
ca.conf | ||
cert.go | ||
docker_watcher.go | ||
event_dispatcher.go | ||
flake.lock | ||
flake.nix | ||
go.mod | ||
go.sum | ||
localhostd.go | ||
localhostd.service | ||
logger.go | ||
main.go | ||
process_watcher.go | ||
ui.go | ||
usage.txt | ||
utils.go |
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
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