- Python 100%
| coffeegit_pages_server | ||
| examples | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
coffeegit-pages-server
A small FastAPI/uvicorn service that serves “Pages”-style static sites from a Forgejo instance.
It supports:
- Pages subdomains:
https://{owner}.<PAGES_DOMAIN>/... - Repo subdomains:
https://{repo}.{owner}.<PAGES_DOMAIN>/... - Branch+repo subdomains:
https://{branch}.{repo}.{owner}.<PAGES_DOMAIN>/... - Custom domains mapped via DNS to your pages domain (CNAME or TXT)
- A raw domain for directly fetching files via a URL path scheme
_redirects(Netlify-style)_headers(Netlify-style)- Optional/unstable support for:
- symlinks (via Forgejo contents API)
- Git LFS pointer resolution
This project is intended to be deployed behind a reverse proxy (tested with Caddy) that terminates TLS and forwards requests to the service.
Installation
Using a virtualenv (recommended)
python3 -m venv /opt/coffeegit-pages/venv
/opt/coffeegit-pages/venv/bin/pip install --upgrade pip
/opt/coffeegit-pages/venv/bin/pip install git+https://git.private.coffee/privatecoffee/coffeegit-pages-server.git
Or install from a local checkout:
pip install .
Running
CLI entrypoint
The package installs a console script:
coffeegit-pages-server
It runs uvicorn on 0.0.0.0:${PORT} (default 8131).
Configuration
Configuration is via environment variables:
| Variable | Default | Meaning |
|---|---|---|
FORGE_ROOT |
https://git.private.coffee |
Forgejo root URL (no trailing slash) |
FORGE_API_TOKEN |
empty | Optional API token for Forgejo (recommended if you have rate limits / private repos) |
PAGES_DOMAIN |
coffeegit.page |
Base pages domain suffix used for subdomain routing |
RAW_DOMAIN |
raw.coffeegit.page |
Domain that enables raw routing mode |
PAGES_BRANCHES |
pages |
Space/comma-separated list of candidate branches to try (in order) |
ALLOWED_CORS_DOMAINS |
empty | If request Host matches one of these, Access-Control-Allow-Origin: * is returned |
BLOCKED_PATH_PREFIXES |
/.well-known/acme-challenge/ |
Deny requests for these path prefixes |
ENABLE_SYMLINK_SUPPORT |
false |
Resolve Forgejo symlinks to their targets |
ENABLE_LFS_SUPPORT |
false |
Detect Git LFS pointer files and serve the actual LFS object |
DEFAULT_MIME_TYPE |
application/octet-stream |
Fallback content type |
FORBIDDEN_MIME_TYPES |
empty | If guessed type is in this list, fallback to DEFAULT_MIME_TYPE |
ASK_STRICT_DOMAINS_FILE |
true |
/ask uses .domains validation instead of only “owner exists” |
ENABLE_FILE_CACHE |
true |
Enable in-memory body cache |
FILE_CACHE_TTL |
300 |
Cache TTL seconds |
FILE_CACHE_MAX_BYTES |
2097152 |
Max size to buffer/cache per response |
Routing behavior
1) Pages subdomain mode (*.PAGES_DOMAIN)
Assuming PAGES_DOMAIN=coffeegit.page:
-
https://alice.coffeegit.page/
Servesowner=alice,repo=pages,branchchosen fromPAGES_BRANCHES, path/ -
https://docs.alice.coffeegit.page/
Servesowner=alice,repo=docs,branchchosen fromPAGES_BRANCHES -
https://dev.docs.alice.coffeegit.page/
Servesowner=alice,repo=docs,branch=dev
Branch selection rules:
- If branch is explicitly encoded in the hostname (
{branch}.{repo}.{owner}) it is used. - Otherwise the server tries branches from
PAGES_BRANCHESand picks the first existing one.
2) Custom domain mode
For example.org, configure DNS so the server can map it back to a hosted pages target.
Supported DNS mapping methods:
-
CNAME to a hostname under the pages domain suffix, e.g.
example.org CNAME alice.coffeegit.page.example.org CNAME docs.alice.coffeegit.page.
-
TXT containing such a hostname (useful when CNAME isn’t possible), e.g.
example.org TXT "alice.coffeegit.page"example.org TXT "docs.alice.coffeegit.page"
Custom domains must also be allowed by a .domains file in the repo (see below), otherwise the request is rejected with 421.
3) Raw domain mode (RAW_DOMAIN)
Requests to RAW_DOMAIN use a path format:
https://RAW_DOMAIN/{owner}/{repo}[/@{branch}]/{path}
Example:
https://raw.coffeegit.page/alice/pages/@main/index.html
Raw mode forces Content-Type: text/plain; charset=utf-8.
Repository conventions
index.html and directory paths
- If the request is “directory-ish” (
/suffix), the server tries{path}/index.html. - If
index.htmlexists and the URL didn’t end in/, it redirects to the/form. - If a file isn’t found, it falls back to
404.htmlif present.
Also:
/somethingmay redirect to/something.htmlif that exists (GitHub Pages-like behavior)./foo/index.htmlredirects to/foo/(non-raw mode only).
.domains (custom domain allowlist + canonical choice)
If a repo contains a .domains file at the repository root on the served branch, its contents define which custom domains are valid.
- One domain per line
- Lines may include
http(s)://prefixes (they will be stripped) - Blank lines and comments (
# ...) are ignored
The first valid domain is treated as the canonical domain.
If .domains is missing or does not list the requested domain, the request is rejected with 421.
A fallback canonical is always appended: {owner}.<PAGES_DOMAIN> (and /{repo} for non-pages repos).
_headers (optional)
If _headers exists at repo root, it is parsed in a Netlify-like format:
/*.css
Cache-Control: public, max-age=86400
X-Test: hello
_redirects
_redirects at repo root is parsed in a Netlify-like format:
/blog /blog/ 301
/docs/* /documentation/:splat 302
Symlink support (optional)
If ENABLE_SYMLINK_SUPPORT is true, the server will resolve symlinks in the repo.
Git LFS support (optional)
If ENABLE_LFS_SUPPORT is true, the server will detect Git LFS pointer files and fetch the actual LFS object from the Forgejo LFS endpoint.
Endpoints
-
GET /ask?domain=example.org
Returns200 okor403 forbiddendepending on whether a domain would be considered authorized. This is used by Caddy’saskdirective for automatic TLS. -
GET /__inspect?url=https://...
Returns JSON describing how the service would route a given public URL. Add&probe_upstream=trueto also request the upstream raw file and report status/headers.
Running behind a reverse proxy
Run the service on localhost (for example 127.0.0.1:8131) and have your proxy forward requests to it. Ensure the proxy preserves the Host header correctly.
Your proxy should also handle TLS termination. For an example, see the Caddyfile example.
License
This project is licensed under the MIT License. See the LICENSE file for details.