HoTCo:RE
The easiest, simplest way to serve multiple domains from the same server
HoTCo:RE is an HTTP(S) reverse proxy that routes requests based on the domain name.
- Painless install
- Zero configuration
- Out of the box HTTPS
- Cool name!
Problem
You have multiple web apps running on the same machine.
You want to route requests based on the domain name. e.g.:
https://a-app.com
→localhost:8080
https://b-app.com
→localhost:9000
DNS entries already set up & point to your server.
Now what?
You can spend time to learn how to setup & configure a tool like nginx, HAProxy, Traefik, or maybe Caddy?
You can ask ChatGPT or Claude to guide you through the process.
Or you can just use HoTCo:RE
Quick Start
Step 1: Install
Login to your server and run the following command:
$ curl -fsSL https://judi.systems/hotcore/install.sh | bash
In about 5 seconds, hotcore will be up and running!
Step 2: Add domain mappings:
$ hotcore add a-app.com 8080
$ hotcore add b-app.com 9000
That's it! You can now access your apps at https://a-app.com
and https://b-app.com
.
You're basically done. There's really not much else to learn or configure.
Notes
- HoTCo:RE is installed as a
systemd
service:- Starts automatically on boot
- Restarts automatically if it crashes (though it shouldn't)
- SSL certificates are managed automatically:
- Provisioned via Let’s Encrypt
- Renewed when needed
- WebSocket connections work out of the box!
- HoTCo:RE is only a domain-to-port reverse proxy
- Does not serve static files, let alone php or CGI scripts
Philosophy
HoTCo:RE is part of our effort to make self-hosting easy and painless.
That's why it is designed to be low touch & hands off; just fire and forget.
Web infrastructure is already very complicated. Complex systems cause headaches. Every piece in the system is a potential point of failure.
The request routing engine should be mostly invisible, just like the kernel, or the TCP/IP stack; you never have to think about them.
Small self-contained binaries are preferred to complex systems with external dependencies. We don't like to ship things as "Docker" images or scripts that require the installation of an interpreter and package manager.
We prefer not having to edit config files, because that would make the system difficult to automate in a reliable manner. It's also easy to make syntax errors but difficult to report on them.
Being programmable and driven by commands makes it easy to automate, which is cruicial for enabling software that makes self-hosting simple and easy.
License
HoTCo:RE is open source software and is distributed under the terms of the zlib/libpng license.
Copyright (c) 2025 Judi Systems
This software is provided ‘as-is’, without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
This notice may not be removed or altered from any source distribution.
Programmatic interface
HoTCo:RE can be "configured" by sending it messages (commands) either via the command line or via UDP port 40608.
The CLI is meant for shell script or interactive sessions.
The UDP interface is meant for programs.
Add a domain → port mapping
- CLI:
hotcore add <domain> <port>
- UDP:
add <domain> <port>
Remove a domain
- CLI:
hotcore remove <domain>
- UDP:
remove <domain>
List current mappings
- CLI:
hotcore list
- UDP:
list
Full listing
Full list of CLI commands and UDP messages are available by running:
hotcore help
Below is a reproduction of the list
Run without a command to report system status.
install Installs the system service and launches it.
Requires sudo / root permissions.
add <domain> <port>
Manually add a mapping entry
remove <domain>
Manually remove a mapping entry
list
List the domain-to-port mapping entries
msg <msg> Send a message to the HoTCo:RE server via the UDP port,
and print the response.
See below for a list of possible messages
log Tails the log file (runs 'tail -f' on the log file)
help Print this help menu
version Print version number and exit
license Print the license text and exit
credits List open source libraries used along and their licenses
serve Launch the routing engine. Meant for use by the system launcher.
Messages you can send over the UDP port 40608 (or manually with the "msg" command):
add <domain> <port>
Map the given <domain> to the given <port>.
remove <domain>
Remove the given <domain> from the mapping entries.
list
Responds with a listing of the domain-to-port mapping entries
check
Health check. Responds with "kcehc"
version
Responds with the version number
System Requirements
- Linux Server
- User with sudo access
- SystemD based distribution, such as:
- Fedora / RedHat / Alma / CentOS
- Debian / Ubuntu
Download
Version: v0.1-beta4
Binary: https://judi.systems/hotcore/hotcore-linux-amd64-v0.1-beta4.tar.xz
Source: https://judi.systems/hotcore/hotcore-src-v0.1-beta4.tar.xz
Installation
Download the latest binary and run it with the install
sub-command with root permissions.
$ sudo ./hotcore-linux-amd64-v0.1-beta4 install
As mentioned in the quick start section, we provide a bash script that downloads the latest version and installs it.
$ curl -fsSL https://judi.systems/hotcore/install.sh | bash
Please DO NOT automate the download into your CI/CD pipeline.
The whole thing should not take more than a few seconds. The only bottlenecks:
- Network download
- SystemD config
When it completes, the latest version of HoTCo:RE will be installed as a system service, and it will also already be up and running.
The install command is idempotent: it's safe to call multiple times.
It's also safe to call even if you have an older version installed; it will just upgrade it.
Here's what the install command does:
- Create a user
hotcore
if it does not exist - Create directory
/opt/hotcore
, if it does not exist, and assign it to userhotcore
- Install the program binary to
/opt/hotcore/bin
- Provision capability to listen on port 80 and 443 without root permissions
using
setcap CAP_NET_BIND_SERVICE=+eip
. - Unblock ports 80 and 443 from the system firewall. Supported firewalls:
ufw
firewall-cmd
- Install a symlink to
/usr/local/bin/
- Create a system wide SystemD service:
- Service runs under non-root user
hotcore
- Service is started immediately
- Configured to start on system boot
- Configured to restart if crashed
- Service runs under non-root user
As we alluded to in the Philosophy section, HoTCo:RE is a self-contained binary. It's not a wrapper around some other tool.
Building from source
HoTCo:RE is implemented in Go.
The source code is vendored, you should be able to build it as-is without downloading any packages from the internet.
Make sure to pass the -tags=release
argument.
go build -tags=release .
For reference, this is the actual full build command we use:
GOWORK=off GOOS=linux GOARCH=amd64 go build -ldflags "-w -s" -tags=release -o $binpath .
Local mode
If you build this program without any build tags, it will run in "local dev mode".
- You still need to run
sudo ./hotcore install
for the local build. - HTTPS support requires
mkcert
. If it's not detected, it will fall back to HTTP. - No special
hotcore
user is created.
We recommend using domains of the form app.localhost
, since they work out of
the box, without you have to edit the system's hosts
file.
How to send UDP messages
Sending UDP messages is a very simple and effective way to send messages between processes running on the same machine.
Very lightweight, compared to a full blown JSON API.
Since UDP isn’t common in web development, here are examples of how to send a string message and wait for the response in popular languages.
Go
import "net"
import "time"
func SendUDPMessage(port int, msg string) (string, bool) {
conn, _ := net.DialUDP("udp", nil, &net.UDPAddr{Port: port})
defer conn.Close()
conn.Write([]byte(msg))
conn.SetReadDeadline(
time.Now().Add(time.Millisecond * 100)) // 100 ms read timeout
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
return string(buffer[:n]), err == nil
}
Javascript (node.js)
const dgram = require('dgram');
function sendUDPMessage(port, message) {
return new Promise(resolve => {
const socket = dgram.createSocket('udp4');
socket.on('message', (msg) => resolve(msg.toString()));
const cleanup = () => { socket.close(); resolve(null) }
socket.send(message, port, 'localhost',
() => setTimeout(cleanup, 100));
});
}
PHP
function send_udp_message(int $port, string $message): ?string {
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
// 100ms timeout for reading
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 0, 'usec' => 100000]);
socket_sendto($socket, $message, strlen($message), 0, '127.0.0.1', $port);
$buf = ''; $from = ''; $fromPort = 0;
$bytes = socket_recvfrom($socket, $buf, 1024, 0, $from, $fromPort);
socket_close($socket);
return $bytes === false ? null : $buf;
}
Python
import socket
def send_udp_message(port, message):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(message.encode(), ('127.0.0.1', port))
sock.settimeout(0.1)
data, _ = sock.recvfrom(1024)
return data.decode()
except socket.timeout:
return None
finally:
sock.close()
Ruby
require 'socket'
require 'timeout'
def send_udp_message(port, message)
socket = UDPSocket.new
begin
socket.send(message, 0, '127.0.0.1', port)
Timeout.timeout(0.1) do
data, _ = socket.recvfrom(1024)
return data
end
rescue Timeout::Error
return nil
ensure
socket.close
end
end
Q & A
What advantages does this have over nginx or Caddy?
nginx and Caddy can do a lot of things that HoTCo:RE does not do (and will not support).
However, neither of them is easy to control programmatically.
The most common use case for a reverse-proxy, which is to re-route requests based on the domain name, is very difficult to do programmatically in a reliable way. You have to manually edit config files, with a special syntax.
Caddy, although they have a JSON API, does not have a straightforward and reliable way to add a domain to port mapping through their API.
Why does HoTCo:RE use UDP? I heard UDP is unreliable!
We are sending short string messages to a UDP port on the local machine.
It's as reliable as you can get.
Yes, in theory it can fail under extreme conditions, but the same applies to memory allocations and disk I/O.
We take reliability seriously. If you find situations where HoTCo:RE fails, please report it to us!
One of the ways HoTCo:RE is more reliable than alternatives is it has fewer failure modes due to exposing a smaller surface area.
-
HoTCo:RE knows how to install itself with a single command, whereas other tools require you to run several commands and edit several files in a specific order.
-
HoTCo:RE does not ask you to edit config files. For example, in nginx, a single syntax error in a config file can break the entire system. Not so with HoTCo:RE!
-
HoTCo:RE's routing rules are very easy to reason about. There's only one type of rule: domain to port mapping.
Why have both a CLI interface and a UDP interface?
The UDP interface is the actual interface. The CLI interface is provided for convenience, and is implemented on top of the UDP interface.
For controlling HoTCo:RE programmatically, we strongly recommend using the UDP interface; the CLI interface should be left for shell scripts and interactive shell sessions.
When might a program want to use the UDP interface?
-
In a Red/Blue deployment strategy, the newly spawned instance can choose the right timing to tell HoTCo:RE to start routing traffic to itself instead of the previous version, while giving the previous instance some time to wind down in the background. A shell script might not be the right thing here if there is some delay between the time you invoke the add command via the shell script and the time it takes for the webapp to start up.
-
A web app can dynamically decide to serve new domains without the need to trigger the deployment process. For example, a static server can detect when a new folder is added, treat it as a domain name, and map the domain to itself.
How do you handle security?
Security is maintained by not accepting UDP messages from outside the system. Only programs running on the same machine can send messages to HoTCo:RE, so no one from outside the system can control it or subvert it.
In addition, HoTCo:RE does not run as root, instead it runs under a regular user 'hotcore' created during the installation process.
Threat Model
HoTCo:RE is assumed to run on a system controlled entirely by one person or entity.
-
Any program on the system can tell HoTCo:RE to map any domain to any port. This would be undesirable in a system with multiple users with different or competing interests. Linux is designed with that in mind; that's why it has a system of users, groups, and permissions.
-
You should only run programs that you trust. Even under regular Linux rules, any program you run can read off your private ssh keys and upload them to a malicious server, for example. Linux security is based on users, not programs. Any program you run will run under your username, with all of your permissions.
HoTCo:RE's threat model assumes attackers cannot gain access to your server machine, your VPS account or your DNS account.
If someone gains access to your server, they can "take over" your domain without needing root permissions. They can run their own reverse proxy that listens on port X, forwarding requests to their own malicious server, and responding to that.
This is already the case whether you use HoTCo:RE or not.
For example, if nginx is configured to forward requests for a-app.com
to port
8080
, an attacker who gains access to your server can simply run a process on
port 8080
. No root privileges are needed to hijack the domain.