Add base-path deployment support

This commit is contained in:
sas.fajri
2026-04-14 06:47:40 +07:00
parent ab4d56bfef
commit dbda44112e
7 changed files with 502 additions and 4 deletions

171
INSTALLATION.md Normal file
View File

@@ -0,0 +1,171 @@
# Installation Guide - Ubuntu
Panduan instalasi `doclink_web` di Ubuntu dengan Apache reverse proxy untuk URL publik seperti:
```text
https://domain.com/docklink-desktop
```
## Prerequisites
- Ubuntu Server
- Node.js 18+ atau yang kompatibel
- Apache2
- SSL certificate jika ingin HTTPS
## 1. Copy Project
Masuk ke folder project:
```bash
cd /path/to/doclink_web
```
Pastikan file berikut ada:
- `server.js`
- `styles.css`
- `logo.png`
- `start.sh`
- `stop.sh`
## 2. Jalankan App
Set base path sesuai URL publik:
```bash
export DOCLINK_BASE_PATH=/docklink-desktop
export PORT=5173
node server.js
```
Kalau mau jalan di background, pakai script:
```bash
DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh
```
### Catatan
- `DOCLINK_BASE_PATH` harus sama dengan path publik di browser.
- Nama folder project di disk tidak harus sama.
- Aplikasi tetap bisa diakses lewat port lokal `5173`, lalu Apache yang meneruskan request ke sana.
## 3. Install Dependencies Ubuntu
Update paket dan install Apache:
```bash
sudo apt update
sudo apt install -y apache2
```
Kalau Node.js belum ada, install sesuai versi yang kamu pakai di server.
## 4. Aktifkan Module Apache
Di Ubuntu / Debian:
```bash
sudo a2enmod proxy proxy_http ssl
sudo systemctl restart apache2
```
## 5. Buat Virtual Host
Edit konfigurasi Apache di Ubuntu:
```bash
sudo nano /etc/apache2/sites-available/domain.conf
```
Contoh konfigurasi:
```apache
<VirtualHost *:80>
ServerName domain.com
Redirect permanent / https://domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName domain.com
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/
ProxyPass /docklink-desktop/ http://127.0.0.1:5173/
ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/
</VirtualHost>
```
## 6. Enable Site
Aktifkan site dan reload Apache:
```bash
sudo a2ensite domain.conf
sudo systemctl reload apache2
```
## 7. Test
Buka URL berikut:
```text
https://domain.com/docklink-desktop/
```
Kalau benar, aplikasi akan tampil tanpa expose port `5173` ke publik.
## 8. Stop App
Kalau pakai `start.sh`:
```bash
./stop.sh
```
Kalau mau stop port tertentu:
```bash
./stop.sh 5173
```
## Troubleshooting
### Halaman tidak muncul
- Pastikan Node.js app masih jalan di `127.0.0.1:5173`
- Pastikan Apache module `proxy` dan `proxy_http` aktif
- Pastikan `DOCLINK_BASE_PATH=/docklink-desktop`
## Ubuntu Notes
- File konfigurasi Apache biasanya ada di:
- `/etc/apache2/sites-available/`
- `/etc/apache2/sites-enabled/`
- Log Apache biasanya ada di:
- `/var/log/apache2/access.log`
- `/var/log/apache2/error.log`
- Service Apache dikelola dengan:
```bash
sudo systemctl status apache2
sudo systemctl restart apache2
sudo systemctl reload apache2
```
### CSS atau logo tidak loading
- Cek apakah request asset ikut lewat path `/docklink-desktop/...`
- Pastikan Apache proxy tidak memotong base path secara salah
### Redirect balik ke root
- Pastikan semua request ke app memakai base path yang sama
- Gunakan `DOCLINK_BASE_PATH=/docklink-desktop`

246
INSTALLATION_UBUNTU.md Normal file
View File

