Files
FileHost/docs-site/en/guide/domain-separation.md
T
O96u b05d572221
GitHub Release / release (push) Canceled after 0s
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
Publish Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Canceled after 0s
Publish Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Canceled after 0s
Publish Docker image / merge (push) Canceled after 0s
release: v1.2.8 — 双域名保存确认与 clear-domains 恢复
启用双域名时以确认框提示锁死风险与恢复方式,不再强制校验 Host。
新增 clear-domains 运维 CLI 与相关文档。
2026-09-04 14:46:37 +08:00

7.8 KiB

Dual-domain separation · Recommended deployment

When admin and image domains are split, the admin UI, API, and public image links use different hostnames. PicHost enforces isolation in application middleware: the image host only serves image paths; other requests return 404.

Architecture

One Docker instance + two domains, both fully reverse-proxied to port 6892 (isolation is handled by PicHost middleware; no path splitting in Nginx).

Domain Purpose
admin.example.com Admin UI, API, Twikoo upload
pic.example.com Public image links (non-image paths blocked)

Set during /setup or in Settings:

  • Site URL: https://admin.example.com
  • Image URL: https://pic.example.com

Setup and Settings do not auto-fill the site URL from window.location.origin; use manual input or “Use detected URL”. Disabling dual-domain requires confirmation and clears the site URL. Saves include domainSeparation: true/false. See Changelog.

Nginx example

# admin.example.com — admin + API
server {
  server_name admin.example.com;
  location / {
    proxy_pass http://127.0.0.1:6892;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 12m;
  }
}

# pic.example.com — same full proxy; PicHost middleware blocks non-image paths
server {
  server_name pic.example.com;
  location / {
    proxy_pass http://127.0.0.1:6892;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

# Default server: reject bare IP and unknown Host values (strongly recommended in production)
server {
  listen 443 ssl default_server;
  listen 80 default_server;
  server_name _;
  return 444;
}

Caddy, NPM, and similar tools work the same: proxy both hostnames to http://<host>:6892 and forward Host and X-Forwarded-Proto.

Security notes

Middleware isolation and third hosts

PicHost uses the request Host (and X-Forwarded-Host when proxied) to decide site vs image domain:

  • Image host: non-image paths (e.g. /, /settings) → 404
  • Site host: image URL paths (e.g. /images/...) → 404
  • Unconfigured third hosts (e.g. *.pages.dev, bare IP, old image domains): from v1.2.x the app returns 404 for all methods (localhost / 127.0.0.1 are exempt in development for npm run dev)

This is independent of hyphenated or long domain names. Older versions did not block third hosts; rely on a reverse-proxy default server as a complement in production.

Production recommendations:

  1. Configure server_name only for the site and image hosts; add a default server to reject other Host values and bare IP access (see Nginx example above; complements app-layer blocking)
  2. With Cloudflare orange-cloud DNS, restrict the origin firewall to Cloudflare IP ranges so traffic cannot bypass the CDN
  3. Before saving, read the risk confirmation in Settings / setup; saving via IP, LAN, or a proxy that does not forward Host correctly may lock you out — see Recovery when locked out below
  4. Sign in only on the site hostname from Settings; do not mix localhost, IP, or unlisted domains
  5. Do not put Cloudflare Pages / Workers in front of PicHost as a full-site reverse proxy (IMAGE_BASE_URL may point at a CDN URL, but the app itself should run on Docker/VPS)
  6. If you must proxy at the edge, preserve the client Host or X-Forwarded-Host (Lucky/NPM “force hostname” must match PicHost settings) or isolation fails or you get locked out after save
  7. PicHost cannot run as-is on Pages/Workers (needs node-server, SQLite, sharp, local data/). Keep Docker/VPS; use orange-cloud DNS and optional R2 storage on CF

Recovery when locked out

If IP / LAN access returns 404 after a bad save:

docker exec pichost clear-domains

Local: npm run clear-domains. Then reconfigure using the site hostname and verify proxy Host headers.

Cloudflare orange cloud (incl. preferred edge IPs)

Full guide: Cloudflare deployment.

Recommended: both admin.example.com and pic.example.com DNS Proxied → origin Nginx / 1Panel → port 6892, SSL Full (strict).

Feature Notes
Orange-cloud CDN Compatible with dual-domain; preserve Host and X-Forwarded-Proto on origin
DNS “preferred IP” setups Only changes which CF edge IP clients hit; origin identity must stay admin → admin, img → img
R2 storage Add an R2 backend under Storage; independent of orange cloud
cloudflare Git branch R2-focused deployment line; still Docker, not Workers hosting

Do not confuse “Create Worker” in the CF dashboard (Git + wrangler deploy) with deploying PicHost — the repo uses nitro.preset: 'node-server' and is not Workers-ready.

Do not chain the image hostname to fetch(admin…) via Pages/Workers — that rewrites Host and breaks isolation (bad topologies).

See Reverse proxy for origin hardening.

FN NAS · Lucky reverse proxy

In Lucky, create one reverse proxy rule per domain. Frontend is the public hostname; backend is PicHost on your LAN (e.g. http://192.168.8.3:6892).

Site domain (admin)

Field Example
Service type Reverse proxy
Frontend admin.pichost.com
Backend http://192.168.8.3:6892

Image domain

Field Example
Service type Reverse proxy
Frontend image.pichost.com
Backend http://192.168.8.3:6892

Both rules can point to the same backend; PicHost applies host-based isolation automatically.

Environment variables (optional)

SITE_BASE_URL=https://admin.example.com
IMAGE_BASE_URL=https://pic.example.com

See Environment variables.

Local testing (Windows / macOS)

1. Edit hosts

127.0.0.1 admin.pichost.test
127.0.0.1 pic.pichost.test

Prefer the .test TLD; avoid .local where it conflicts with mDNS.

2. Start dev server

npm run dev

Dev binds 127.0.0.1:3000 by default. If the hostname resolves but the browser cannot connect, check whether dev is listening on IPv6 ::1 only.

3. Configure dual domains

Field Example
Site URL http://admin.pichost.test:3000
Image URL http://pic.pichost.test:3000

Always open admin on the site hostname, not localhost (cookies and Host must match).

4. Expected behavior

URL Expected
http://admin.pichost.test:3000/ Admin UI works
http://admin.pichost.test:3000/images/... or /2026/08/xxx.webp 404 (use image host)
http://pic.pichost.test:3000/ 404 (image host has no admin)
http://pic.pichost.test:3000/images/... Image served
http://localhost:3000/ or http://127.0.0.1:3000/ Admin still loads (development exception; block unconfigured hosts at proxy in production)

After upload, copied links should use pic.pichost.test; gallery thumbnails should load.

Notes

  • Hostnames must differ (same machine, different subdomains is fine)
  • Site / image URLs in Settings must exactly match proxy server_name (including www or not)
  • No separate static root on the image domain; unlike EasyImages, PicHost isolates in-app
  • Single-domain setups can skip separation and use IMAGE_BASE_URL or the request host
  • When Referer protection is enabled, site and image hosts are whitelisted automatically

For single-domain proxy basics see Reverse proxy. Chinese version: 双域名分离