Deploy a full-stack app (React + API + Postgres)
A React frontend and a separate Express + Postgres API. Each is 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. There's no app-count limit. You just need enoughcompute in your pool to cover both, and this recipe is the full chain.
Two ready-made templates: theAPI (Express + Postgres) and theweb 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 thePORT 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, thendeploy 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, setALLOWED_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, and you run as many as your compute pool covers. The free 256 MB is enough to try one small app; add compute to run both, sized however you like.
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 as asecret. 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 as a secret (not a plain orVITE_ 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