Custom domains
Point your own domain at a Cube with automatic HTTPS at the edge — add and remove domains, read real visitor IPs, and tune per-domain proxy and CORS settings.
Map a domain you own to a port on a Cube. Krova Cloud runs a reverse proxy in front of your Cube that terminates HTTPS, forwards requests to your app, and passes through the real visitor information your app needs.
Add a domain
- In your Cube's networking tab, choose Add Domain and enter the domain and the port your app listens on inside the Cube.
- At your DNS provider, add the record that page shows for the domain, with its exact target: a CNAME for a subdomain like
app.example.com. A root domain likeexample.comcannot have a plain CNAME, so the page shows what to add instead, depending on your provider: a flattened CNAME, an ALIAS, or address records. The records stay on the domain afterwards, each showing whether we can see it yet. - HTTPS is provisioned and renewed automatically. There are no certificates to manage.
A domain you already pointed at dns.krova.cloud keeps working as it is.
If your DNS is on Cloudflare, both modes work: DNS-only (grey cloud — Krova Cloud issues the certificate) or Proxied (orange cloud — Cloudflare's edge sits in front). If you use the orange cloud, you must set Cloudflare's SSL/TLS mode to Full — see Using Cloudflare.
Find which Cube holds a domain
Domains in the dashboard sidebar lists every domain in your space next to the Cube that serves it, with its current status. Select a domain to open that Cube's networking tab, where you change or remove it.
Wildcard domains
Add *.example.com once and every subdomain routes to your Cube — no separate mapping per subdomain. This is the right choice when your app creates subdomains itself, for example one per customer.
A wildcard covers one level of subdomain: app.example.com and api.example.com, but not example.com itself and not a.b.example.com. That is a rule of the certificate standard, not a Krova Cloud limit — add the root domain or a deeper name as its own domain if you need it.
Every subdomain shares one Cube, one port and one set of settings, because they are all one mapping. If a specific subdomain needs different settings, add it as a separate domain — an exact match always wins over the wildcard.
One certificate, many Cubes
A wildcard domain sends every subdomain to the same Cube. If you instead run one subdomain per Cube — say customer-a.example.com on one and customer-b.example.com on another — turn on Universal SSL on the claimed domain in space settings. Each subdomain keeps its own mapping and its own Cube, and they all share one certificate.
This matters once you add subdomains regularly. Certificate authorities cap how many certificates a single registered domain may be issued per week, and a busy account adding a subdomain at a time can reach that cap and wait for new subdomains to get HTTPS. One certificate for the whole domain removes the cap from the picture.
Turn it on when you claim the domain, or later from the claimed domain's card. It needs the same _acme-challenge CNAME a wildcard uses, shown next to the verification record as soon as you turn it on, so you can add both at once. Click Check record after adding it. We order the certificate once the domain is verified. Leave the record in place: we renew against it automatically, and removing it stops renewal.
Universal SSL and a wildcard domain for the same name are alternatives, not a pair. Turn on one or the other.
Add the domain first — all three records, with their exact values, then appear on it, each showing whether we can see it yet. Use Check DNS after you publish them.
Three DNS records, all added once and never changed again:
- TXT
_krova-verify.example.com— proves the domain is yours. A wildcard covers every subdomain, so we check ownership once, for the whole namespace. It does not add example.com itself as a domain. - CNAME
*.example.com→dns.krova.cloud, which routes the traffic. - CNAME
_acme-challenge.example.com→ the target shown on the domain — lets us issue and renew the HTTPS certificate.
The third record trips people up, three ways. First, your own * record does not cover it — DNS wildcards match at any depth, so without an explicit record your wildcard answers _acme-challenge with something that has no certificate information in it. Second, on Cloudflare it must be DNS only (grey cloud); a proxied record returns Cloudflare's own addresses and the certificate authority finds nothing. Third, Cloudflare's own Universal SSL can answer _acme-challenge with hidden TXT records of its own, which do not show on its DNS page, and the certificate authority reads those instead of ours. We detect this and say so on the domain. If nothing on the domain is proxied, turn off Universal SSL in Cloudflare under SSL/TLS → Edge Certificates.
Wildcard domains are available on every plan, with no cap on the number of subdomains they serve.
Sharing a domain with another space
Verifying a domain locks it, and every name under it, to one space — no other space can map a hostname beneath it. If you run more than one space and want to use the same domain in both, share it: open Space settings → Domains, find the verified domain, and choose Share. Pick one of your own spaces, or paste another customer's space ID.
The other space's owner is emailed a link and has 48 hours to accept. Nothing changes until they do. Once they accept, they can add domains under yours exactly as you can, and their subdomains are covered by your certificate.
You keep ownership. Withdraw at any time from the same place — their domains under your name stop serving immediately, and they cannot add new ones. Their setup is kept, so sharing it again brings their domains straight back.
One thing to know if you are on the receiving end: the space that owns the domain also owns its certificate. If their certificate stops renewing, your subdomains under it lose HTTPS and only they can fix it. We email both spaces when that starts going wrong.
Using Cloudflare
If your domain is proxied through Cloudflare (the orange cloud in your DNS records), Cloudflare's SSL/TLS encryption mode must be set to Full. This is the single most common cause of a custom domain not working behind Cloudflare.
In your Cloudflare dashboard, for the zone holding your domain:
- Go to SSL/TLS → Overview.
- Set the encryption mode to Full.
The setting is per-zone, so it applies to every proxied record in that domain — not just the one pointing at your Cube.
Why Full, and not the other modes
- Flexible — Cloudflare fetches your origin over plain HTTP. Krova Cloud always redirects HTTP to HTTPS, so Cloudflare receives that redirect and passes it to the browser; the browser retries over HTTPS, Cloudflare fetches the origin over plain HTTP again, and the request bounces forever. The browser gives up with
ERR_TOO_MANY_REDIRECTS. - Full — Cloudflare connects to your origin over HTTPS without validating the origin's certificate. This is what Krova Cloud requires, and it works from the moment you add the domain.
- Full (strict) — Cloudflare connects over HTTPS and validates the origin certificate against a public CA. A proxied domain can be served a placeholder origin certificate so that it works instantly, and a placeholder is exactly what Full (strict) rejects, so Cloudflare returns error 526 instead of your site.
Cloudflare's own documentation often advises "Full or higher" — do not follow that advice here. "Higher" means Full (strict), which breaks proxied Krova Cloud domains with a 526. Choose Full, specifically.
If your domain is DNS-only (the grey cloud), Cloudflare is not in the request path at all and none of this applies — there is no SSL/TLS mode to change, and Krova Cloud issues a real Let's Encrypt certificate for the domain.
Troubleshooting
ERR_TOO_MANY_REDIRECTS, "too many redirects", or a redirect loop — your SSL/TLS mode is almost certainly Flexible. Set SSL/TLS → Overview → Full, then reload. Cloudflare caches redirects, so also purge the cache or test in a private window if the loop persists.- Cloudflare 526 ("Invalid SSL certificate") — your SSL/TLS mode is Full (strict). Change it to Full. Do not try to fix this by installing a certificate on the origin; the origin certificate is managed by Krova Cloud and is not something you can replace.
- The domain works on grey cloud but breaks when you turn the orange cloud on — this is the same SSL/TLS mode issue. Set the mode to Full before switching the record to Proxied.
Krova Cloud does not detect your SSL/TLS mode — it lives in your Cloudflare zone, not ours. Set it to Full before you point an orange-cloud domain at us and neither failure can happen. If a domain is already looping, change the mode to Full, then purge your Cloudflare cache: Cloudflare can keep serving a cached redirect from before the fix.
Real visitor information
Your app receives the real client details on every request, so logging, rate-limiting, and geolocation work as expected:
X-Real-IPandX-Forwarded-For— the real visitor IP (spoof-safe; not the proxy's address).X-Forwarded-ProtoandX-Forwarded-Host— the original scheme and hostname.
If your app reads the visitor's address from the connection itself rather than from a header, turn on the PROXY protocol for the domain. See Visitor address on the connection.
Per-domain settings
Each domain has its own independent proxy configuration — changing one domain never affects another. You can tune all of these from the domain's settings (and over the REST API, at create time or any time after):
- Security headers — let Krova Cloud enforce platform security headers, or pass your app's own through.
- Request & response header overrides — inject or remove headers to/from your app.
- Max request body size — cap upload sizes (or leave blank for no cap).
- Response compression — optional gzip/zstd at the edge (off by default; most CDN-fronted domains are already compressed).
- IP allow / deny lists — restrict who can reach the app by IP or CIDR (IPv4 and IPv6).
- Basic Auth — put a username/password in front of the domain (the password is hashed and never stored in plaintext).
- Connect over HTTPS — speak HTTPS rather than plain HTTP on the internal hop to your Cube. Leave this off unless something inside your Cube insists on HTTPS; see When your Cube terminates TLS itself.
- PROXY protocol: send the visitor's address at the start of each connection to your Cube. Leave this off unless your app accepts it; see Visitor address on the connection.
See the REST API reference for the exact field names, types, and an example that sets these in one request.
Uploads & CORS
Upload size limits
Krova Cloud does not cap upload sizes by default — your app receives every byte you send. If an upload returns a 413 Request Entity Too Large, the limit is almost always inside your Cube, not at the Krova Cloud proxy:
- nginx — raise
client_max_body_sizein your nginx config (e.g.client_max_body_size 500m;). - PHP — raise
post_max_sizeandupload_max_filesizeinphp.ini. - Framework body parsers — check your framework's body-size limit (Express
limit, Next.jsbodyParser.sizeLimit, DjangoDATA_UPLOAD_MAX_MEMORY_SIZE, etc.).
If you want Krova Cloud to enforce a hard cap at the proxy (to reject over-size requests before they reach your app), set Max request body in the domain's settings. When a request declares its size in Content-Length, the proxy returns 413 cleanly before forwarding. Chunked uploads without a Content-Length header that exceed the cap may in rare cases return a 502 instead of 413 — this is a known Caddy limitation.
CORS
If a cross-origin frontend (e.g. a separate SPA on app.example.com calling an API at api.example.com) is being blocked by the browser, enable CORS in the domain's settings. Krova Cloud handles the entire CORS protocol at the proxy — your app never needs to touch Access-Control-* headers:
- Preflight
OPTIONSrequests are answered at the edge (your app never sees them). Access-Control-Allow-Originis added to every response, including error responses (413, 403) and upstream-down interstitials, so cross-origin callers always get a parseable error.- Use the wildcard (
*) for a public API that any origin may call. Use an explicit origin list (e.g.https://app.example.com) when you need cookies orAuthorizationheaders (credentials). - The preflight cache (
Access-Control-Max-Age) defaults to 600 s and can be set up to 86 400 s (Chromium's hard cap), reducing the number of preflight round-trips.
Removing a domain
Removing a domain takes down its route immediately. Update your DNS afterwards if you're pointing the domain somewhere else.
HTTPS is handled at the edge
Krova terminates TLS at its edge and forwards requests to your Cube as plain HTTP. Nothing about this is a compromise, and nothing about it leaves your traffic unencrypted: the hop from Krova's edge to your Cube stays entirely inside Krova's own infrastructure. Krova terminates TLS on the Cube's own server, or on a separate edge server that reaches it over an internal-CA mutually authenticated connection, and from the server a private bridge carries the request into the Cube. Nothing crosses the public internet unencrypted at any point.
Services running inside a Cube should therefore listen on plain HTTP, not HTTPS. Inbound traffic reaches your Cube through Krova's edge rather than directly, so a certificate requested from inside the Cube can never be validated by a public CA — there is nothing for a validator to connect to. And if the in-cube proxy redirects port 80 to HTTPS anyway, the browser is sent to https://, Krova terminates that at the edge and forwards plain HTTP again, the panel redirects again, and the request loops forever.
If the software inside your Cube cannot be told to stop redirecting — CloudPanel, typically — you do not have to fight it. See When your Cube terminates TLS itself.
If you run Dokploy for a domain that Krova fronts, set Certificate: None for that domain in Dokploy. Krova is already handling the certificate at the edge; a certificate configured in the panel only causes the redirect loop above.
When your Cube terminates TLS itself
Some control panels hold their own certificate and cannot be told to stop — CloudPanel is the common one. Those Cubes answer plain HTTP with a redirect to HTTPS, which is exactly the loop described above. For them, turn on Connect over HTTPS in the domain's settings: Krova then speaks HTTPS on the internal hop instead of plain HTTP, the panel is satisfied, and the loop stops.
Krova applies the setting as you save it and does not test your Cube first. If nothing inside the Cube answers HTTPS for this hostname on that port, visitors to the domain get a Krova error page instead of your site until you turn the setting off again. Other domains are not affected.
This is not a security setting, and turning it on does not make your traffic more private: the internal hop was never exposed to begin with (see above). It exists only to satisfy software inside the Cube that refuses to answer over cleartext. If your app is happy on plain HTTP — which almost every app is — leave this off.
It is settable when you attach a domain and changeable at any time afterwards, from the domain's settings, the REST API (originScheme), the CLI, the SDK, MCP, or n8n.
Visitor address on the connection
Krova passes the visitor's address in the X-Real-IP and X-Forwarded-For headers. Some software reads it from the TCP connection instead, the way it would on a server of its own. For that, turn on the PROXY protocol in the domain's settings, as version 1 (a line of text) or version 2 (binary). Krova then sends a PROXY header naming the visitor at the start of every connection to your Cube, on every path a visitor can take to the domain, and with Connect over HTTPS on as well.
Your app must accept the PROXY protocol on the domain's port before you turn this on. An app that does not will answer every request to the domain with an error (nginx answers 400) until you turn it off. Krova does not test this before saving. Other domains are not affected.
An app accepts the PROXY protocol per port, not per domain, so every domain mapped to the same Cube port needs the same setting. Krova warns you before a save that would mix them, and the API refuses one with a 409 unless you send confirmMixedProxyProtocol: true.
The header's source address is the visitor. The port in it is always 0 (Krova does not pass the visitor's port), and the destination is all zeros.
nginx (version 1.13.11 or later for version 2): add proxy_protocol to the listen line of the server that answers the domain, and let the real IP module take the address from it. 198.18.0.0/15 is Krova's internal network, which the connections to your Cube come from.
server {
listen 3000 proxy_protocol;
server_name shop.example.com;
set_real_ip_from 198.18.0.0/15;
real_ip_header proxy_protocol;
# $remote_addr is now the visitor's address.
}Node.js, with no dependencies, for version 1: read the header line off each new connection, then hand the connection to your HTTP server.
import http from "node:http";
import net from "node:net";
const app = http.createServer((req, res) => {
res.end(`Your address: ${req.socket.visitorAddress}\n`);
});
net
.createServer((socket) => {
socket.once("data", (chunk) => {
// PROXY v1: "PROXY TCP4 <visitor> <destination> <port> <port>\r\n"
const end = chunk.indexOf("\r\n");
const fields = chunk.subarray(0, end).toString("latin1").split(" ");
if (end === -1 || fields[0] !== "PROXY") {
socket.destroy();
return;
}
socket.visitorAddress = fields[2];
socket.pause();
if (end + 2 < chunk.length) {
socket.unshift(chunk.subarray(end + 2));
}
app.emit("connection", socket);
socket.resume();
});
})
.listen(3000);Set it when you attach a domain or at any time afterwards, from the domain's settings, the REST API (proxyProtocol: "v1", "v2" or null), the CLI (krova domains set-proxy-protocol), the SDK, MCP, or n8n.