@@ -0,0 +1,246 @@
# DocLink Web Installation Guide for Ubuntu
This guide explains how to run `doclink_web` on Ubuntu behind Apache, using a subfolder URL such as:
```text
https://domain.com/docklink-desktop
```
## 1. Requirements
- Ubuntu Server
- Apache 2
- Node.js 18+ or compatible
- A valid SSL certificate if you want HTTPS
## 2. Copy the Application
Place the project on the server and enter the project directory:
```bash
cd /path/to/doclink_web
```
Make sure these files exist:
- `server.js`
- `styles.css`
- `logo.png`
- `start.sh`
- `stop.sh`
## 3. Install Apache on Ubuntu
Update package lists and install Apache:
```bash
sudo apt update
sudo apt install -y apache2
```
If Node.js is not installed yet, install the version you want to use on the server.
## 4. Create the systemd Service
The app should run as a service so it starts automatically and keeps running in the background.
Create an environment file, for example:
```bash
sudo nano /etc/doclink-web.env
```
Example content:
```bash
DOCLINK_BASE_PATH=/docklink-desktop
PORT=5173
DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra
```
Then create the service file:
```bash
sudo nano /etc/systemd/system/doclink-web.service
```
Example content:
```ini
[Unit]
Description=DocLink Web
After=network.target
[Service]
Type=simple
WorkingDirectory=/path/to/doclink_web
EnvironmentFile=/etc/doclink-web.env
ExecStart=/usr/bin/node /path/to/doclink_web/server.js
Restart=on-failure
RestartSec=3
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target
```
Reload systemd and enable the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable doclink-web
sudo systemctl start doclink-web
```
Check status:
```bash
sudo systemctl status doclink-web
```
## 5. Run the Application Manually
This step is optional. Use it only if you want to test the app without systemd.
Run it directly:
```bash
export DOCLINK_BASE_PATH=/docklink-desktop
export PORT=5173
node server.js
```
If you prefer the helper script:
```bash
DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh
```
Important notes:
- `DOCLINK_BASE_PATH` must match the public URL path.
- The local project folder name can be anything.
- Apache will forward requests to the Node.js app on `127.0.0.1:5173`.
## 6. Enable Apache Modules
Enable the required proxy and SSL modules:
```bash
sudo a2enmod proxy proxy_http ssl
sudo systemctl restart apache2
```
## 7. Configure Apache Virtual Host
Create or edit your site config in:
```bash
sudo nano /etc/apache2/sites-available/domain.conf
```
Example configuration:
```apache
<VirtualHost *:80>
ServerName domain.com
Redirect permanent / https://domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName domain.com
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/
ProxyPass /docklink-desktop/ http://127.0.0.1:5173/
ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/
</VirtualHost>
```
If you want a reusable deployment template, start from:
- `deploy/apache-vhost.template.conf`
- `deploy/env.template`
Then replace:
- `__SERVER_NAME__`
- `__BASE_PATH__`
- `__APP_PORT__`
- `__SSL_CERT_FILE__`
- `__SSL_KEY_FILE__`
## 8. Enable the Site
Enable the site and reload Apache:
```bash
sudo a2ensite domain.conf
sudo systemctl reload apache2
```
## 9. Verify the Deployment
Open the application in the browser:
```text
https://domain.com/docklink-desktop/
```
If everything is configured correctly, the application should load without exposing port `5173` publicly.
## 10. Stop the Application
If you started the app with the helper script:
```bash
./stop.sh
```
Or stop a specific port:
```bash
./stop.sh 5173
```
## 11. Ubuntu-Specific Locations
- Apache site configs:
- `/etc/apache2/sites-available/`
- `/etc/apache2/sites-enabled/`
- Apache logs:
- `/var/log/apache2/access.log`
- `/var/log/apache2/error.log`
- Apache service commands:
```bash
sudo systemctl status apache2
sudo systemctl restart apache2
sudo systemctl reload apache2
```
## 12. Troubleshooting
### The page does not open
- Check that the `doclink-web` service is running
- Check that the Node.js app is listening on `127.0.0.1:5173`
- Check that Apache modules `proxy` and `proxy_http` are enabled
- Confirm that `DOCLINK_BASE_PATH=/docklink-desktop`
### CSS or logo does not load
- Confirm requests are going through `/docklink-desktop/...`
- Check the Apache proxy configuration for the correct trailing slash
### Redirects go back to `/`
- Make sure every public request uses the same base path
- Keep `DOCLINK_BASE_PATH` aligned with the URL folder

View File

@@ -21,6 +21,14 @@ Open:
http://localhost:5173
```
To serve under a subfolder like `https://domain.com/folder`, set:
```bash
DOCLINK_BASE_PATH=/folder npm start
```
That makes internal links, HTMX requests, and redirects stay under the same folder.
## Click To Run
If you want a one-click launcher on macOS/Linux:
@@ -55,6 +63,7 @@ If you run the server in the foreground, stop it with `Ctrl+C`.
- If the upstream login rejects the credentials or is unavailable, demo mode creates a local session so the UI can still be exercised.
- Order search, order detail helpers, FPP loading, password change, and special message save are wired through the API adapter with mock fallback for local preview.
- `start.sh` writes a pid file named `.doclink-web.pid` and a log file named `doclink-web.log`.
- Deployment templates are in `deploy/`.
## Main Files

