App owner's guide

Preparing your app for the host

Everything an app owner needs, from the rules to the first deploy. The checker it mentions downloads from this host too.

Preparing your app for the host

For app owners. You keep your repository as it is; the host asks for a short list of things, checks them for you, and deploys from a web console. This copy is served by the host at https://pochostsrv01.fls.cloud/docs/preparing-your-app.md with its own addresses filled in.

What you get

  • A public URL: https://apps.pochostsrv01.fls.cloud/<you>/<app>/, HTTPS, certificate managed.
  • Access control in front of the app with no code on your side. Pick one visibility: company (anyone at the company, through Google sign-in), link (the company, plus people you send an access link), owner only, or public.
  • Deploys from a button or from a push to your branch, with a health gate and automatic rollback.
  • A page with your containers, health, logs, deploy history, disk and backups.
  • Nightly backups of your volumes, and a PostgreSQL dump if you run one.

What your repository must have

Ten rules. The checker tells you which ones you miss.

  1. **docker-compose.prod.yml** at the repository root, or up to four levels below it. It is the only file the host reads.
  2. No reliance on the project name. The host runs Compose as <you>--<app>, with any dot in your name turned into _. A name: in the file is fine and ignored.
  3. **Exactly one published port, on loopback, from APP_PORT:**
       ports:
         - "127.0.0.1:${APP_PORT}:8080"
    Everything else your services need, they reach over the Compose network by service name.
  4. A real health endpoint: a path on that port that answers 2xx with JSON or plain text when the app works. Not the frontend. A single-page frontend answers every path with its index.html, and the host refuses an HTML answer, so point the host at your backend's endpoint, for example /api/health. Name it when you register, or leave the field blank: at the first deploy the host tries the routes in your repository and the usual names, keeps the first that answers properly, and records it.
  5. Persistent data in named volumes, declared in the compose file. No bind mounts to host paths for data.
  6. **Configuration from .env**, which the host writes next to your compose file, read with env_file: or ${VAR}. Secrets never in the repository. A .env.prod.example listing every variable you need, with a comment each.
  7. No host privileges: no privileged, network_mode: host, pid: host, Docker socket, cap_add, devices, or fixed container_name.
  8. Safe restarts: restart: unless-stopped; migrations on boot or in a one-shot service that exits 0. The host restarts you on every deploy and reboots occasionally.
  9. **Honour BASE_PATH.** Your app is served under /<you>/<app>, not at the root. Read BASE_PATH (for example /jane.doe/invoices) and build every URL you emit from it: the frontend's asset base, the router's base, the API prefix the frontend calls, redirects, cookie paths. Client-side navigation counts too: location.replace('/'), location.href = '/login' and a router's navigate('/') are emitted URLs, and a grep for href= and fetch( will not find them. Default it to empty so local runs and other hosts are unchanged. If the app is a third-party image you cannot change, tick strip prefix at registration; links the app emits are still wrong in that mode, so it is the inferior option.
  10. Nothing external: no external: true volumes or networks.

Variables the host sets for you

Never ask anyone to type these, and default your own names to them:

VariableValueUse it for
APP_PORTthe loopback portthe published port in compose
BASE_PATH/<you>/<app>every URL you build
APP_ORIGINhttps://apps.pochostsrv01.fls.cloudCORS allow-list, absolute links
APP_URLhttps://apps.pochostsrv01.fls.cloud/<you>/<app>/links in emails, OAuth callbacks

In compose, a name of your own can default to one of them: CORS_ORIGIN: ${CORS_ORIGIN:-${APP_ORIGIN}}.

Who is visiting

When the visibility is anything but public, every request that reaches your app carries two headers set by the host:

HeaderA signed-in colleagueA visitor with an access link
X-Apphost-Usertheir email addressthe link's label, for example Acme pilot
X-Apphost-Kinduserguest

Use them to greet the visitor, hide admin screens from guests, or record who did what. They cannot be forged on the host: the proxy drops any copy a visitor sends, and your app is reachable only through the proxy. On a public app they are absent. Locally there is no proxy, so take a development identity from a setting, never from the request.

const who = req.get('X-Apphost-User');           // Express
const isGuest = req.get('X-Apphost-Kind') === 'guest';
who = request.headers.get("X-Apphost-User")        # Flask
is_guest = request.headers.get("X-Apphost-Kind") == "guest"

Sharing a demo with a client

Set the visibility to link, then on the app's Access tab make one link per client, with a label and an expiry. The client clicks it once; their browser stays signed in until the link expires or you revoke it, and the plain address then works as a bookmark. Revoking cuts them off on their next click. The link is shown once, when you make it; copy it then.

Recommended

  • A compose healthcheck that probes 127.0.0.1, never localhost.
  • A boot-time assertion that refuses to start with a debug or bypass flag set.
  • Defaults in compose for variables that have a sensible one (${MODEL:-gpt-4o}), so the console does not ask for them.

Check before you register

With Docker on your laptop. Download the checker from the host, then run it on your repository:

curl -fsSLo apphost-check-repo https://pochostsrv01.fls.cloud/docs/apphost-check-repo
chmod +x apphost-check-repo
./apphost-check-repo /path/to/your/app

A ✗ is a rule you miss; a ! is advice. The host runs the same check before every deploy.

Onboarding, in order

  1. Open https://pochostsrv01.fls.cloud/ and sign in with your company Google account.
  2. Applications → Register an app. App name (permanent, lowercase), repository URL, branch, the health path from rule 4, visibility.
  3. The app page shows a deploy key. Add it to your repository: Settings → Deploy keys → Add, read-only. Nobody's personal GitHub access is involved.
  4. Environment: the page lists every variable your app declares. Fill the ones without a default and save. Values are written once and never shown again. Any character is kept exactly as typed, $ and quotes included, so a generated password needs no escaping; only line breaks are refused.
  5. Check. Fetches your branch with the key and runs the contract check. Fix anything red and press it again.
  6. Deploy. Watch the log. A failed health gate rolls back to the previous version, or stops the app if there was none.
  7. Optional, push to deploy: GitHub → Settings → Webhooks, the payload URL and secret from the app page, content type application/json, event push.

When something fails

SymptomCauseFix
Health gate: "200 but text/html"the health path hits the frontend's catch-allset the path to your backend's endpoint in Settings
Health gate: connection refused or 5xxthe app did not start, or listens on another portcontainer logs on the app page; the published port must be the one the app listens on
Build fails on a missing variablecompose interpolates a variable that is not setfill it under Environment, or give it a default in compose
Page loads, assets or API calls 404absolute paths that ignore the prefixrule 9; as a stopgap, strip prefix in Settings
Login or OAuth comes back to the wrong placecallback URL without the prefixbuild it from APP_URL

Limits to know about

  • The host is shared. Each app gets a CPU and memory limit set by the host; images are built on the host, so keep builds reasonable.
  • One port per app; everything public goes through the proxy on 443.
  • All apps share one hostname, so do not put real client data in a demo: a hole in one app on the host can be used against the sessions of another.
  • Local backups are kept 30 days; ask your administrator about off-site copies.
  • An app nobody has deployed for 90 days is parked: its containers stop and its address shows a page saying the service is switched off. Nothing is deleted. You get an email and a Slack message two weeks and three days ahead, and the dashboard warns you too. Any deploy, or the Keep button on the app page, resets the clock; Unpark or Deploy brings a parked app back. You can also park an app yourself when a demo is over.