Deploy a full-stack app (React + API + Postgres)
A React frontend and a separate Express + Postgres API — each its own app, the way a real app is built.
Production apps keep the frontend and the API as separate services, each deployed and scaled on its own. On Dockhold that's two apps with two URLs, and the API gets its own managed database. Running more than one app is a Pro feature — this recipe is the full chain.
Two ready-made templates: the API (Express + Postgres) and the web frontend (React). Click Use this template on each to get your own copies, then follow the order below.
The shape
- API app — Express, with a managed Postgres database, at its own URL.
- Web app — a React (Vite) single-page app at its own URL, calling the API.
They have to learn each other's URL: the web app needs the API's URL, and the API needs the web app's URL to allow it through CORS. Both are dashboard variables — so deploy both apps, then set the two URLs and restart.
Your app must listen on 0.0.0.0 and read its port from the
PORT environment variable — never localhost, never a
hardcoded port. Dockhold assigns PORT at runtime; an app that
ignores it can't receive traffic.
1. Deploy the API (with a database)
- Use the fullstack-api template, then deploy it.
- On the deploy form, check Add a managed database. Dockhold injects
DATABASE_URL. - When it's live, copy its URL — something like
https://fullstack-api-xxxx.dockhold.app.
The API creates its table on startup and stores guestbook messages in Postgres, so they persist across restarts and deploys.
2. Deploy the web frontend
- Use the fullstack-web template, then deploy it. It builds from its Dockerfile and goes live at its own URL.
-
In the dashboard, set the
API_URLvariable to the API URL from step 1, then restart the web app. It's read at runtime, so there's no rebuild — the URL is injected when the app starts.
3. Allow the frontend through CORS
Back on the API app, open the Variables tab, set
ALLOWED_ORIGIN to the web app's URL, and restart. Now the
browser is allowed to call the API, and the guestbook works end to end.
Why two apps?
You could cram everything into one process, but real apps keep the frontend and API separate so they deploy, scale, and fail independently. That's two apps — available on Pro. On the free plan you can run one of them; the second is the upgrade.
Lock the API down to your app
By default the API is public — anyone with the URL can call it (CORS only
restrains browsers, not scripts). To require a token on every request, make
the API a private app: on the API app's Access tab, switch
it to Private and mint an access token. Now every request
must carry Authorization: Bearer <token>, and anything
without it is rejected at the edge.
Don't put that token in the React app. A browser app ships all of its code to every visitor, so a token baked into it is readable in the browser's developer tools — that only slows down bots, it doesn't keep anyone out. A secret in a frontend is not a secret.
To genuinely restrict the API to your app, keep the token on a server the browser can't read — put a small server in front of the API that holds the token and forwards requests (a "backend for frontend"):
-
Use a Next.js frontend
instead of a static SPA: a server route handler forwards
/api/*to the private API, adding theAuthorizationheader from a token stored in your Vault. The browser talks only to your own server (same origin, no token), and your server talks to the API. -
Or front the static build with a small server (e.g. an Express app that
serves the files and proxies
/api/*to the private API with the token).
Either way the token lives only on the server, never in the browser — so
only your app can reach the API. Keep the token in the Vault (not a plain or
VITE_ variable) so it's encrypted and injected at runtime.
Troubleshooting
- Frontend loads but calls fail with a CORS error: set
ALLOWED_ORIGINon the API to the exact web app URL and redeploy (step 3). - Frontend says "API_URL is not set": set the
API_URLvariable in the dashboard and restart the web app. - API returns 500 on the database: make sure you enabled
the managed database so
DATABASE_URLis injected.
Next
- The Vite + React recipe · The Express API recipe
- Agent rules — make your AI tool generate Dockhold-ready apps.
- All recipes