View File

@@ -0,0 +1,20 @@
<VirtualHost *:80>
ServerName __SERVER_NAME__
Redirect permanent / https://__SERVER_NAME__/
</VirtualHost>
<VirtualHost *:443>
ServerName __SERVER_NAME__
SSLEngine on
SSLCertificateFile __SSL_CERT_FILE__
SSLCertificateKeyFile __SSL_KEY_FILE__
ProxyPreserveHost On
ProxyRequests Off
RedirectMatch 301 ^/__BASE_PATH__$ /__BASE_PATH__/
ProxyPass /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/
ProxyPassReverse /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/
</VirtualHost>

View File

@@ -0,0 +1,16 @@
[Unit]
Description=DocLink Web
After=network.target
[Service]
Type=simple
WorkingDirectory=__WORKING_DIRECTORY__
EnvironmentFile=__ENV_FILE__
ExecStart=/usr/bin/node __WORKING_DIRECTORY__/server.js
Restart=on-failure
RestartSec=3
User=__RUN_USER__
Group=__RUN_GROUP__
[Install]
WantedBy=multi-user.target

3
deploy/env.template Normal file
View File

@@ -0,0 +1,3 @@
DOCLINK_BASE_PATH=/__BASE_PATH__
PORT=__APP_PORT__
DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra

View File

@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const PORT = Number(process.env.PORT || 5173);
const BASE_PATH = normalizeBasePath(process.env.DOCLINK_BASE_PATH || "");
const API_BASE =
process.env.DOCLINK_API_BASE ||
"https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra";
@@ -23,6 +24,36 @@ function escapeHtml(value) {
.replaceAll('"', "&quot;");
}
function normalizeBasePath(value) {
const raw = String(value || "").trim();
if (!raw || raw === "/") return "";
const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`;
return withLeadingSlash.replace(/\/+$/, "");
}
function appPath(pathname = "/") {
const text = String(pathname || "/");
if (/^https?:\/\//i.test(text) || text.startsWith("//")) return text;
if (!BASE_PATH) return text.startsWith("/") ? text : `/${text}`;
if (text === BASE_PATH || text.startsWith(`${BASE_PATH}/`)) return text;
if (text === "/" || text === "") return BASE_PATH;
if (text.startsWith("/")) return `${BASE_PATH}${text}`;
return `${BASE_PATH}/${text}`;
}
function stripBasePath(pathname) {
const path = String(pathname || "/");
if (!BASE_PATH) return path || "/";
if (path === BASE_PATH) return "/";
if (path.startsWith(`${BASE_PATH}/`)) return path.slice(BASE_PATH.length) || "/";
return path;
}
function rewriteHtmlPaths(html) {
if (!BASE_PATH || typeof html !== "string") return html;
return html.replace(/\b(href|src|action|hx-get|hx-post)="\/(?!\/)/g, `$1="${BASE_PATH}/`);
}
function statusClass(status) {
const mapping = {
Processing: "warning",
@@ -1998,13 +2029,15 @@ function json(res, status, payload) {
}
function redirect(res, location, headers = {}) {
res.writeHead(302, { Location: location, ...headers });
const resolvedLocation = typeof location === "string" && location.startsWith("/") ? appPath(location) : location;
res.writeHead(302, { Location: resolvedLocation, ...headers });
res.end();
}
function html(res, status, body, headers = {}) {
const output = rewriteHtmlPaths(body);
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers });
res.end(body);
res.end(output);
}
function isHtmx(req) {
@@ -2204,7 +2237,7 @@ async function fragmentResultDetail(session, resultId) {
}
async function renderRoute(req, res, url) {
const path = url.pathname;
const path = stripBasePath(url.pathname);
const query = Object.fromEntries(url.searchParams.entries());
const session = readSession(req);
const authed = Boolean(session);
@@ -2572,7 +2605,7 @@ async function renderRoute(req, res, url) {
return;
}
html(res, 404, emptyRoute(path));
html(res, 404, emptyRoute(url.pathname));
}
const server = http.createServer(async (req, res) => {