2026-08-01 11:07:12 -04:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00
2026-07-20 17:11:01 +00:00

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.com already 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 40 for the exact error (often a port mismatch between the tunnel's Published Application Route and what nginx is actually listening on).
  • nginx 502/403 locally: check sudo tail -n 30 /var/log/nginx/error.log.
  • cloudflared tunnel list shows "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).
Description
No description provided
Readme 970 KiB
Languages
TypeScript 95.2%
CSS 2.5%
HTML 1.3%
JavaScript 0.8%
Shell 0.2%