The Hoody Proxy
Section titled “The Hoody Proxy”Every URL you’ve been using? The proxy makes it work.
The Hoody Proxy is the gateway between the outside world and your containers. It handles HTTPS certificates, routes requests to the right service, manages permissions, and preserves real client IPs. All automatically. Every program you run gets HTTPS, HTTP/2, and HTTP/3 (QUIC) — out of the box. No configuration. No cert rotation. No Let’s Encrypt dance.
You will never think about a certificate again in your life. It just works.
What the Proxy Does
Section titled “What the Proxy Does”-
Automatic HTTPS — Wildcard TLS certificates for every container URL. No Let’s Encrypt setup, no cert rotation, no DNS challenge.
-
URL routing — Parses
{projectId}-{containerId}-{service}-{instance}.{serverName}.containers.hoody.icufor Kit services, and{projectId}-{containerId}-http-{port}for anything you started yourself, then routes to the right process inside the right container. -
Permission enforcement — Authentication (JWT, password, IP whitelist, bearer token) checked before any request reaches your container.
-
Real client IP — Uses netfilter hooks to preserve the real client IP address. Your application sees the actual visitor, not a proxy address.
-
Protocol support — HTTP/1.1, HTTP/2, HTTP/3 (QUIC), and WebSocket upgrades. Real-time terminals and displays work seamlessly.
How URLs Route
Section titled “How URLs Route”When you hit:
https://abc123-def456-terminal-1.node-us-1.containers.hoody.icu/api/v1/terminal/executeThe proxy:
- Extracts
abc123(project),def456(container),terminal-1(service + instance) - Looks up
node-us-1to find the physical server - Routes to
terminalservice instance1inside containerdef456 - Forwards the request with real client IP preserved
All in milliseconds. No configuration on your part.
Your Own Programs Get URLs Too
Section titled “Your Own Programs Get URLs Too”Kit services have names. Your programs get ports — and the proxy speaks that too.
Start anything that listens on a TCP port, then put http-{port} where the service name would go. That’s the whole feature.
# Start a dev server on port 3000 inside your containerhoody terminal sessions exec -c $CONTAINER_ID --terminal-id 1 \ --command "nohup python3 -m http.server 3000 >/tmp/dev.log 2>&1 &"
# It's already on the public internet, with HTTPScurl https://$PROJECT_ID-$CONTAINER_ID-http-3000.$SERVER_NAME.containers.hoody.icu/// Start a dev server on port 3000 inside your containerawait box.terminal.execution.execute({ command: 'nohup python3 -m http.server 3000 >/tmp/dev.log 2>&1 &', wait: true}, { terminal_id: '1' });
// It's already on the public internet, with HTTPSconst url = `https://${PROJECT_ID}-${CONTAINER_ID}-http-3000.${SERVER_NAME}.containers.hoody.icu/`;console.log(await (await fetch(url)).text());# Start a dev server on port 3000 inside your containercurl -X POST \ "https://$PROJECT_ID-$CONTAINER_ID-terminal-1.$SERVER_NAME.containers.hoody.icu/api/v1/terminal/execute?terminal_id=1" \ -H "Content-Type: application/json" \ -d '{"command": "nohup python3 -m http.server 3000 >/tmp/dev.log 2>&1 &", "wait": true}'
# It's already on the public internet, with HTTPScurl https://$PROJECT_ID-$CONTAINER_ID-http-3000.$SERVER_NAME.containers.hoody.icu/No EXPOSE. No port mapping. No ingress controller. No certificate. The process started listening and the URL started working.
Any port from 1 to 65535 works, and one container can serve as many as you like at once:
https://{projectId}-{containerId}-http-3000.{serverName}.containers.hoody.icu (frontend)https://{projectId}-{containerId}-http-5000.{serverName}.containers.hoody.icu (backend)https://{projectId}-{containerId}-http-8000.{serverName}.containers.hoody.icu (admin)http- or https-?
Section titled “http- or https-?”Both give your visitor HTTPS — that never changes. The prefix describes the inside leg, from the proxy to your process:
http-3000— the proxy speaks plain HTTP to your process. This is what you want for almost everything: your dev server doesn’t need a certificate, because the proxy already presented one.https-3000— the proxy opens a second TLS connection to your process. Use this only when your program is itself serving TLS on that port.
Point https- at a plaintext server and the handshake fails — that mismatch is the usual cause of a working port that won’t load.
Ports you can’t take
Section titled “Ports you can’t take”Kit services occupy their own ports inside every container, and a raw-port URL can’t be used to sneak past their permissions. Ask for a port that belongs to terminal, files, daemon, code, or sqlite and the request is judged by that service’s permission rule, not your http rule. The agent daemon and the shared display server are refused outright. Everything else — 3000, 5000, 8080, whatever you picked — is yours.
Custom Domain Aliases
Section titled “Custom Domain Aliases”Tired of sharing a URL stuffed with Project and Container IDs? Create an alias — a memorable name (3-61 chars, a-z, 0-9, hyphens; cannot start or end with a hyphen; containers is reserved as an exact name, and names equal to or starting with proxy / workspaces (e.g. proxy-app) are rejected) that resolves to a short, clean URL:
# Create a proxy aliashoody proxy create \ --container-id $CONTAINER_ID \ --program "exec" \ --alias "my-api" \ --index 1
# ...or alias your own app instead of a Kit servicehoody proxy create \ --container-id $CONTAINER_ID \ --program http \ --port 3000 \ --alias "my-app"const alias = await client.api.proxyAliases.create({ container_id: CONTAINER_ID, program: 'exec', alias: 'my-api', index: 1});// Now https://my-api.{server}.containers.hoody.icu → your exec scriptscurl -X POST https://api.hoody.icu/api/v1/proxy/aliases \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "container_id": "'$CONTAINER_ID'", "program": "exec", "alias": "my-api", "index": 1 }'Permissions
Section titled “Permissions”By default, container URLs are accessible by anyone who has the URL. The URL itself is unguessable (48+ characters of hex), which provides a baseline of security.
When you’re ready to lock things down:
# Require password authenticationhoody containers proxy permissions replace -c $CONTAINER_ID \ --if-match file:v<N> \ --project $PROJECT_ID \ --groups auth='{"type": "password", "username": "dev", "password": "my-secret", "algorithm": "sha256", "salt": "unique-salt"}' \ --permissions auth='{"terminal": true, "files": true, "display": true, "http": [8080]}'
# Restrict to specific IP addresseshoody containers proxy permissions replace -c $CONTAINER_ID \ --if-match file:v<N> \ --project $PROJECT_ID \ --groups office='{"type": "ip", "range": "203.0.113.10/32"}' \ --permissions office='{"terminal": true, "files": true, "display": true, "http": [8080]}'// Require password authawait client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { devs: { type: 'password', username: 'dev', password: 'my-secret', algorithm: 'sha256', salt: 'unique-salt' } }, permissions: { devs: { terminal: true, files: true, display: true, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v<N>' });
// IP restrictionawait client.api.proxyPermissionsContainer.replace(CONTAINER_ID, { project: PROJECT_ID, container: CONTAINER_ID, groups: { office_primary: { type: 'ip', range: '203.0.113.10/32' }, office_subnet: { type: 'ip', range: '198.51.100.0/24' } }, permissions: { office_primary: { terminal: true, files: true, display: true, http: [8080] }, office_subnet: { terminal: true, files: true, display: true, http: [8080] } }, default: 'deny'}, { ifMatch: 'file:v<N>' });# Set password authcurl -X PATCH https://api.hoody.icu/api/v1/containers/$CONTAINER_ID/proxy/permissions \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "If-Match: file:v<N>" \ -d '{"project":"'$PROJECT_ID'","container":"'$CONTAINER_ID'","groups":{"devs":{"type":"password","username":"dev","password":"my-secret","algorithm":"sha256","salt":"unique-salt"}},"permissions":{"devs":{"terminal":true,"files":true,"display":true,"http":[8080]}},"default":"deny"}'One permissions document declares reusable auth groups (password, JWT, IP, token) and grants per-program access to them. Add a program in the permissions.<group>.<program> map to let that group in; anything not granted stays at the default policy. Configure once, apply it across whichever services need protection.
The Philosophy: Open by Default
Section titled “The Philosophy: Open by Default”URLs are unguessable. Sharing requires knowing the URL. This means:
- Development: No auth friction. Just build.
- Collaboration: Share the URL. Everyone’s in.
- Production: Add authentication when you’re ready.
No premature security configuration slowing you down. No “I can’t access the dev environment” tickets.
The proxy is invisible when you don’t need it, and bulletproof when you do.
Next: Your First API →