Self-hosting
The public site does not install an offline copy in the browser. Two self-hosted options exist instead: a Docker image for a company that hosts the tools on its own network, and an offline zip with a small local server.
The self-hosted image
Section titled “The self-hosted image”The image contains the built site, served by server.mjs exactly as Cloudflare Pages serves the public site (the same _headers, _redirects, clean URLs and 404 page), in a distroless Node.js image that runs as a non-root user. The server only hands out files, stores nothing, contains no analytics or tracking and never connects to the internet. All work happens in each person’s browser.
What the installer needs
Section titled “What the installer needs”- A Linux server or virtual machine with Docker Engine 23 or later and Docker Compose v2 (the
docker composecommand). - About 1 GB of free disk space and 256 MB of memory (about 150 MB is used while running).
- No internet access, during installation or afterwards.
- For users: a current version of Chrome, Edge or Firefox.
Building a release package
Section titled “Building a release package”Building needs Docker 23 or later and git. From the repository:
node scripts/build/build-container.mjs # version: today's datenode scripts/build/build-container.mjs 2026.11.02 # or name itThis writes release/developer-tools-container-<version>/: the image as a .tar.gz, compose.yaml set to that version, INSTALL.md and SHA256SUMS. Send the customer that folder. To leave modules out for one customer, list them under "disabled" in features.json first; see Feature switches.
Installing
Section titled “Installing”1. Check the files. In the folder with the files:
sha256sum -c SHA256SUMSEvery line should end in OK. If one does not, ask for a new copy.
2. Load the image into Docker.
docker load -i developer-tools-<version>.tar.gzDocker prints Loaded image: developer-tools:<version>.
3. Start it.
docker compose up -dOpen http://<server name or address>:8080/. docker compose ps should show the service as running, and healthy within a minute.
HTTPS is recommended. Over plain http://, some browser features do not work: the hashing, key and certificate tools, the copy buttons and saving work to a folder. There are two ways to provide it.
A. A reverse proxy or load balancer with HTTPS (nginx, IIS, F5, Traefik and so on). Point it at port 8080 and serve the tools at the root of their own name, for example https://devtools.example.local/, not under a sub-path.
B. Your own certificate on the container.
- Make a folder named
certsnext tocompose.yaml. - Copy the certificate into it as
cert.pem(with any intermediate certificates after it) and the private key askey.pem. Both must be PEM files without a password. - The container runs as user 65532, so that user must be able to read the key:
Terminal window sudo chown 65532 certs/key.pemsudo chmod 400 certs/key.pem - In
compose.yaml, remove the#from the five lines under “HTTPS with your own certificate”, and change the port line to- "443:8080"(or keep 8080 and usehttps://<server>:8080/). - Restart:
Terminal window docker compose up -d
Settings
Section titled “Settings”All are optional. Set them under environment: in compose.yaml.
| Setting | What it does | Default |
|---|---|---|
PORT |
The port inside the container | 8080 |
TLS_CERT, TLS_KEY |
Paths to the certificate and key, to serve HTTPS | not set: HTTP |
ACCESS_LOG |
1 prints one line per request (method, address, status) to the container log |
off |
Updating
Section titled “Updating”- Load the new file:
docker load -i developer-tools-<new version>.tar.gz - In
compose.yaml, changeimage: developer-tools:<old version>to the new version. - Run
docker compose up -d. It replaces the running copy in a few seconds.
To go back, put the old version number back and run docker compose up -d again. The old image stays on the server until you remove it.
Running without a release package
Section titled “Running without a release package”To build and run the image from a checkout:
docker build -f container/Dockerfile -t developer-tools .docker compose -f container/compose.yaml up -d # http://localhost:8080/Without Docker, the same server runs on a build directly:
node scripts/build/build-pages.mjsSITE_DIR=dist node container/server.mjs # http://localhost:8080/Security notes
Section titled “Security notes”- No outbound traffic: the tools never connect anywhere, so you can block all outgoing traffic for the container in your firewall.
- Nothing is stored on the server: no accounts and no database. What people type stays in their own browser.
- The container runs as an ordinary user, its files cannot be changed while it runs, and it holds no special Linux permissions. The image contains no shell or package manager.
- By default it logs only when it starts and when something goes wrong.
- The same security headers as the public site are sent with every page, including a strict Content-Security-Policy and protection against being framed by other sites.
Kubernetes
Section titled “Kubernetes”Run the image with container port 8080 and keep these settings:
securityContext:runAsNonRoot: true,readOnlyRootFilesystem: true,allowPrivilegeEscalation: false,capabilities: {drop: ["ALL"]}- Probes: an HTTP GET on
/, port 8080. - HTTPS: terminate it at your ingress, or mount the certificate as a secret and set
TLS_CERTandTLS_KEY.
Troubleshooting
Section titled “Troubleshooting”| What you see | What to do |
|---|---|
docker compose up says the port is already allocated |
Another program uses that port. Change the left number in the ports line, for example "8081:8080". |
| Some tools or copy buttons do nothing | The address is http://. Switch on HTTPS. |
The container restarts, and the log says permission denied for key.pem |
Run the chown and chmod commands from the HTTPS section. |
| Anything else | Look at the log with docker compose logs and send it to your supplier with a description of the problem. |
Verification and tests
Section titled “Verification and tests”The repository tests the server against Cloudflare’s behaviour (scripts/tests/container-server-test.mjs: clean URLs, every _redirects rule, every _headers value, 404s, ETags, gzip, HTTPS, --health and shutdown). Its CI builds the real image, runs it locked down, and runs security-smoke.mjs and headers-check.mjs against it over HTTP and HTTPS. The container/ folder is not part of the public site and is not served by it.
Offline zip
Section titled “Offline zip”Separately, node scripts/build/build-offline-zip.mjs produces a zip of the built site with a small local-server launcher (run.sh, run.bat or serve.mjs) and a SHA256SUMS file. After launching, open http://localhost:8080. The tools need an HTTP origin (Web Workers and WASM), so the launcher serves locally rather than relying on file://. A copy served from localhost links the web app manifest, so the browser can add it as an app window; it needs no service worker.