Tiao On Coolify
Tiao is now set up to deploy as two applications:
- a frontend container built from
client/Dockerfile - a backend container built from
server/Dockerfile
The recommended production shape still keeps a single browser origin:
- the frontend serves the SPA
- either the frontend proxies
/apiand/api/wsto the backend over the private network, or Coolify path-routes those same paths directly to the backend - the backend does not serve frontend assets anymore
Recommended production shape
Use two Coolify applications built from this repo:
tiao-client: public, built fromclient/Dockerfiletiao-server: internal or public as needed, built fromserver/Dockerfile
Recommended dependencies:
- MongoDB: external managed MongoDB, or a Coolify MongoDB resource
- Object storage: S3, Cloudflare R2, Hetzner Object Storage, or MinIO
MongoDB backs more than account metadata here:
- multiplayer room persistence
- social data
- opaque session storage for the
HttpOnlyauth cookie
What localhost Means In Coolify
When Coolify shows a server named localhost, that is the actual machine where Coolify itself is installed.
For your current setup, that means:
- the Hetzner VPS is the one and only deployment server
- Tiao can run on that same server
- a Coolify MongoDB resource can also run on that same server
You do not need to add another server just because the current one is named localhost.
Coolify Application Settings
Backend app
Suggested base settings:
- Application Type:
Docker Image - Registry image name:
ghcr.io/<owner>/<repo>-server - Registry image tag:
main - Port:
3000 - Health Check Path:
/api/health - Domain: optional
The backend does not need a public domain if the frontend proxies traffic to it over the internal network. If you want to keep a single public domain without depending on an internal upstream hostname, you can instead attach path-based domains:
https://tiao.your-domain.com/apihttps://tiao.your-domain.com/api/ws
Frontend app
Suggested base settings:
- Application Type:
Docker Image - Registry image name:
ghcr.io/<owner>/<repo>-client - Registry image tag:
main - Port:
80 - Health Check Path:
/healthz - Domain:
https://tiao.your-domain.com
Important:
- do not put
:maininside the image name field - put
mainin the image tag field - if the image is private, add GHCR credentials in Coolify first
If you prefer image-based deploys instead of building on the VPS:
- publish from GitHub Actions to both
ghcr.io/<owner>/<repo>-client:mainandghcr.io/<owner>/<repo>-server:main - point each Coolify app at the matching image
- add these GitHub secrets so pushes to
maintrigger a redeploy through the Coolify API:COOLIFY_BASE_URLCOOLIFY_API_TOKENCOOLIFY_CLIENT_RESOURCE_UUIDCOOLIFY_SERVER_RESOURCE_UUID
If the repository or package is private:
- add a GHCR registry entry in Coolify
- use a GitHub personal access token with package read access
- configure both Coolify apps to pull from that private registry
Required Environment Variables
Start from server/.env.example.
Required:
MONGODB_URITOKEN_SECRETS3_BUCKET_NAMES3_PUBLIC_URLorCLOUDFRONT_URLAWS_REGIONAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY
Optional:
FRONTEND_URLS3_ENDPOINTS3_FORCE_PATH_STYLE
Notes:
- the backend
FRONTEND_URLshould be the public frontend URL if you want strict CORS when accessing the backend directly S3_ENDPOINTandS3_FORCE_PATH_STYLE=trueare useful for MinIO and some S3-compatible providers.
Frontend runtime variables:
BACKEND_UPSTREAM=http://tiao-server:3000
Notes:
BACKEND_UPSTREAMis only needed in frontend-proxy mode- if you use Coolify path-based routing on the same public domain for
/apiand/api/ws, the frontend app can leaveBACKEND_UPSTREAMunset
Recommended values for a first production deploy:
FRONTEND_URL=https://tiao.your-domain.comMONGODB_URI=<Coolify Mongo internal URL or managed Mongo URL>PORT=3000or simply omitPORTand let the backend default to3000BACKEND_UPSTREAM=http://<coolify-internal-backend-host>:3000
Recommended values for a single-domain Coolify path-routing deploy:
- frontend domain:
https://tiao.your-domain.com - backend domains:
https://tiao.your-domain.com/api,https://tiao.your-domain.com/api/ws FRONTEND_URL=https://tiao.your-domain.com- backend
PORT=3000or omit it - no extra public backend hostname is required
Step-By-Step First Deploy
- Push the Tiao repo to GitHub.
- Let GitHub Actions build and publish both
ghcr.io/<owner>/<repo>-client:mainandghcr.io/<owner>/<repo>-server:main. - In Coolify, add GHCR as a registry if the images are private.
- In Coolify, create a MongoDB resource in the same project and environment as Tiao.
- Deploy the MongoDB resource.
- Copy the MongoDB resource's internal connection string.
- Create a new backend application of type
Docker Image. - Point it at
ghcr.io/<owner>/<repo>-server:main. - Set the backend port to
3000. - Set the backend health check path to
/api/health. - Add backend runtime environment variables from
server/.env.example. - Replace
MONGODB_URIwith the Coolify Mongo internal URL, notlocalhost. - Set
FRONTEND_URLto the eventual public frontend URL. - Deploy the backend once and confirm
/api/healthis healthy. - Create a new frontend application of type
Docker Image. - Point it at
ghcr.io/<owner>/<repo>-client:main. - Set the frontend port to
80. - Set the frontend health check path to
/healthz. - Attach the public domain, for example
https://tiao.ricos.site. - Set
BACKEND_UPSTREAMto the backend app's internal URL, for examplehttp://tiao-server:3000. - Deploy the frontend once and confirm the site loads at the public domain.
- After the first successful deploy, keep using the GitHub Actions workflow for ongoing redeploys.
Important:
- do not copy your local development
PORT=5005into production - the recommended backend port for this image is
3000 - the recommended frontend port for this image is
80
DNS / Proxy Notes
Tiao expects the frontend domain to receive:
- normal HTTPS traffic for the SPA
- API requests at
/api - websocket upgrade requests at
/api/ws
Whether you use the frontend proxy or Coolify path-based routing, the browser can keep using one origin. That means:
- no cross-site cookies are required
- no browser-facing CORS complexity is required in the default production setup
- multiplayer websocket URLs continue to work without special browser configuration
If https://tiao.your-domain.com returns a response from Coolify or Traefik, then DNS and HTTPS are at least partially working.
If you see no available server, that usually means:
- the domain reached the reverse proxy
- but the proxy does not currently see a healthy frontend container to route traffic to
For Tiao, that almost always means the frontend is crashing, restarting, or failing /healthz, or the frontend cannot reach the backend upstream.
Deploy Flow
- Push to
main - GitHub Actions runs build + tests
- GitHub Actions builds and publishes both Docker images to GHCR
- GitHub Actions calls the Coolify deploy API for both app UUIDs
- Coolify pulls the updated images and replaces the running containers
Recommended Workflow For This Repo
For now, the recommended setup is:
- GitHub Actions builds the frontend and backend images
- GHCR stores both images
- Coolify deploys both images on the VPS
That keeps build load off the VPS while still using Coolify for domains, env vars, health checks, logs, proxying, and app lifecycle.
Coolify API Setup
To use the documented API deployment flow:
- In Coolify, enable the API.
- Create an API token in
Keys & Tokens. - Copy the application UUIDs from both Coolify apps.
- Save these GitHub repository secrets:
COOLIFY_BASE_URLCOOLIFY_API_TOKENCOOLIFY_CLIENT_RESOURCE_UUIDCOOLIFY_SERVER_RESOURCE_UUID
Troubleshooting
TOKEN_SECRET not provided in the environment
The backend is starting, but required runtime env vars are missing.
Fix:
- add the missing variables in the backend Coolify app settings
- save and redeploy
connect ECONNREFUSED 127.0.0.1:27017
The backend is trying to connect to MongoDB on localhost, which means "inside the backend container itself".
Fix:
- do not use your local development Mongo URI in production
- create a MongoDB resource in Coolify or use an external MongoDB
- copy the database
internal URLor managed URL intoMONGODB_URI
no available server on the public domain
This usually means Traefik is up, but the frontend container is not healthy enough to receive traffic.
Check:
- the frontend app logs
- the deployment logs
- that the frontend port is
80 - that the frontend health check path is
/healthz - that
BACKEND_UPSTREAMpoints at the backend internal URL - that the public domain is attached to the frontend app, not only present in DNS
Common gotcha:
- the backend may be healthy while the frontend still fails to serve the app because
BACKEND_UPSTREAMis wrong - another common issue is mapping the frontend app to
3000instead of80
HTTPS does not seem to be working
If the browser reaches https://... at all, Coolify's proxy is already handling TLS.
If the app page still fails, the issue is usually frontend health or frontend-to-backend proxying, not certificate setup.
Image pull or deploy errors from GHCR
Check:
- backend image name is
ghcr.io/<owner>/<repo>-server - frontend image name is
ghcr.io/<owner>/<repo>-client - tag is
main - the apps are not accidentally configured with
:maininside the image name field - Coolify has registry credentials if the images are private
What To Automate Later
The most manual parts today are:
- creating the frontend and backend Coolify apps
- creating the MongoDB resource
- wiring registry credentials
- copying both app UUIDs and API tokens into GitHub secrets
- copying backend/frontend runtime env vars into Coolify
These are good candidates for the reusable ops repo later via:
- Coolify API scripts
- env templates
- secrets bootstrap helpers
- a standard "new app" checklist
Realtime Limitation
Tiao currently keeps live multiplayer socket state inside one Node.js process. Deploys are graceful, but active multiplayer matches may briefly reconnect while the container is replaced.
That is acceptable for a single-instance hobby deployment, but true zero-downtime realtime play would require shared realtime state outside the process.