Put your website on FastDog.
Four steps: add your address, add two DNS records, connect your server, then check that it’s live. Your app keeps running where it is.
For AI assistants: the same guide in plain text is at /llms.txt.
AI assistants · vs Cloudflare Tunnel · vs ngrok · vs Tailscale Funnel
How it works
- Visitors type your address. A CNAME record at your DNS provider sends them to FastDog’s edge.
- The edge answers over HTTPS with a certificate it gets for you automatically.
- A small program on your server (the agent) keeps one outbound connection open to the edge. Each request travels down that connection to your app.
Why no ports? Your server connects out to FastDog, the way a browser does, so you don’t open any inbound ports. Your app only needs to listen locally. This doesn’t hide your server’s IP address: old DNS records or your app’s own responses can still reveal it.
1. Add your website address
In the portal, enter the address people will use, such as app.example.com. Use a subdomain: most DNS providers can’t put a CNAME on a root domain like example.com. If yours supports ALIAS, ANAME or CNAME flattening, that works too.
2. Add two DNS records
The portal shows both records with the exact field names for Cloudflare, Namecheap, GoDaddy, Route 53, Porkbun and others.
- CNAME on your address, pointing at the attach target the portal shows. It sends visitors to FastDog.
- TXT on
_fastdog-challenge.plus your address, with the value the portal shows. It shows you control the domain’s DNS, which is how we confirm the hostname is yours to connect; we verify the record before issuing the agent credential. Keep your DNS account secure, since anyone who controls your DNS could do the same. Leave it in place after verifying: we check it again before issuing your certificate.
Why two records? One routes traffic and one proves ownership. Keep both in place; FastDog may re-check them.
Cloudflare: use grey cloud. Set Proxy status to “DNS only” (grey cloud). Grey cloud means Cloudflare only answers DNS and visitors go straight to FastDog. Orange cloud (Proxied) puts Cloudflare in the middle, which hides your record from our check and stops your HTTPS certificate.
DNS changes can take a few minutes, sometimes a few hours, to spread. Press Check in the portal once they’re saved.
3. Connect your server
On the Server page, enter where your app listens on that server, such as http://127.0.0.1:PORT, then press Get my connect command. Run the command on that server. Docker is recommended (no sudo needed); the Linux systemd installer, macOS and Windows commands are also offered.
The command contains a private key and is shown once. Don’t paste it into chats, tickets or logs. Getting a new command replaces the old key and closes any current connection until you run the new one.
4. Check it’s live
The Status page shows a big Live or Not live, when it was last checked, your certificate’s expiry, whether the agent is connected and your app is answering, requests over the last 24 hours, typical response time, and bot-check counts in shadow mode (counted only, nothing blocked). Test it loads your home page through FastDog and shows the status code, time and certificate.
Using an AI assistant
On the Server page, press Create prompt under “Copy prompt for your AI assistant”. The prompt includes a connect token that:
- works for one website address only,
- can only read that address’s connection status and get its connect command once,
- expires after one hour, and can be revoked at any time from the same page.
The API origin is the portal: if you fetched the guide from https://DOMAIN/llms.txt, the API origin is https://portal.DOMAIN (never the bare DOMAIN), and every endpoint is written as https://portal.DOMAIN/api/v1/... (and https://portal.DOMAIN/api/connect/v1/...).
The assistant sends it as Authorization: Bearer <token> to two endpoints on the portal:
GET /api/connect/v1/hostname: the address, token expiry, andagent.connected/agent.origin_reachable.POST /api/connect/v1/commandwith{"origin":"http://127.0.0.1:PORT","platform":"docker","format":"env"}(or"linux"for the systemd installer): returns the private env file once (default format "json" retains the copy-paste human command). Origins must be local or private addresses with a port (link-local 169.254.0.0/16 and fe80::/10 are refused). Docker group access is root-equivalent: use Docker only if you already have it without sudo.
Tokens are never accepted in a URL. Your password and sudo prompts stay with you.
An assistant that holds an API key (see below) can do the whole job from scratch:
- It adds the website address and shows you the two DNS records. It normally can’t add them itself, so add them at your DNS provider (on Cloudflare, “DNS only”, grey cloud).
- It checks every 30 seconds. “Not found yet” results before the records exist are normal. A pending address is reserved for 24 hours.
- Before DNS work, list the curl calls and exact docker run line together as described below. After the initial header-file setup and curl approval, only the single docker run needs a new approval (plus one docker rm -f on a reconnect name conflict). Each address allows 3 new commands a day; each rotates the key.
- It polls the status until it says live, then opens your address. Once the address 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, which can take about a minute. If the status stays on “no HTTPS certificate”, wait, and contact support if it persists beyond 30 minutes.
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.
You can create the connect-token prompt only after the address is verified. Its token covers that one hostname and the connect step. An API key does only what its selected scopes allow: read status, add websites and check DNS, or get a connect command. Removing a website is portal-only. Assistants with a command allowlist refuse commands that expand a secret like $KEY, so you save the Authorization header once to a private file yourself (or approve the assistant running it once): 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. The leading space keeps the command out of shell history in most shells (HISTCONTROL=ignorespace); otherwise delete it from your history. The assistant then uses curl -H @/absolute/path/HOSTNAME.header and never curl -v or --trace, which 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. When setup is done, delete the file and revoke the key if you no longer need it; a connect token’s file can be deleted right after connecting. If you already told it your app’s port, it won’t ask again. The full machine-readable guide is /llms.txt.
API keys
For scripts and AI assistants that do more than one connection, create an API key on the portal’s API keys page. Paid plans create API keys; on a trial, the page offers one key, limited to exactly one hostname, that expires when the trial ends. You choose what it can do, optionally which websites it covers, and when it expires (30 days by default, 90 at most). It is shown once, stored only as a hash, and you can revoke it at any time. Changing your password ends all your keys.
hostnames:create: add a website and check its DNS records.status:read: read the live status of your websites.connect: get a connect command. Each one replaces the current server connection.
Removing a website is only possible in the portal. Send the key as Authorization: Bearer <key>, never in a URL.
Errors include 403 forbidden (the key lacks that scope, or the website isn’t on its list: a key limited to one website, such as a trial key, gets 403 for any other website, including when adding one) and 400 reserved_hostname (names under FastDog’s own domain and generic reserved names can’t be added; use an address on a domain you control). The verify response’s checks may be absent once verified, and the status agent.agent_version may be null.
POST /api/v1/hostnameswith{"hostname": "app.example.com"}: adds it and returns the two DNS records.GET /api/v1/hostnames/<hostname>: the address, its state and the records still to add.POST /api/v1/hostnames/<hostname>/verify: checks the DNS records.GET /api/v1/hostnames/<hostname>/status: the same data as the Status page.POST /api/v1/hostnames/<hostname>/connectwith{"origin":"http://127.0.0.1:PORT","platform":"docker","format":"env"}(or"linux"): returns a connect command.
Limits: 60 requests a minute per key, 5 new keys an hour, 10 active keys per account. Keys are shown once and stored only as a hash. For accepted API-key requests we record the key ID, route, method, status and time.
Troubleshooting
- “We haven’t seen your records yet”
- Wait a few minutes and check again. Make sure the TXT name starts with
_fastdog-challenge.and your provider didn’t add your domain twice. - “Your record is proxied by Cloudflare”
- Switch the CNAME to DNS only (grey cloud).
- Agent connected, but your app isn’t answering
- Start your app, or fix the address you entered. The Linux Docker command uses host networking, so
127.0.0.1works; on Docker Desktop127.0.0.1is the container itself, so usehost.docker.internal. - Not live: no certificate yet
- HTTPS isn’t ready yet. Make sure DNS is verified and the agent is connected, wait about 10 minutes, then refresh Status or press Test it. If it still says no certificate after 30 minutes, contact support with your address.
Self-hosted apps
Set your app's own base or public URL setting (for example BASE_URL, APP_URL, or similar) to your public HTTPS hostname, such as https://app.example.test, not localhost. Then restart the app so its links and redirects use the public address.
- Mealie: set BASE_URL to https://your-hostname.
- Other apps: look for a setting such as PUBLIC_URL, APP_URL, MAIN_URL or FRONTEND_URL in that app's docs.