Hosting on localhost
This is the simplest way to run plandrop: the whole stack on one machine, over plain
HTTP, reachable at http://localhost:8083. No TLS, no DNS records, no external reverse
proxy. It's also the foundation the larger guides build on — once this works, putting a
real TLS proxy in front is the only thing that changes.
How it fits together
The stack is four containers sharing one data volume:
| Service | Role |
|---|---|
| ingress | A dockerised nginx — the control entrypoint. Serves /.plandrop/* template statics and reverse-proxies /api/* to the control plane over the internal docker network. |
| apache | A dockerised Apache mod_dav host. One dynamic vhost serves every subdomain: open reads, per-tenant authenticated WebDAV writes. Served content is purely static. |
| control | A small Node/Hono service: the only writer of the content tree and the credentials file. It mints hosts (create) and changes/removes them (rotate, remove). Internal-only — reached solely through the ingress. |
| proxy | The testproxy front proxy — one nginx container that routes by Host on a single published port: the bare parent domain → ingress, *.<parent> → apache. It stands in for the TLS proxy a larger deployment puts in front, and it's the same proxy the automated test harness uses. |
In a bigger deployment a TLS-terminating reverse proxy you run (NPM, Cloudflare, plain
nginx) plays the proxy role. On localhost the bundled proxy container plays it for
you, over plain HTTP, so a browser and the CLI share one origin — and nothing else
about the stack differs.
Requirements
- Docker + Docker Compose. Every image builds inside Docker (multistage), so no Node toolchain is needed on the host — it builds on x86-64 or arm64 alike.
- That's it.
*.localhostresolves to loopback on macOS and modern Linux, so tenant subdomains reach the proxy with no DNS records and no/etc/hostsedits.
Bring the stack up
git clone https://github.com/Xalior/plandrop.git
cd plandrop
cp .env.example .env # first time only; the defaults are fine for localhost
mkdir -p data/hosts data/auth # or point PLANDROP_DATA at an existing dir
docker compose --profile testproxy up -d --build
The testproxy profile is what adds the proxy container — a plain docker compose up
(no profile) starts only ingress + apache + control and leaves you without the single
shared origin, so include the profile here.
This builds the ingress, control, and proxy images, pulls Apache, and starts all four
containers bound to 127.0.0.1. To tear it back down (and drop the seeded template
volume):
docker compose --profile testproxy down -v
Push your first document
Point the client at the one proxy origin — http://localhost:8083 — and it handles both
the control calls (to the ingress) and the uploads (to apache) over that single port:
mkdir my-plan && cd my-plan
# mint a host (parent -> ingress -> control); writes ./.plandrop at mode 0600
npx plandrop create --domain http://localhost:8083
# scaffold a doc from the default template, or upload your own finished HTML
npx plandrop newdoc index.html
npx plandrop upload .
create prints the shareable URL — http://<label>.localhost:8083/. Open it in a browser
on the same machine and your document renders, served with zero server-side logic. See the
client quick start for the full command set.
Tenant subdomains use
localhost, which only resolves on this machine. To reach a doc from a phone or another laptop you need a wildcard domain pointing at this host's LAN IP — that's a small step beyond localhost, covered in the next guide.
Configuration (.env)
The defaults work as-is for localhost. The ones you might touch:
| Variable | Meaning | Default |
|---|---|---|
PLANDROP_PROXY_PORT |
The single host port the proxy publishes — the origin the browser and CLI both use. | 8083 |
PLANDROP_PROXY_DOMAIN |
The bare parent domain the proxy routes to the ingress; everything else (*.<domain>) routes to apache. |
localhost |
PLANDROP_BIND |
Host address the services bind on. | 127.0.0.1 |
PLANDROP_DEFAULT_TEMPLATE |
Template applied to a host's autoindex chrome when none is requested. | bootstrap5 |
PLANDROP_USER_TEMPLATES |
Host path to operator drop-in templates, layered over the built-ins. | ./user-templates |
PLANDROP_DATA |
Host path to the data root (holds hosts/ and auth/). Keep it out of version control. |
./data |
PLANDROP_UID / PLANDROP_GID |
The user/group the containers run as, and that the data/ tree should be owned by. |
1000 |
Data layout
<data>/hosts/<label>/www/ per-tenant content (served docroot)
<data>/auth/htpasswd shared credentials (bcrypt, one line per host)
The control plane creates host directories and writes the credentials file; Apache reads
the credentials and serves/accepts writes under hosts/. Both run as PLANDROP_UID so
ownership stays consistent. Back up the data/ tree to preserve hosted content and
credentials.
⚠️ Security:
createhas no authentication — anyone who can reach the proxy origin can mint a host. On localhost that's just you. The moment you expose this beyond the loopback interface, restrict who can reach it (LAN-only, VPN, or a proxy access list). Per-host writes are always authenticated by the generated passphrase; reads are public by design.
Operating
| Task | Command (from the stack directory) |
|---|---|
| Update to the latest version | git pull && docker compose --profile testproxy up -d --build |
| Restart | docker compose --profile testproxy restart |
| Logs | docker compose --profile testproxy logs -f |
| Stop | docker compose --profile testproxy down (data in data/ persists) |
Next step
Putting a TLS proxy in front — so docs are reachable over HTTPS from any device on your network — is the only thing that changes from here. That's a separate guide (plain nginx in front of this exact stack), coming next.