Distiller Reverse Proxy
Overview
The Distiller platform provides a built-in reverse proxy capability that exposes local web applications to the internet via HTTPS without requiring tunnel services, port forwarding, or proxy configuration. This skill addresses the common issue of absolute paths breaking behind reverse proxies and provides tools to fix these issues automatically.
ℹ️ You might also see frpc/frps mentioned elsewhere. Those tunnels are optional: the Distiller proxy works entirely on-device. Use the FRP context in this doc only if your deployment already relies on
/etc/frp/frpc.tomlto advertise a hostname such astest.devices.pamir.ai.
Key Pattern:
Local App (port 5000) → Distiller Proxy → Public HTTPS URL http://localhost:5000 → https://subdomain.devices.pamir.ai/distiller/proxy/5000/
When to Use This Skill
Use this skill when users:
- •Want to make a local web app publicly accessible
- •Report CSS or JavaScript not loading (MIME type errors)
- •Experience API calls returning 404 errors
- •Ask about exposing apps through the Distiller proxy
- •Mention reverse proxy path issues
Quick Start
Expose Any App in 3 Steps
Step 1: Run the app locally
# Any web server on any port python app.py # Flask on 5000 npm run dev # Vite on 3000 python -m http.server 8080 # HTTP server on 8080
Step 2: Access via proxy URL
https://{subdomain}.devices.pamir.ai/distiller/proxy/{PORT}/
Where do I get the subdomain?
- •If you’re using the Distiller proxy only, the Devices dashboard shows the assigned subdomain.
- •If you’re also tunneling with frpc, you can inspect
/etc/frp/frpc.toml:bashcat /etc/frp/frpc.toml | grep subdomain # Example output: subdomain = "test" # Your URL: https://test.devices.pamir.ai/distiller/proxy/5000/
Step 3: Fix path issues if needed
# Check for issues ./scripts/check-paths.sh /path/to/app # Auto-fix common patterns ./scripts/fix-paths.sh /path/to/app
The Absolute vs Relative Path Problem
Root cause: Absolute paths (starting with /) resolve to domain root, breaking when behind reverse proxies.
Symptoms:
- •CSS not loading: "MIME type 'application/json' is not a supported stylesheet"
- •JavaScript 404 errors
- •API calls fail with 404
- •Images don't load
Examples:
❌ Broken (absolute paths):
<link rel="stylesheet" href="/styles.css"> <script src="/main.js"></script>
Resolves to: https://domain.com/styles.css (wrong - not behind proxy path)
✅ Working (relative paths):
<link rel="stylesheet" href="styles.css"> <script src="main.js"></script>
Resolves to: https://domain.com/distiller/proxy/5000/styles.css (correct)
Fixing Path Issues
Automatic Fix (Recommended)
Use the provided script to automatically fix common patterns:
# Dry run (preview changes) ./scripts/fix-paths.sh /path/to/app --dry-run # Apply fixes ./scripts/fix-paths.sh /path/to/app
The script fixes:
- •HTML:
href="/..."→href="..." - •HTML:
src="/..."→src="..." - •JavaScript:
API_BASE = '/api'→API_BASE = 'api' - •JavaScript:
fetch('/api/...')→fetch('api/...')
⚠️ The fixer is intentionally conservative but still performs in-place edits. Run with
--dry-runfirst, keep backups (e.g., via git), and review the diff—projects sometimes rely on intentional absolute URLs.
Manual Fix
HTML Files:
<!-- BEFORE --> <link rel="stylesheet" href="/styles.css"> <script src="/main.js"></script> <img src="/logo.png"> <!-- AFTER --> <link rel="stylesheet" href="styles.css"> <script src="main.js"></script> <img src="logo.png">
JavaScript Files:
// BEFORE
const API_BASE = '/api';
fetch('/api/data');
// AFTER
const API_BASE = 'api';
fetch('api/data');
Exception: Keep absolute paths for external resources:
<!-- These are fine (external URLs) --> <script src="https://cdn.example.com/library.js"></script>
Serving Under a Base Path (e.g., /watchdog)
Sometimes you must keep the proxy path segment (such as /distiller/proxy/5000/watchdog) instead of stripping the /. The pattern below lets your app live under any prefix without code rewrites each time.
- •
Server-side base path variable
bash# systemd / shell export BASE_PATH=/watchdog
javascript// Express example const basePath = (process.env.BASE_PATH || '/').replace(/\/$/, ''); app.use(`${basePath}/static`, express.static('public')); app.use(basePath, router); app.get(`${basePath}/health`, handler); - •
Inject the base path into rendered HTML
html<script>window.BASE_PATH = "{{ basePath }}";</script> <link rel="stylesheet" href="{{ basePath }}/static/dashboard.css"> <script src="{{ basePath }}/static/app.js"></script> - •
Teach the front-end to respect it
javascriptconst BASE = window.BASE_PATH || ''; await fetch(`${BASE}/api/status`);
With those three pieces, requests to https://subdomain.devices.pamir.ai/distiller/proxy/5000/watchdog/… stay scoped to /watchdog on both server and client, preventing 404s while still allowing the app to run at / locally.
Framework-Specific Guides
Flask (Python)
Works well by default. Only fix HTML templates.
# No changes needed to Flask code
app = Flask(__name__, static_folder='static')
@app.route('/api/data')
def data():
return jsonify({'status': 'ok'})
Fix: Change paths in HTML templates from absolute to relative.
Vite (JavaScript)
Add base path configuration:
// vite.config.js
export default defineConfig({
base: './', // Use relative base path
server: {
host: '0.0.0.0',
port: 3000
}
})
Create React App
Update package.json:
{
"homepage": "."
}
Express (Node.js)
const express = require('express');
const app = express();
app.use(express.static('public'));
app.get('/api/data', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(5000, '0.0.0.0');
Fix: HTML files to use relative paths.
Common Issues
CSS loads but styles not applied
Cause: MIME type mismatch
Solution:
- •Check browser console for error
- •Verify path is relative:
href="styles.css"nothref="/styles.css"
API calls return HTML instead of JSON
Cause: Flask returning index.html for missing routes
Solution:
# Make sure API routes come BEFORE catch-all
@app.route('/api/data')
def data():
return jsonify({'status': 'ok'})
# This should be last
@app.route('/<path:path>')
def serve_static(path):
return send_from_directory(app.static_folder, path)
Page loads but refresh gives 404
Cause: Client-side routing needs server fallback
Solution:
@app.errorhandler(404)
def not_found(e):
# For SPA with client-side routing
if request.path.startswith('/api/'):
return jsonify(error='Not found'), 404
return send_from_directory(app.static_folder, 'index.html')
POST requests timeout or return 404
Cause: Long-running operations (>30s) don't work well through proxy
Symptoms:
- •POST requests hang/timeout after 30-60 seconds
- •Request completes on server but browser never gets response
- •Works locally but fails through proxy
Solution: Use background tasks with immediate response
import threading
def run_long_task():
"""Background task"""
# Do expensive work here
time.sleep(60)
# Save results to file/database
@app.route('/api/process', methods=['POST'])
def process_data():
# Start task in background
thread = threading.Thread(target=run_long_task, daemon=True)
thread.start()
# Return immediately
return jsonify({
'success': True,
'message': 'Processing started. Check back in 1-2 minutes.'
})
# Frontend polls for updates
// JavaScript
async function startProcess() {
const res = await fetch('./api/process', { method: 'POST' });
const data = await res.json();
// Poll every 10 seconds for updates
const interval = setInterval(async () => {
const status = await fetch('./api/status');
const result = await status.json();
if (result.complete) {
clearInterval(interval);
// Update UI with results
}
}, 10000);
}
Why: The proxy can't maintain connections for long-running synchronous requests. Background tasks + polling is the standard pattern for web apps.
Best Practices
- •
Use relative paths everywhere
- •✅
href="style.css" - •❌
href="/style.css"
- •✅
- •
Namespace API routes
- •✅
/api/users,/api/data - •❌
/users(conflicts with static files)
- •✅
- •
Support a base path
- •Use env vars/config to mount your router under
/watchdog(or similar) when the proxy requires it. - •Render
BASE_PATHinto templates and prepend it inside JavaScript fetch calls.
- •Use env vars/config to mount your router under
- •
Test locally first
- •Test on
http://localhost:5000/ - •Then test through proxy
- •Test on
- •
Document the public URL
markdownPublic: https://subdomain.devices.pamir.ai/distiller/proxy/5000/ Local: http://localhost:5000/
Port Management
Check what's running
# See all listening ports netstat -tuln | grep LISTEN # Check specific port lsof -i :5000
Common port assignments
| Port | Typical Use |
|---|---|
| 5000 | Flask apps |
| 3000 | Vite/React dev servers |
| 8000 | Django apps |
| 8080 | Generic web servers |
Kill process on port
lsof -i :5000
kill -9 {PID}
# Or one-liner
pkill -f "python app.py"
Resources
scripts/
check-paths.sh - Scans project files for absolute path issues and reports potential problems (uses ripgrep when available, falls back to grep). Accepts a project root and reports findings without modifying files.
fix-paths.sh - Automatically fixes common absolute path patterns in HTML and JavaScript files. Supports --dry-run to preview changes; always review the diff afterwards if you run it for real.
Usage examples are shown in the "Fixing Path Issues" section above.
Quick Reference
┌─────────────────────────────────────────────────┐
│ Distiller Proxy Quick Reference │
├─────────────────────────────────────────────────┤
│ URL Pattern: │
│ https://{subdomain}.devices.pamir.ai/ │
│ distiller/proxy/{PORT}/ │
│ │
│ Fix Checklist: │
│ □ Remove leading / from href/src │
│ □ Change API calls to relative paths │
│ □ Test locally first │
│ □ Hard refresh browser (Ctrl+Shift+R) │
│ │
│ Common Fixes: │
│ href="/style.css" → href="style.css" │
│ src="/main.js" → src="main.js" │
│ fetch('/api/...') → fetch('api/...') │
└─────────────────────────────────────────────────┘