Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deployment: app.mydentalofficemanagement.com
How this app is served in production: nginx serves a static production
build on this machine, and Cloudflare Tunnel exposes it publicly at
https://app.mydentalofficemanagement.com without opening any ports on
the router.
Browser → Cloudflare (DNS + edge) → Tunnel (cloudflared, systemd) → nginx (:80) → static files
1. Prerequisites
- Debian (tested on Debian 13 "trixie")
- sudo access
- Node.js + pnpm installed, repo cloned
- The domain
mydentalofficemanagement.comalready added as a zone in Cloudflare, with a Cloudflare account you can log into
2. Install cloudflared
Debian's codename is usually too new for Cloudflare's apt repo, so install
the .deb package directly instead:
curl -fsSL -o /tmp/cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i /tmp/cloudflared.deb
cloudflared --version
3. Authenticate cloudflared
cloudflared tunnel login
Opens a browser — log in and authorize the mydentalofficemanagement.com
zone. This writes ~/.cloudflared/cert.pem.
4. Tunnel
This project uses an existing dashboard-managed tunnel named
mydentalmanagement (tunnel ID 027669d4-3d96-4f90-920d-e727cecf3b92).
It already has other subdomains routed through it, so reuse it — don't
create a new tunnel for this app.
If it doesn't exist yet on a fresh Cloudflare account, create one:
cloudflared tunnel create mydentalmanagement
Get a run token
cloudflared tunnel token mydentalmanagement
Copy the printed token for the next step. (This token authenticates this machine to run the tunnel — treat it like a secret, don't commit it.)
Install cloudflared as a service
sudo cloudflared service install <TOKEN>
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
5. Cloudflare public hostname route
In the Cloudflare dashboard:
Zero Trust → Networks → Tunnels → mydentalmanagement → Published application routes → Add a published application route
| Field | Value |
|---|---|
| Hostname | app.mydentalofficemanagement.com |
| Path | (leave empty) |
| Service Type | HTTP |
| URL | localhost:80 |
Note: mydentalmanagement was originally a locally-configured tunnel and
had to be migrated to dashboard-managed mode the first time this was
set up (Cloudflare shows a one-time "Migrate" prompt for this). That step
is already done — future public hostname routes can just be added
directly from this tab.
If you see "A DNS record with this name already exists," a stale CNAME/A record is occupying that hostname — delete it in DNS → Records first, then re-add the route (this recreates the DNS record correctly as part of the route).
6. Install nginx
sudo apt-get install -y nginx
sudo rm -f /etc/nginx/sites-enabled/default
If Apache is installed and already bound to port 80, disable it first (only if nothing else depends on it):
sudo systemctl disable --now apache2
nginx site config
Create /etc/nginx/sites-available/app.mydentalofficemanagement.com.conf:
server {
listen 80;
server_name app.mydentalofficemanagement.com;
root /var/www/app.mydentalofficemanagement.com;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
sudo ln -s /etc/nginx/sites-available/app.mydentalofficemanagement.com.conf \
/etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl enable nginx
Note: /var/www/... is used (not a path under /home/<user>/...)
because nginx runs as www-data, and a typical home directory
(drwx------) blocks that user from traversing into it.
7. Build and deploy the app
vite.config.ts requires PORT and BASE_PATH env vars to even load
(PORT isn't actually used by the static build, but the config throws
without it regardless of command).
cd "artifacts/dental-app"
PORT=5173 BASE_PATH=/ npm run build
Deploy the build output:
sudo mkdir -p /var/www/app.mydentalofficemanagement.com
sudo cp -r dist/public/* /var/www/app.mydentalofficemanagement.com/
sudo chown -R www-data:www-data /var/www/app.mydentalofficemanagement.com
Redeploying after code changes
Repeat the build + copy steps above any time the app changes:
cd "artifacts/dental-app"
PORT=5173 BASE_PATH=/ npm run build
sudo cp -r dist/public/* /var/www/app.mydentalofficemanagement.com/
8. Running the site
By default nginx and cloudflared are systemd services and can either
auto-start on boot or be started manually.
Enable auto-start on boot:
sudo systemctl enable nginx
sudo systemctl enable cloudflared
Disable auto-start (manual start only):
sudo systemctl disable nginx
sudo systemctl disable cloudflared
Manually start the site (needed after every reboot if auto-start is disabled):
sudo systemctl start cloudflared
sudo systemctl start nginx
Stop the site:
sudo systemctl stop nginx
sudo systemctl stop cloudflared
Check status:
systemctl status nginx cloudflared
curl -I https://app.mydentalofficemanagement.com
No Node/Vite process is needed to serve the site — it's a static build served directly by nginx.
9. Local development preview (not the public site)
To preview changes locally before building/deploying, separate from the live public site:
cd "artifacts/dental-app"
PORT=5173 BASE_PATH=/ npm run dev
Then open http://localhost:5173 in a browser on this machine.
Troubleshooting
- 502 from the public URL: check
sudo journalctl -u cloudflared -n 40for the exact error (often a port mismatch between the tunnel's Published Application Route and what nginx is actually listening on). - nginx
502/403locally: checksudo tail -n 30 /var/log/nginx/error.log. cloudflared tunnel listshows "no active connection": the systemd service isn't running —sudo systemctl status cloudflared.- Port 80 already in use: check
sudo ss -tlnp | grep ':80 'to see what's holding it (commonly Apache's default install).