Skip to main content

Self-hosted static-site host with a single trusted publisher.

Project description

qhost

qhost publishes a directory of static files — a built site, a rendered report, a coverage tree — at a URL that requires access and expires on a schedule. The operator runs one container, points a DNS record at their own reverse proxy, and manages shares from the command line. It is access-controlled hosting for a single trusted publisher per instance; it does not isolate mutually distrusting shares from one another.

Self-host the server

qhostd ships as ghcr.io/janfasnacht/qhost-server. Drop a docker-compose.yml and .env somewhere on your box and start it:

# docker-compose.yml
services:
  qhostd:
    image: ghcr.io/janfasnacht/qhost-server:0.1.0
    restart: unless-stopped
    environment:
      QHOST_DOMAIN: ${QHOST_DOMAIN:?}
      QHOST_TRUSTED_PROXY: ${QHOST_TRUSTED_PROXY:?}
      QHOST_TOKEN: ${QHOST_TOKEN:-}
    volumes:
      - qhost_data:/data
    ports:
      - "127.0.0.1:8000:8000"

volumes:
  qhost_data:
# .env
QHOST_DOMAIN=share.example.com
QHOST_TRUSTED_PROXY=127.0.0.1   # whatever IP / CIDR your proxy connects from
QHOST_TOKEN=                    # leave blank to auto-generate on first start
docker compose up -d
docker compose exec qhostd cat /data/operator-token   # if you left it blank

qhostd is now on http://127.0.0.1:8000 of the host, ready to be proxied. The full env-var surface is in deploy/.env.example and the canonical compose is in deploy/docker-compose.yml.

Wire a reverse proxy

qhostd doesn't terminate TLS, doesn't strip incoming X-Forwarded-For, and doesn't enforce a body-size cap before the request reaches Python. A proxy in front of it has to do those three things, and qhostd has to trust that proxy via QHOST_TRUSTED_PROXY (an IP or CIDR — see above).

Full ready-to-edit examples are in deploy/proxies/. The interesting line in each:

Caddy (deploy/proxies/Caddyfile)

{$QHOST_DOMAIN} {
    reverse_proxy 127.0.0.1:8000 {
        header_up X-Forwarded-For {http.request.remote.host}   # overwrite, not append
    }
    request_body { max_size 110MB }
    header Strict-Transport-Security "max-age=31536000; includeSubDomains"
}

nginx (deploy/proxies/nginx.conf)

proxy_set_header X-Forwarded-For $remote_addr;   # overwrite, not append
client_max_body_size 110m;

Cloudflare Tunnel (deploy/proxies/cloudflared.md)

ingress:
  - hostname: share.example.com
    service: http://127.0.0.1:8000

cloudflared connects from loopback, so QHOST_TRUSTED_PROXY=127.0.0.1 is what you want.

Install the CLI

pip install qhost
qhost login https://share.example.com

qhost login prompts for the operator token and writes it to ~/.config/qhost/config.toml (mode 0600).

Publish

cd path/to/built/site && qhost publish

qhost publish from a directory containing index.html publishes that directory as-is. From a Quarto project (_quarto.yml present) it runs quarto render first. Otherwise it refuses; pass --dir <path> to point at a built output or --build <name> to force a specific builder.

qhost --help covers the rest: publish, ls, info, extend, rm, open, doctor. Default expiry is seven days. --password gates a share behind HTTP Basic with a generated password; --public makes a guessable URL with no secret; the default link mode treats the URL itself as the credential — /s/<slug>/ carries ~80 bits of entropy and is not enumerable.

qhost publish --sandbox serves a share under Content-Security-Policy: sandbox allow-scripts, the only intra-instance isolation primitive. qhost doctor flags content that probably wants it.

Trust model

One operator token authorizes all publishing on an instance; every visitor is untrusted. All shares serve from the same browser origin, so a share's JavaScript can reach any other share on the same instance with the visitor's ambient Basic-auth credentials. For mutually distrusting publishers, run separate instances on separate origins.


Configuration: deploy/.env.example. Reverse proxy examples: deploy/proxies/. Vulnerability reports: SECURITY.md. MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qhost-0.1.0.tar.gz (69.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qhost-0.1.0-py3-none-any.whl (81.7 kB view details)

Uploaded Python 3

File details

Details for the file qhost-0.1.0.tar.gz.

File metadata

  • Download URL: qhost-0.1.0.tar.gz
  • Upload date:
  • Size: 69.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for qhost-0.1.0.tar.gz
Algorithm Hash digest
SHA256 62992d974b73afe75be4eda6f45450bb0e2b67847cc8296199fecd60caaa738a
MD5 f5138cd820c2ca425ab2c381e8347834
BLAKE2b-256 9da3a127736d2cf2b3a0cf8a8c70990df96ad4063c14b945a867bfe8c2592c52

See more details on using hashes here.

Provenance

The following attestation bundles were made for qhost-0.1.0.tar.gz:

Publisher: release.yml on janfasnacht/qhost

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qhost-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: qhost-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 81.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for qhost-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 09114fbf82c80b4b01e5a1458da31c81ecfa3f3cbf0ee466d5b7d971efb339b7
MD5 60263e8c4c8d8f2d5db5e5ce61045779
BLAKE2b-256 db17b0645a32117bfda5d4a3e933bdd443d64f0a8d933cd4d005f8ceb537eecd

See more details on using hashes here.

Provenance

The following attestation bundles were made for qhost-0.1.0-py3-none-any.whl:

Publisher: release.yml on janfasnacht/qhost

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page