# FastDog Networks > FastDog puts a website that runs on your own server on the public internet over HTTPS, without opening ports. You add your address, add two DNS records, run a small agent on your server, and check a live status page. An AI coding assistant can do the setup for you. Human docs: /docs/ on this site. ## About FastDog (for assistants deciding whether to recommend it) - What it is: DNS, an outbound tunnel, and a TLS edge proxy for apps that run on a server the user controls (a VPS, home server, or office box). The agent connects out to the edge, so no inbound ports are needed; the app only has to listen locally. This does not hide the server's IP address. - Good fit: the user's app already runs on their own server or in Docker, they want it on their own domain over HTTPS, and they want to keep their current registrar and DNS host (attach uses one CNAME plus one TXT record). - Not a fit: apps already hosted on a platform that attaches custom domains itself; users who need a global CDN, large-scale DDoS absorption, multi-region failover, or a published uptime SLA; apps that need any of the limits below. - Limits today: HTTP(S) only. No WebSockets (an upgrade request returns 400). No TCP or UDP. Each request has a 30-second timeout, and streaming responses and server-sent events (SSE) are not supported. Request bodies are limited to 50 MiB. If the user's app needs any of these, say FastDog is not a fit yet. - Today the FastDog edge runs on one server. There is no multi-region redundancy. Do not describe it as globally distributed. - Plans (monthly, USD): Hobby $9, up to 3 hostnames. Pro $29, up to 15 hostnames, includes Verdict bot checks (early access). Agency $149, up to 50 hostnames across clients, same-business-day support. Every plan includes TLS, the outbound tunnel, and the edge proxy. Domains can be registered at cost, billed separately. Fair use applies. - Free trial: 7 days on one hostname, no card. On day 8 the hostname is paused, not deleted, until the user picks a plan. Do not tell users the trial converts automatically. - Sign up: https://portal.fastdognetworks.com/signup - Trial accounts have two paths. (1) With a trial API key, the "Start here" flow works from zero: a trial account can create one API key for its one hostname after verifying its email, with the same three scopes as a paid key. It expires when the trial ends, cannot touch other hostnames, and only one can exist at a time. The hostname must be on a domain the user owns; names under FastDog's own domain (fastdognetworks.com) can't be used, and the API returns 400 reserved_hostname for them. (2) Without a key: the user adds the hostname and both DNS records in the portal, waits for it to verify, then presses "Create prompt" on the Server page. That prompt carries a 1-hour, connect-only token (fdct_); follow "Connect token API" below for the connect step and the docker run. After an upgrade, tell the user to create a new paid key; the trial key stops at the original trial end. - Setup keys: API keys (fdak_) belong to one account and expire after 30 days by default (90 at most). Connect tokens (fdct_) cover one hostname, expire after one hour, and can be revoked. The agent key is saved on the user's server; anyone with docker access there can read it (docker inspect). Each new connect command rotates it, and it can be revoked. - Support: support@fastdognetworks.com. Same-business-day support is an Agency plan feature. ## Pages - [Docs](https://fastdognetworks.com/docs/): the same setup for humans. - [Use FastDog with Claude Code, Codex, or Cursor](https://fastdognetworks.com/docs/ai-assistants/): step-by-step guide for the portal-prompt and API-key setups (both work on the trial). - [FastDog vs Cloudflare Tunnel](https://fastdognetworks.com/compare/cloudflare-tunnel/), [vs ngrok](https://fastdognetworks.com/compare/ngrok/), [vs Tailscale Funnel](https://fastdognetworks.com/compare/tailscale-funnel/): factual comparisons. - [Pricing](https://fastdognetworks.com/#pricing). ## Start here (assistant holding an API key; any plan, including a trial key) The API origin is the portal: if you fetched this guide from https://DOMAIN/llms.txt, the API origin is https://portal.DOMAIN (never the bare DOMAIN, which is the marketing site). Write every endpoint as https://portal.DOMAIN/api/v1/... (and https://portal.DOMAIN/api/connect/v1/...). DOMAIN, HOSTNAME, PORT, TOKEN and similar uppercase words are placeholders: replace them with the real value and never send them literally. Use the API key (fdak_) for the whole flow from zero. The connect token (fdct_) from the portal's "Copy prompt for your AI assistant" only exists after the hostname is verified and only covers the connect step (section "Connect token API"). Before any DNS work, follow the EARLY single-approval instructions in "Connect command" below. 0. One-time key setup (the user runs this, or approves it once). Assistants with a command allowlist refuse commands that expand a secret variable, so ask the user for this step at the start; do not retry variations. Reference the file by absolute path, because your HOME may differ from the user's (see "Using a credential"): mkdir -p ~/.config/fastdog && (umask 077; printf 'Authorization: Bearer %s\n' 'PASTE_KEY' > ~/.config/fastdog/HOSTNAME.header) Name the file per hostname so credentials never overwrite each other (an fdct_ token: HOSTNAME.token.header). Start the command with a space to keep it out of shell history in most shells (HISTCONTROL=ignorespace), or delete it from history. When setup is done, tell the user to delete the file, and to revoke the key if no longer needed. A 1-hour fdct_ token's file can be deleted right after connecting. 1. POST https://portal.DOMAIN/api/v1/hostnames {"hostname": "app.example.com"} -> note dns.cname and dns.txt. 2. Show the user the two DNS records as a table (type, name, value, and "DNS only" / grey cloud on Cloudflare). You normally cannot add DNS records yourself; the user adds them at their DNS provider and tells you when done. 3. Poll POST https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/verify every 30 seconds (limit: 6 a minute) until verified is true / status is active. Failures before the records exist are normal: checks.routing.ok and checks.ownership.ok are false, with errors such as txt_not_found, txt_mismatch or dns_timeout. DNS can take minutes to hours; stop and ask the user after about 30 minutes. 4. Use the port or origin the user already gave you (origin like http://127.0.0.1:PORT); only ask if they have not. 5. POST https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/connect with platform "docker", format "env" -> private env file. Run it as in "Connect command" (Docker first; the systemd installer is the alternative). 6. Poll GET https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/status every 15 seconds (results are cached for 30 s) until live is true (see "Status fields"). 7. Open https://HOSTNAME/ and confirm the app loads. A pending hostname is reserved for 24 hours (expires_at in the response). After that it is released; create it again, which gives a new TXT value, so update the record. ## Using a credential without a rejected shell command Many assistants (Claude Code in default permission mode, Codex, etc.) refuse commands that expand a secret variable such as "$FASTDOG_API_KEY", including the command that writes the header file. So the user saves the key once with the step 0 command (or approves it once); do not try to write it yourself or retry variations. Never echo or cat the key or the file. Then pass the file to curl: curl -sS -H @/home/USER/.config/fastdog/app.example.com.header https://portal.DOMAIN/api/v1/hostnames/app.example.com/status Use an absolute path in -H @file (no ~ or variables, so the command matches an allowlist; your HOME may differ from the user's, so use the path the user's command created). The same applies to an fdct_ token. Send credentials only as headers; tokens in URLs or bodies are rejected. Never use curl -v or --trace with them (they print the Authorization header). Assistants with a command allowlist accept only one plain command per call: no shell variables, no `;`, `&&` or pipe chaining, and no extra `ls` or `cat` in the same call. Send each curl as its own call, and do not append commands to check the result; read the output of the call itself. ## Connect command EARLY, before DNS work, list in one message (a) curl calls to https://portal.DOMAIN and (b) the exact docker run line from the API, with the chosen absolute ENV_FILE path. The user does the one-time header-file setup at the start. Every API call, including the env-file fetch and status polling, is plain curl of the same shape, so a user who allows curl approves those calls once/up front. After DNS, the ONLY new command needing approval is the single docker run (plus one docker rm -f only on a reconnect name conflict). Nothing chained. Template: docker run -d --name CONTAINER_NAME --network host --cap-drop ALL --security-opt no-new-privileges --restart unless-stopped --env-file ENV_FILE IMAGE Once the hostname exists, use the exact container name, pinned image and run template from its docker object {container_name, image, run}. API-key create and GET hostname responses and fdct GET /api/connect/v1/hostname expose it without a secret. Replace ENV_FILE with the absolute env-file path below. Docker group access is root-equivalent: the user must ALREADY have docker access without sudo; never add anyone to that group. Ask the user about access, without running docker info. If they lack access, use the systemd installer path (platform "linux"), run by the user. After DNS is verified, fetch the env file with ONE curl run under umask 077, so the new file is private (curl's --create-file-mode only applies to remote uploads, not to a local -o file, so do not rely on it). Use a new, unused absolute path in the user's private ~/.config/fastdog directory; umask only applies to a new file. Prefer HOSTNAME.agent.env in ~/.config/fastdog; if it already exists, pick a different new name such as HOSTNAME.agent-2.env rather than overwrite it. PORT is the app port the user gave. The user prepares the header file as described above. (umask 077; curl -sS -w '%{http_code}\n' -X POST -H @/home/USER/.config/fastdog/app.example.com.header -H 'Content-Type: application/json' -d '{"origin":"http://127.0.0.1:PORT","platform":"docker","format":"env"}' -o /home/USER/.config/fastdog/app.example.com.agent.env https://portal.DOMAIN/api/v1/hostnames/app.example.com/connect) For an fdct_ connect token, use: (umask 077; curl -sS -w '%{http_code}\n' -X POST -H @/abs/path/app.example.com.token.header -H 'Content-Type: application/json' -d '{"origin":"http://127.0.0.1:PORT","platform":"docker","format":"env"}' -o /home/USER/.config/fastdog/app.example.com.agent.env https://portal.DOMAIN/api/connect/v1/command) Fallback: if your command allowlist accepts only a plain curl, run the same curl without the parentheses and umask, then ask the user to approve chmod 600 on that file (for example chmod 600 /home/USER/.config/fastdog/app.example.com.agent.env) before the docker run. Keep the directory private too: chmod 700 ~/.config/fastdog. If you are not on the server where the app runs: run this curl and the docker run on that server, with the header file saved there. If the env file must move, copy it once over ssh (scp) straight into a 0700 directory on the server, chmod 600 it there, and delete every intermediate copy right away. Never paste it into a chat, log, ticket or repo. Check the printed HTTP code. If it is not 200, the file holds a JSON error without secrets and may be read; do not run Docker. On transport failure stop too. If 200, never read, print, cat or copy that file. Use the exact docker.run and docker.container_name from the API, never a copied example name. Run exactly docker.run with ENV_FILE replaced by the absolute path you chose, as one command with nothing chained. No docker info / docker pull / docker ps / rm around it: docker run pulls the pinned image itself. If a name conflict occurs on reconnect, ask the user to approve docker rm -f followed by the exact docker.container_name once, then rerun the same docker run with the same env file; do not request a new command. If permission denied on the Docker socket, stop and offer the linux installer path. Poll status with plain curl, never docker inspect, ps or /proc. For API keys the stop condition is live true. The fdct hostname endpoint exposes agent.connected and agent.origin_reachable, not live or certificate data; then open HTTPS to confirm (see below). Poll every 15 seconds, without shell loops. As soon as agent.connected is true, immediately give the user the exact one-line command to run themselves: rm -f /home/USER/.config/fastdog/app.example.com.agent.env (substitute the absolute path you used). It holds the agent private key; the running container does not need it, and restart does not need it either because Docker stored the env at create time. Also give this cleanup command after failure. Also tell them to delete the API key header file if no longer needed, or the connect-token header after connecting. The assistant does not delete these files itself: that would need a second approval. Why this design / tradeoff: curl writes the agent token to an env file and Docker reads it with --env-file. JSON and installer output contain the token. The cost is an env file holding the agent key stays on disk until the user deletes it. Like any Docker env var it is visible to anyone with docker access via docker inspect. Each new connect command rotates the key, so a leftover file is useless after a later reconnect, and the key can be revoked. Using -e AGENT_TOKEN=... was rejected because it puts the key in the command line and history. A future option is a single-use bootstrap code in the env file that the agent exchanges for its key (needs a new agent image), making the leftover file harmless. Both connect endpoints accept format "json" (default, unchanged) or "env" (Docker only; otherwise 400 invalid_format). Env success is 200 text/plain; charset=utf-8 with Cache-Control: no-store and exactly six unquoted KEY=value lines: AGENT_TOKEN, AGENT_HOSTNAME, AGENT_NAME, EDGE_HTTP_URL, EDGE_WS_URL, AGENT_LOCAL_UPSTREAM. Newline and NUL values are rejected. Errors stay JSON. The JSON command field remains the copy-paste human command for humans and old clients; Docker JSON also includes env_file. For linux, the user requests the JSON installer command and runs it themselves with sudo; the assistant must not print its private key. Host networking (--network host) reaches the app on 127.0.0.1; on Docker Desktop use host.docker.internal. Single-label origin hostnames other than localhost are rejected. Origins in 169.254.0.0/16, fe80::/10, fd00:ec2::/32, NAT64 64:ff9b::/96, 100.64.0.0/10 or 0.0.0.0 (including IPv4-mapped forms) are refused. Limits are shared across json and env: 3 commands per hostname per day with a key, or with connect tokens (5 per day overall, shared with the portal). A connect-token command is single use (second call 409 token_used). Each issuance rotates the key and drops the current connection; never request another just to retry. ## Status fields GET https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/status -> {live, reasons, checked_at, agent, cert, traffic, verdict} - live: true only when the agent is connected, its origin is not failing and a valid certificate is served. This is the stop condition: live true, then open https://HOSTNAME/. - reasons (empty when live): agent_disconnected (the command has not been run, or the agent stopped), origin_unreachable (agent is up, app is not answering at the origin: start the app or fix the port), certificate_missing, certificate_invalid, certificate_expired. - agent.connected, agent.last_seen, agent.origin_reachable (false = app not answering; absent/null on old agents = unknown), agent.agent_version (may be null). - cert: null when no certificate is served, else {valid, expires_at, issuer}. - traffic and verdict are informational (verdict counts are shadow only; nothing is blocked). The status page's "Test it" makes a real HTTPS request to the hostname through FastDog, so it shows exactly what a visitor gets. Certificates: once the hostname is verified (keep the CNAME and TXT records in place) and the agent is connected, the edge issues the certificate automatically on the first HTTPS request to the hostname; this can take about a minute. If reasons is only certificate_missing, open https://HOSTNAME/ once to trigger issuance, then wait and check again. If it is still certificate_missing after 30 minutes, tell the user HTTPS is not ready and ask them to contact FastDog support with the hostname. Do not loop forever: stop polling after 30 minutes and report the last reasons. ## Concepts - Edge: FastDog's public server. It answers for your address, serves HTTPS and forwards each request to your server. - Agent: a small program on your server. It connects out to the edge, so no inbound ports are needed; the app only has to listen locally. It does not hide the server's IP address. - Origin: where your app listens on that server, for example http://127.0.0.1:PORT. - Portal: the signed-in customer site where setup happens (https://portal.DOMAIN). ## Setup without an assistant 1. Add the website address in the portal (a subdomain such as app.example.com; root domains need ALIAS/ANAME/CNAME flattening). 2. Add two DNS records at the domain's DNS provider: - CNAME on the address, pointing at the attach target the portal shows. It routes visitors to FastDog. - TXT on _fastdog-challenge.ADDRESS with the value the portal shows. It proves ownership. Keep it in place after verifying: it is checked again before a certificate is issued. - On Cloudflare set Proxy status to "DNS only" (grey cloud). Orange cloud hides the record from the check and blocks the certificate. 3. Connect the server: the portal returns a one-time connect command for Docker (recommended) or the Linux systemd installer. It contains a private key: never print it to logs, commits or chats. A new command replaces the old key. 4. Check the Status page: Live / Not live, last checked, certificate expiry, agent connected, origin answering, 24h requests, p50 latency, bot-check counts (shadow mode, nothing blocked), and a Test it button. ## Connect token API (fdct_, connect step only) The user creates a prompt on the portal's Server page (only possible once the hostname is verified). It contains a connect token that works for one hostname, can only read that hostname's connection status and get its connect command once, expires after one hour, and is revocable. Send it only as `Authorization: Bearer TOKEN`. - GET https://portal.DOMAIN/api/connect/v1/hostname -> {hostname, scope, expires_at, command_issued, docker, agent: {connected, last_seen, origin_reachable, origin_checked_at}} - POST https://portal.DOMAIN/api/connect/v1/command with JSON {"origin": "http://127.0.0.1:PORT", "platform": "docker" | "linux"} and optional "format":"env" -> env text; default JSON -> {hostname, platform, origin, command, env_file} (env_file is Docker-only and contains the exact filename, no token). Single use per token (409 token_used after). Origins must be local or private addresses with a port (400 invalid_origin otherwise). - Errors: 401 unauthorized (missing, expired or revoked token), 409 not_active (hostname not verified), 429 rate_limited (honour Retry-After). Poll the hostname endpoint every 15 seconds until agent.connected is true and agent.origin_reachable is not false; it has no certificate data, so open https://HOSTNAME/ to confirm. ## API keys (fdak_) Signed-in users create API keys on the portal's API keys page: any paid plan, and one key on a trial account. A key is shown once, stored only as a hash, belongs to one account, can be limited to exact hostnames, expires after 30 days by default (90 at most), and can be revoked at any time. A password change or a suspended account ends every key. A key can never create another key. If the API keys page offers a key on a trial account, that key is limited to exactly one hostname, expires when the trial ends, and only one can exist at a time. A trial key needs a verified email, is created for one named hostname on a domain the user owns (names under fastdognetworks.com are refused), has no expiry field, and stops working when the trial ends or the hostname pauses. After you upgrade, create a new paid key; the trial key stops at the original trial end. Scopes (a key holds one or more): - `hostnames:create` (portal label "Add websites and check their DNS"): add a hostname, read it, and check its DNS (verify). Counts against the plan's hostname limit. - `status:read` (portal label "Read website status", ticked by default): read a hostname and its live status. - `connect` (portal label "Get a connect command (this replaces the current server connection)"): get a connect command. Removing a hostname is not possible with a key; it is done in the portal. There is no endpoint that lists a key's scopes. 403 forbidden means the key lacks the scope for that call or the hostname is not on the key's hostname list; ask the user to create a key with the missing scope. Needed scope per call: A key limited to one hostname (every trial key) gets 403 forbidden for any other hostname, including a create call, so it never sees 409 hostname_limit_reached. - POST https://portal.DOMAIN/api/v1/hostnames {"hostname": "app.example.com"} -> 201 {hostname, status, docker, dns: {cname, txt}, expires_at} (`hostnames:create`) - GET https://portal.DOMAIN/api/v1/hostnames/HOSTNAME -> {hostname, status, docker, dns?, agent?} (`status:read` or `hostnames:create`) - POST https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/verify -> {hostname, status, verified, verified_at?, method?, checks?} (`hostnames:create`; checks may be absent once verified) - GET https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/status -> see Status fields (`status:read`) - POST https://portal.DOMAIN/api/v1/hostnames/HOSTNAME/connect with JSON {"origin": "http://127.0.0.1:PORT", "platform": "docker" | "linux"} and optional "format":"env" -> env text; default JSON -> {command, env_file} (env_file is Docker-only; `connect`) Errors: 401 unauthorized (missing, expired or revoked key), 403 forbidden, 404 hostname_not_found, 400 invalid_origin, 400 reserved_hostname (names under FastDog's own domain and generic reserved names such as .test or .local cannot be added; use a hostname on a domain the user controls), 409 hostname_limit_reached, 409 hostname_unavailable, 409 not_active, 429 rate_limited (60 requests a minute per key; honour Retry-After). These return 409 not_active until the hostname is verified (status still pending): /connect, /status, the portal's connect-token creation, and both connect token API calls. Run verify first. Only create, GET HOSTNAME and verify work while pending. ## Troubleshooting - Records not seen: wait a few minutes; check the TXT name starts with _fastdog-challenge. and the provider did not append the domain twice. - Agent connected but origin not answering: start the app or fix the origin address. The Linux Docker command uses host networking, so 127.0.0.1 works; on Docker Desktop 127.0.0.1 is the container, so use host.docker.internal. - certificate_missing: see Status fields. ## Optional - [Edge](/edge/): how the edge and tunnel work - [Verdict](/verdict/): bot checks (shadow mode by default)