Skip to content

Repository files navigation

Edge plugin (Plugins\Edge, solves edge.routing)

Generates the host's web-server front config from the platform's registered domains, adapting to whatever is actually running on the machine.

It probes the host, picks a strategy, renders the matching config, then validates and reloads the server.

Supported platforms

Edge probes the host for what is installed and what is running, and every probe is platform-specific. What that means per OS:

Binary lookup "is it running" Start hint when it is installed but stopped
Linux command -v systemctl is-active, then pgrep -x sudo systemctl start …, or a generic line on a host without systemd (Alpine, OpenRC, containers)
macOS command -v pgrep -x (no systemd) sudo brew services start … where Homebrew is present, else generic
Windows where sc query, then tasklist net start …, qualified — see below

A note on macOS. brew install nginx leaves the service stopped, so strategy: none is the normal first result on a Mac, not a malfunction. Start the server and re-run; edge:apply names the exact command.

A note on Windows. Detection works, but neither nginx nor Apache is a Windows service unless it was installed as one (nssm, winsw, or Apache's own installer). tasklist therefore carries the common case — nginx.exe started by hand from its install directory — and the net start hint says plainly that it applies only to a real service. The paths in config/edge.php still assume a POSIX layout, so a Windows host must set EDGE_NGINX_PATH, EDGE_APACHE_PATH and the log directories explicitly; only EDGE_HOSTS_PATH defaults correctly (%SystemRoot%\System32\drivers\etc\hosts).

Strategy detection

Detected stack Strategy Rendered config
nginx and Apache active, nginx has the stream module nginx-stream nginx SNI (L4) stream splitter — listed domains → nginx (:444), everything else → Apache (:8443)
only nginx active (or Apache present but inactive, or nginx lacks stream) nginx-only plain nginx reverse-proxy vhost (no stream)
only Apache active apache-only Apache SSL VirtualHost
neither active none nothing — reports and stops

This is exactly the "check what's on the host and apply accordingly" rule: nginx is the front; if it can stream and Apache is up, split by SNI; if Apache is down, just nginx without stream; if only Apache, configure Apache.

Reusing (and updating) an existing nginx stream splitter

When both servers are running and the running nginx already declares an SNI stream {} splitter (a map $ssl_preread_server_name … { … } using ssl_preread, located from nginx -T, excluding Edge's own managed file), Edge does not write a second, conflicting splitter. Instead it:

  1. emits only the internal backend vhosts (TLS-terminating on :444), and
  2. merges the platform's public domains INTO your existing map in place — editing the host file (e.g. nginx.conf) where the splitter lives.

The merge is surgical and idempotent: your hand-written entries are left exactly as they are, and Edge's additions live inside a marked sub-block placed just before the default line. A domain already present anywhere in the map (yours or ours) is never re-added; re-runs never duplicate.

    map $ssl_preread_server_name $backend_name {
        migratetravel.com                  nginx_backend;   # your entries — untouched
        www.migratetravel.com              nginx_backend;
        # >>> HKM Edge (managed domains) >>>
        app.showmeuganda.com   nginx_backend;               # added by `edge:apply`
        admin.hkmvote.com      nginx_backend;
        # <<< HKM Edge (managed domains) <<<
        default                            apache_ssl;
    }

The upstream name the domains map to is nginx_backend by default (override with EDGE_STREAM_BACKEND to match your upstream { … }). Writing nginx.conf usually needs sudo — a failed write fails the whole apply loudly rather than reporting success. Disable this reuse/merge behaviour and always write Edge's own stream block with EDGE_REUSE_STREAM=0. Preview the exact map diff without touching anything using edge:apply --dry-run.

Forcing a single server (no fallback)

Auto-detection can be overridden to pin one server with no fallback:

hkm cli -p <project> edge:apply --nginx-only    # nginx serves everything, NO Apache fallback
hkm cli -p <project> edge:apply --apache-only   # Apache serves everything, no fallback

--nginx-only renders the plain nginx reverse-proxy vhost (no stream layer); --apache-only renders the Apache SSL VirtualHost. The same choice can be set as a deploy default with EDGE_FORCE_STRATEGY=nginx-only|apache-only. edge:status accepts the same two flags to preview the forced strategy without writing.

The SNI stream splitter (the nginx-stream output)

stream {
    upstream nginx_backend { server 127.0.0.1:444; }
    upstream apache_ssl    { server 127.0.0.1:8443; }

    map $ssl_preread_server_name $backend_name {
        app.example.com   nginx_backend;
        ...
        default apache_ssl;
    }

    server {
        listen 443;
        proxy_pass $backend_name;
        ssl_preread on;
    }
}

ssl_preread reads the TLS ClientHello's SNI without decrypting, then the raw TLS stream is forwarded to whichever backend the map picked. TLS is terminated by that backend (nginx on :444, Apache on :8443) — the stream layer never sees plaintext, so certificates live on the backends.

This strategy writes TWO files, at two different nginx contexts

stream {} and server {} are not interchangeable — included at the main context nginx refuses server, inside http {} it refuses stream — so the splitter and the vhosts behind it are separate files:

File Default Include it
the stream {} splitter EDGE_STREAM_PATH at the main context, top level of nginx.conf
the backend vhosts (TLS on :444) EDGE_NGINX_PATH inside http { … }, like any vhost file
# /etc/nginx/nginx.conf
include /etc/nginx/hkm-edge-stream.conf;          # main context — the splitter
http {
    include /etc/nginx/sites-enabled/hkm.conf;    # http context — the vhosts
}

edge:apply prints both paths with their contexts, writes them together, and rolls both back if the config test fails. When an existing splitter is reused (above) only the vhost file is written.

The single-server strategies (nginx-only, apache-only) write one file.

Commands

By default every command scopes to the current project (read from base_path()/proj.json — i.e. the project you run it in). Add --all to act on every registered project in the global projects.json.

hkm cli -p <project> edge:status         # probe host; show THIS project's plan
hkm cli -p <project> edge:status --all   # every registered project
hkm cli -p <project> edge:apply          # render + write config + sync /etc/hosts + reload
hkm cli -p <project> edge:apply --dry-run   # print what WOULD be written
hkm cli -p <project> edge:apply --no-reload # write only; skip validate + reload
hkm cli -p <project> edge:apply --no-hosts  # skip the /etc/hosts sync
hkm cli -p <project> edge:apply --all       # render ALL projects into one file
sudo hkm cli -p <project> --dev edge:hosts   # sync THIS project's local domains → /etc/hosts
sudo hkm cli -p <project> --dev edge:hosts --remove   # remove the HKM-managed block
hkm cli -p <project> edge:hosts --dry-run --force     # preview outside dev mode

Notes:

  • --dev makes hkm use your dev kernel checkout — and is required for edge:hosts (see the /etc/hosts rules below).
  • sudo is needed to write /etc/hosts (and /etc/nginx in production).

Per-project serving (the vhost model)

Edge is project-aware: it reads the global registry (projects.json → name/path/domains) and renders one vhost per project, with:

  • docroot = <project path>/app/public (never the project root — keeps .env/config/src/vendor out of the web tree), modeled on templates/app/{nginx,apache}.conf.example;
  • the run-env injected so the served project boots (FPM workers don't inherit your shell/hkm env) — as fastcgi_param (nginx) / SetEnv (Apache). Edge passes through the kernel-resolution env the launcher already exported for the active context — it doesn't derive or configure it. hkm … --dev carries HKM_DEV_HOME + the checkout's HKM_KERNEL_HOME / PSP_GLOBAL_AUTOLOAD; a live hkm cli carries the installed kernel's paths. The pass-through set is kernel_env_keys (default HKM_KERNEL_HOME, HKM_DEV_HOME, HKM_USERDATA_DIR, PSP_GLOBAL_AUTOLOAD, PSP_PROJECTS_DIR) — only the ones actually set in the environment are written. Plus APP_ENV. Set EDGE_INJECT_KERNEL_ENV=false to skip all of them;
  • a serve model per project: fpm (fastcgi to PHP-FPM) or swoole (reverse-proxy to the project's OpenSwoole port).

Each project may override the model + upstream + extra env in its proj.json:

{
  "name": "shop",
  "edge": {
    "serve":  "swoole",          // or "fpm"
    "port":   9601,              // swoole upstream port
    "socket": "unix:/run/php/php8.4-fpm.sock",  // fpm socket (fpm model)
    "env":    { "APP_ENV": "production", "SHOP_FLAG": "1" }   // per-project extras
  }
}

Defaults come from EDGE_SERVE_MODEL / EDGE_FPM_SOCKET / EDGE_SWOOLE_HOST / EDGE_SWOOLE_BASE_PORT.

Domains — public vs local

Collected from the current project's proj.json domains[] (or every registered project with --all), plus EDGE_EXTRA_DOMAINS, minus EDGE_EXCLUDE_DOMAINS. Every hostname is validated against a strict charset before it can reach a rendered config, so a malformed entry can never inject directives.

Domains are then split:

  • Public (real FQDN, e.g. app.example.com) → go into the server config (nginx stream / vhost / Apache).

  • Local (*.local, *.test, *.localhost, *.example, *.invalid, or a single-label host like myapp) → are dev-only: kept OUT of the public server config and written to /etc/hosts pointing at the loopback, so they resolve on this machine. The managed block is delimited by markers, so the rest of your hosts file is never touched and re-runs are idempotent:

    # >>> HKM Edge (local domains) >>>
    127.0.0.1    api.hkm.local
    127.0.0.1    hkm.local
    # <<< HKM Edge (local domains) <<<
    

Tune the local TLD set with EDGE_LOCAL_TLDS. In dev mode (hkm … --dev, which exports HKM_DEV=1) the local domains are served by the vhost automatically — they appear in BOTH the server config and /etc/hosts — so hkm cli -p <p> --dev edge:apply gives you a working local nginx/Apache site with no extra flag. A production (non --dev) run keeps them OUT of the server config (public domains resolve through DNS); set EDGE_LOCAL_IN_SERVER=true to force local-in-server outside dev too.

/etc/hosts rules — dev only, never duplicates

1. Requires dev mode. /etc/hosts is a developer-machine concern: a live server resolves its public domains through DNS. So the hosts sync only runs when the launcher marks the invocation as dev (hkm … --dev, which exports HKM_DEV=1). Outside dev:

  • edge:hosts refuses with a clear message (override with --force),
  • edge:apply silently skips the hosts step — a VPS run never touches /etc/hosts.
sudo hkm cli -p myproject --dev edge:hosts       # ✓ writes the block
hkm cli -p myproject edge:hosts                  # ✗ refuses (not dev mode)
hkm cli -p myproject edge:hosts --force          # ✓ explicit override

Writing /etc/hosts needs root, so use sudo. The launcher reads your config.env via SUDO_USER, so sudo hkm … --dev still finds HKM_DEV_HOME.

2. Existing entries win — a host is never duplicated. Before writing, every domain is checked against the rest of the hosts file (outside the managed block, comments and multi-host lines handled). A hostname already mapped there is skipped and left untouched; only genuinely missing ones are added:

already in /etc/hosts (left untouched): hkm.local
Would write 2 new local domain(s) to /etc/hosts:
127.0.0.1    api.hkm.local
127.0.0.1    app.hkm.local

Re-runs stay idempotent, and --remove drops the managed block (never your own entries).

Configuration (config/edge.php, all env-driven)

Env Default Purpose
EDGE_LISTEN_PORT 443 public TLS port
EDGE_NGINX_BACKEND 127.0.0.1:444 nginx TLS backend (stream)
EDGE_APACHE_BACKEND 127.0.0.1:8443 Apache fallback backend (stream)
EDGE_APP_BACKEND 127.0.0.1:8080 app upstream (nginx-only / Apache)
EDGE_SSL_CERT / EDGE_SSL_KEY (per platform) cert used by nginx-only / Apache templates — /etc/ssl/{certs,private} where that layout exists, else the server's own config dir
EDGE_NGINX_LOG_DIR / EDGE_APACHE_LOG_DIR (auto) per-site log directory. Auto-detected from nginx -V / apachectl -V, then the platform's conventional location; when nothing resolves the log directives are omitted rather than pointing at a directory that does not exist
EDGE_HTTP2 auto HTTP/2 spelling: auto picks what the installed nginx understands (http2 on; needs 1.25.1+, older builds need listen … ssl http2), on | listen pin one, off disables
EDGE_SYSTEMD_DIR / EDGE_SUPERVISOR_DIR (auto) where edge:service --write puts the unit; empty when neither manager is installed, and the command then asks for --write=<dir>
EDGE_STREAM_PATH / EDGE_NGINX_PATH / EDGE_APACHE_PATH var/edge/*.conf where each config is written (point at /etc/nginx/... in prod)
EDGE_FORCE_STRATEGY (empty) pin a single server — nginx-only | apache-only (no fallback); empty = auto-detect
EDGE_REUSE_STREAM true reuse an existing nginx stream {} splitter instead of writing a second one
EDGE_RELOAD false reload after write by default (also controllable per-command)
EDGE_*_TEST_CMD / EDGE_*_RELOAD_CMD nginx -t, nginx -s reload, apachectl configtest, apachectl graceful validate/reload commands per distro
EDGE_EXTRA_DOMAINS / EDGE_EXCLUDE_DOMAINS — comma-separated add/drop
EDGE_LOCAL_TLDS local,test,localhost,example,invalid TLDs treated as local (→ /etc/hosts)
EDGE_MANAGE_HOSTS true write local domains to /etc/hosts on apply
EDGE_HOSTS_PATH / EDGE_HOSTS_IP (per platform) / 127.0.0.1 hosts file + loopback target
EDGE_LOCAL_IN_SERVER false also include local domains in the server config
EDGE_SERVE_MODEL fpm default serve model (fpm | swoole); per-project override in proj.json
EDGE_FPM_SOCKET (auto) pin the FPM socket/addr; empty = auto-resolve the socket matching the CLI PHP version
EDGE_SWOOLE_HOST / EDGE_SWOOLE_BASE_PORT 127.0.0.1 / 9500 Swoole upstream host + base port
EDGE_INJECT_KERNEL_ENV true inject PSP_GLOBAL_AUTOLOAD + HKM_KERNEL_HOME into each vhost
EDGE_APP_ENV APP_ENV or production APP_ENV written into each vhost

Defaults write to var/edge/ so no root is needed to test; in production point EDGE_*_PATH at the real nginx/Apache include dirs and run hkm with the privileges needed to reload.

Notes

  • ON-DEMAND module; the value is the CLI. A route that needs the contract declares "requires": ["edge.routing"].
  • Writes are atomic (temp file + rename), so a live include never sees a half-written file.
  • The service is DI-free (collaborators read edge_config()), so it constructs without ports or a database.

About

HKM Kernel plugin: edge.routing

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages