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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62992d974b73afe75be4eda6f45450bb0e2b67847cc8296199fecd60caaa738a
|
|
| MD5 |
f5138cd820c2ca425ab2c381e8347834
|
|
| BLAKE2b-256 |
9da3a127736d2cf2b3a0cf8a8c70990df96ad4063c14b945a867bfe8c2592c52
|
Provenance
The following attestation bundles were made for qhost-0.1.0.tar.gz:
Publisher:
release.yml on janfasnacht/qhost
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qhost-0.1.0.tar.gz -
Subject digest:
62992d974b73afe75be4eda6f45450bb0e2b67847cc8296199fecd60caaa738a - Sigstore transparency entry: 1625512367
- Sigstore integration time:
-
Permalink:
janfasnacht/qhost@7b783310a8b9f5776bec4efb2a312601060030cf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/janfasnacht
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7b783310a8b9f5776bec4efb2a312601060030cf -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09114fbf82c80b4b01e5a1458da31c81ecfa3f3cbf0ee466d5b7d971efb339b7
|
|
| MD5 |
60263e8c4c8d8f2d5db5e5ce61045779
|
|
| BLAKE2b-256 |
db17b0645a32117bfda5d4a3e933bdd443d64f0a8d933cd4d005f8ceb537eecd
|
Provenance
The following attestation bundles were made for qhost-0.1.0-py3-none-any.whl:
Publisher:
release.yml on janfasnacht/qhost
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qhost-0.1.0-py3-none-any.whl -
Subject digest:
09114fbf82c80b4b01e5a1458da31c81ecfa3f3cbf0ee466d5b7d971efb339b7 - Sigstore transparency entry: 1625512371
- Sigstore integration time:
-
Permalink:
janfasnacht/qhost@7b783310a8b9f5776bec4efb2a312601060030cf -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/janfasnacht
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7b783310a8b9f5776bec4efb2a312601060030cf -
Trigger Event:
push
-
Statement type: