HoTCo:RE - painless reverse proxy

12 min read Original article ↗

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.comlocalhost:8080
  • https://b-app.comlocalhost: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 thezlib 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:

  1. 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.

  2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.

  3. 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

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 port8080, 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.