Version: 0.1.0 Status: shipped on this Linux server Date: 2026-09-04
Hosting is a personal PaaS for this Linux server. You give it a GitHub repo (usually a static landing page or a Node web UI). Hosting clones it into a local folder, keeps a project-level .env, runs it on its own port, and can map a domain in front with Caddy (automatic HTTPS).
It is a control plane for one operator, not a multi-tenant product.
- Deploy many small sites and Node apps on one machine without writing systemd units or Caddy config by hand.
- Keep each project isolated by folder and port.
- Let the operator work from a localhost dashboard and a CLI.
- Make clone and deploy two explicit steps: submit clones; Deploy pulls, builds, and starts.
- Multi-user accounts, roles, or billing
- Authentication on the dashboard (it is localhost-only)
- Docker / arbitrary language runtimes
- Auto-deploy on every git push / GitHub webhooks
- Preview environments, PR apps, or rollbacks
- Databases, object storage, or secrets managers
- Changing DNS for the operator
One operator who controls this server (SSH, sudo, DNS). They open the dashboard over an SSH tunnel or run hosting on the machine.
| Topic | Choice |
|---|---|
| Where it runs | This Linux server |
| What a repo can be | Static site (install, optional build, serve dist/out/build or root index.html) or Node long-lived process |
| How you operate it | Dashboard + CLI |
| How repos arrive | Paste a clone URL; optional GitHub PAT for private repos |
| When a project updates | After Submit (clone) and a separate Deploy button / hosting deploy |
| Domains and HTTPS | Hosting owns Caddy; operator points DNS at the server |
| Who can open the dashboard | Localhost only, no auth |
- Binds
127.0.0.1:8787by default. Not exposed on the public interface. - Create a project, watch clone/deploy status, edit settings, edit
.env, read logs, start/stop/delete. - Settings: optional Let’s Encrypt ACME email.
Open from a laptop:
ssh -L 8787:127.0.0.1:8787 USER@THIS_SERVERSame operations as the dashboard, talking to the local API:
serve, list, add, deploy, start, stop, delete, logs, env, status, doctor
Project processes listen on 127.0.0.1:<port>. If a domain is set, Caddy reverse-proxies that hostname to the port and terminates TLS.
- Name: lowercase letters, numbers, hyphens; 1–63 chars; must start with a letter or number. Unique. Can be renamed later (moves
projects/<name>/; refused while cloning or deploying). - Git URL:
https://,http://,git@,ssh://,file://, or an absolute local path. - Type:
staticornode. - Port: integer 1024–65535, unique. Auto-assigned from 4100 if omitted.
- Domain: optional FQDN, unique across projects.
Branch, GitHub token, build command, start command, static output directory.
idle · cloning · building · running · stopped · error
Clone and deploy cannot overlap on the same project. Delete is refused while cloning or building.
$HOSTING_DATA_DIR/ # default: /home/luoxi23vr/hosting-data
hosting.json # settings + project records (token stored here)
caddy/Caddyfile # generated, then copied to /etc/caddy/Caddyfile
projects/<name>/
repo/ # git clone
.env # operator-owned; copied into repo on clone/deploy
logs/deploy.log
logs/runtime.log
Data lives outside the Hosting app tree so a project’s TypeScript does not pick up Hosting’s node_modules/@types. Override with HOSTING_DATA_DIR.
- Validate name, URL, type, domain, port.
- Allocate port, create folders, write empty
.env. - Return immediately; clone runs in the background.
- On success status is
stopped. Deploy is a separate action.
Private HTTPS GitHub repos use GIT_TERMINAL_PROMPT=0 plus an Authorization: Basic extra header from the PAT (x-access-token:<token>). The API never returns the token; it only returns gitTokenSet.
- Pull (
git fetch+ hard reset toorigin/<branch>), or clone if the folder is missing. - Copy
$HOSTING_DATA_DIR/projects/<name>/.envintorepo/.env. - Stop the current process.
- Install dependencies if
package.jsonhas any (npm/pnpm/yarn/bunfrom lockfiles). - static: run build if configured or
scripts.buildexists; serveoutputDirordist/out/build; else serve repo-rootindex.html. - node: optional build; start
startCommand, or anode …start script as-is, ornpm run start/package.json#main. - Inject
PORT,HOST=127.0.0.1,HOSTNAME=127.0.0.1, parsed.env, andCI=true(install/build) /NODE_ENV=production(runtime). - Wait until
127.0.0.1:<port>accepts connections before markingrunning.
Install/build timeout: 60 minutes. Port-ready wait: 60 seconds. If a command finishes successfully after the timer, Hosting still treats it as success.
- Start requires a previous successful deploy (
runtimeCommand+runtimeRoot). - Stop sends SIGTERM to the process group, then SIGKILL if needed.
- On control-plane shutdown, running projects are stopped but kept as
runningso they are started again on boot. - Unexpected process exit →
error(orstoppedif exit code 0).
On create, patch, delete, or ACME email change, Hosting writes a Caddyfile, caddy validates it, copies it to /etc/caddy/Caddyfile, and reloads Caddy.
Each domain block:
example.com {
encode gzip
reverse_proxy 127.0.0.1:<port>
}Hosting replaces the system Caddyfile. This machine’s public HTTP/HTTPS is whatever Hosting generated.
The operator must point DNS A/AAAA at this server before certificates can be issued. After deploy (and whenever a domain is set), the dashboard and CLI show the exact records, using this server’s public IP. ACME email is optional (Settings). Override the detected IP with HOSTING_PUBLIC_IPV4 / HOSTING_PUBLIC_IPV6 if needed.
Stored only under $HOSTING_DATA_DIR/projects/<name>/.env. Copied into the clone on clone and on every deploy. Editing .env does not restart the process; Deploy or Start is required to pick up changes.
Base: http://127.0.0.1:8787/api
| Method | Path | Notes |
|---|---|---|
| GET | /health |
Caddy status, bind, public IP, project count |
| GET/PATCH | /settings |
PATCH accepts acmeEmail |
| GET/POST | /projects |
POST creates and starts clone |
| GET/PATCH/DELETE | /projects/:name |
PATCH name renames the project and folder |
| POST | /projects/:name/deploy |
202, work in background; body includes dns records to add |
| POST | /projects/:name/start |
|
| POST | /projects/:name/stop |
|
| GET | /projects/:name/logs?kind=deploy|runtime |
tail, ~200 KB |
| GET/PUT | /projects/:name/env |
PUT body { "content": "…" } |
JSON errors: { "error": "…" } with 4xx/5xx.
- Process:
systemdunithosting.service(User=luoxi23vr,ExecStart=/usr/bin/node …/dist/server.js). - Dashboard bind:
HOSTING_HOST/HOSTING_PORTor settings (127.0.0.1:8787). - CLI default API:
HOSTING_URLorhttp://127.0.0.1:8787. - Requires:
git,node,npm,caddy, passwordlesssudofor Caddy reload.
- Dashboard and API are loopback-only. Do not bind
0.0.0.0without adding auth. - Git tokens live in
data/hosting.jsonin plaintext. File permissions follow the service user. - Project processes are not containerized; they run as the Hosting user.
- Generated Caddy config only reverse-proxies to
127.0.0.1:<project-port>. - Git URLs reject newlines; names/domains are constrained.
v1 is successful if the operator can:
- Add a public or PAT-gated GitHub repo from the dashboard or CLI.
- Set
.envand Deploy. - Reach a static site or Node app on
127.0.0.1:<port>. - Map a DNS name to that port through Caddy with HTTPS.
- Read deploy/runtime logs and stop/start/delete without logging into each app’s folder.
- Deploy-on-push webhooks
- Dashboard auth if it is ever bound publicly
- Docker / Python / other runtimes
- Releases and instant rollback
- Health checks beyond “port is open”