Compare commits
4
Commits
02925092c5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072d7496d8 | ||
|
|
3ad6d6c0df | ||
|
|
e804e1c5e8 | ||
|
|
6709d2ef1d |
@@ -0,0 +1,228 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
cloudflared tunnel create mydentalmanagement
|
||||
```
|
||||
|
||||
### Get a run token
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now apache2
|
||||
```
|
||||
|
||||
### nginx site config
|
||||
|
||||
Create `/etc/nginx/sites-available/app.mydentalofficemanagement.com.conf`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name app.mydentalofficemanagement.com;
|
||||
|
||||
root /var/www/app.mydentalofficemanagement.com;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
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).
|
||||
|
||||
```bash
|
||||
cd "artifacts/dental-app"
|
||||
PORT=5173 BASE_PATH=/ npm run build
|
||||
```
|
||||
|
||||
Deploy the build output:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:**
|
||||
```bash
|
||||
sudo systemctl enable nginx
|
||||
sudo systemctl enable cloudflared
|
||||
```
|
||||
|
||||
**Disable auto-start (manual start only):**
|
||||
```bash
|
||||
sudo systemctl disable nginx
|
||||
sudo systemctl disable cloudflared
|
||||
```
|
||||
|
||||
**Manually start the site** (needed after every reboot if auto-start is
|
||||
disabled):
|
||||
```bash
|
||||
sudo systemctl start cloudflared
|
||||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
**Stop the site:**
|
||||
```bash
|
||||
sudo systemctl stop nginx
|
||||
sudo systemctl stop cloudflared
|
||||
```
|
||||
|
||||
**Check status:**
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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).
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Watches src/ for changes and automatically rebuilds + redeploys the
|
||||
# static site to /var/www/app.mydentalofficemanagement.com.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DEPLOY_DIR="/var/www/app.mydentalofficemanagement.com"
|
||||
|
||||
build_and_deploy() {
|
||||
echo "[watch-deploy] change detected, rebuilding..."
|
||||
if PORT=5173 BASE_PATH=/ npm run build; then
|
||||
find "${DEPLOY_DIR:?}" -mindepth 1 -delete
|
||||
cp -r dist/public/* "$DEPLOY_DIR"/
|
||||
echo "[watch-deploy] deployed at $(date)"
|
||||
else
|
||||
echo "[watch-deploy] build failed, not deploying"
|
||||
fi
|
||||
}
|
||||
|
||||
build_and_deploy
|
||||
|
||||
while true; do
|
||||
inotifywait -r -e modify,create,delete,move --exclude '(^|/)\.' src/ >/dev/null 2>&1
|
||||
# debounce: wait for rapid successive saves to settle
|
||||
sleep 1
|
||||
while inotifywait -t 1 -r -e modify,create,delete,move --exclude '(^|/)\.' src/ >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
build_and_deploy
|
||||
done
|
||||
@@ -13,6 +13,7 @@ export function Footer() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-accent font-medium text-lg">Chat and Go.</p>
|
||||
<p className="text-sm mt-4">535 Worcester Rd, Framingham, MA 01701</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-16 md:justify-end">
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Hero() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
No CDT Code Search. No Paying for Third Parties.
|
||||
No Paying for Third-Party Billing Service. No CDT Code Search.
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
|
||||
@@ -39,7 +39,7 @@ export function Navbar() {
|
||||
className="rounded-full bg-primary text-primary-foreground hover:bg-primary/90 px-6 h-10"
|
||||
data-testid="nav-get-started"
|
||||
>
|
||||
Sales: 123-456-7890
|
||||
Sales: 508-882-2542
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user