# 26 JAN 2026 · APPS

Building serve: a local server in one command

Why a simple local web server is still useful, and the decisions behind serve: one binary, HTTPS without OpenSSL, a LAN-reachable default with a QR code, and why it is not for production.

>_[ FIG. 00 · APPS ]×
One terminal command serves a folder on 0.0.0.0:8080, reachable from a browser on the same machine and from a phone on the same Wi-Fi networkTERMINAL$ serve ./siteLISTEN http://0.0.0.0:8080LAN http://192.168.1.20:8080GET /index.html 200GET /style.css 200GET /missing.js 4041ILLUSTRATIVE, NOT REAL OUTPUTindex.htmlstyle.cssimg/./SITE2YOUR MACHINELOCALHOST:8080LOOPBACK3192.168.1.20LAN, ON BYDEFAULT (QR CODE)4PHONE, SAME WI-FI1ONE COMMAND2ONE FOLDER,NOTHING ABOVE30.0.0.0:8080 BYDEFAULT4--AUTH AND --TLSON SHARED WI-FIDIAGRAM

A local web server is one of those tools every developer needs a few times a week and nobody thinks about. You have a folder of HTML, CSS and JavaScript, and you want to see it in a browser the way it will really load, over HTTP, not from a file:// path. serve is our small answer to that: a single static binary that serves a folder over HTTP or HTTPS, on macOS, Windows and Linux, under the MIT licence. This post is about why we think the job deserves a dedicated tool, and the decisions that shaped it, including one default we know some people will disagree with.

serve is one of our desktop apps. The flags may grow over time. The reasoning should not change much.

Why you need a local server at all

Opening an HTML file directly in a browser works, up to a point. Then it stops. Browsers treat pages loaded from file:// very differently from pages served over HTTP, mostly for good security reasons. A few things that commonly break:

  • JavaScript modules. A <script type="module"> loaded from a file path is blocked in most browsers, because module loading follows CORS rules and a local file has no proper origin.
  • fetch() and XHR. Loading a JSON file next to your page usually fails from file://.
  • Service workers. They need a secure context. http://localhost counts as one. A file path does not.
  • Root-relative links. A link to /about points at the root of your disk, not the root of your site.

A local server fixes all of that by giving your folder a real origin. It also lets you check things you cannot check any other way: that your MIME types are right, that your 404 page appears, that a WebAssembly file loads with application/wasm so the browser can compile it while streaming.

The second reason is sharing. You finish a page and want to see it on your phone, or hand a folder of files to someone across the desk. Deploying to a staging host for that is overkill. Serving it on your local network for five minutes is not.

One binary, one command

The idea is that serving a folder should be a single action: point at the folder, get a URL. What matters is what you do not have to do: write a config file, install a runtime, set up a project.

There are plenty of one-liners that almost do this. python3 -m http.server is probably the best known, and it is fine for a quick look, if Python is installed and you remember the module name. The Node and Ruby equivalents have their own quirks, and they all assume you have that runtime. serve is one native executable with no runtime behind it. Copy it onto a machine and it works.

These are the commands we actually use:

serve                            # the current folder, HTTP on port 8080
serve -p 9000 ~/Downloads        # another port, another folder
serve --tls /root                # HTTPS
serve --upload .                 # also accept uploads
serve --auth alice:secret --tls  # password, over HTTPS

Zero config is not the same as no decisions. It means someone made the decisions for you, and made them carefully. That is where most of the work in a tool like this goes.

The basics a local server has to get right

Each of these is small. Together they are the difference between a tool you trust and one you have to think about.

Correct content types. HTML, CSS, JavaScript modules, SVG, fonts, JSON, WebAssembly and video all need the right Content-Type. A surprising amount of "it works on the real server but not locally" comes down to one wrong MIME type.

Range requests. Browsers ask for parts of a file when you scrub through a video or resume a large download. A server that ignores the Range header makes video previews jump back to the start and big downloads restart from zero. serve supports them.

Directory listings. Open the URL and see your files. For a folder of assets, that is often the whole point.

HTTPS without the setup. --tls makes serve generate a self-signed certificate in memory at startup, using rustls rather than OpenSSL. Nothing is written to disk and there is nothing to install. The browser will warn that it does not recognise the certificate, which is correct: it is not signed by anyone it trusts. For local testing that warning is the fair price for encryption with no configuration.

Why it listens on the whole network

This is the default that needs explaining. serve binds to 0.0.0.0:8080, which means every network interface, not only loopback. Other devices on the same network can reach it.

Many tools do the opposite, and we understand why. A server on 127.0.0.1 can only be reached from the same machine, which is the safest possible starting point. But the job we built serve for is mostly the second one above: getting a page onto a phone, or a file onto another laptop, right now. A loopback-only default would make that job need a flag every time, and a tool whose main use needs a flag is a tool people stop reaching for.

So serve starts reachable and says so. At startup it prints a QR code. Point a phone camera at the terminal and the page opens. That is the whole sharing flow, and it only works because the server is listening on the network.

The trade-off is real. On a home network, a LAN-reachable server is usually fine. On café Wi-Fi, a conference network or a shared office, anyone on the same network who finds the port can browse the folder you are serving. People start a quick server in their home directory more often than you would think, and then forget it is running. We do not want to pretend that away, so here is how we think about using it safely:

  • Serve the folder, not your home directory. Point serve at the project, serve ./site, rather than starting it wherever the terminal happens to be.
  • Add a password on shared networks. --auth alice:secret puts HTTP Basic authentication in front of everything.
  • Add --tls with it. Basic auth over plain HTTP sends the password in a form anyone on the network can read. Over HTTPS it is encrypted.
  • Uploads are off unless you ask. serve only accepts files with --upload. By default, nobody on the network can write to your disk through it.
  • Stop it when you are done. Ctrl+C. A server you forgot about is the most common way this goes wrong.

On macOS and Windows, the system firewall may also ask whether to accept incoming connections the first time. That prompt is a feature, not an annoyance. It is the operating system checking that you meant it. If you only want the machine you are sitting at, deny it.

One more caveat about phones: a page loaded from http://192.168.1.20:8080 is not a secure context, because it is plain HTTP on a non-local address. Service workers, camera access and some clipboard APIs will not work there, even though they work on localhost. With --tls the page is served over HTTPS, but the phone still has to accept a self-signed certificate first, and some features stay picky about that.

What not to put in the folder

Directory listings are handy, and they are also the easiest way to leak something you did not mean to share. A typical project folder contains more than the site. There might be a .git directory with the whole history, a .env file with API keys, editor backups, notes, or client assets that are not public yet. With a .git folder exposed, someone can often reconstruct the entire repository.

The simplest defence is the one above: serve the build output or a folder made for sharing, not the repository root. If you need to share part of a project, copy it somewhere first. It takes ten seconds and removes the question entirely.

Not for production

A local server is a development and sharing tool. It is not a web host, and we think it should say so rather than drift into being one. Production static hosting needs a trusted certificate, long cache lifetimes, compression, a CDN, sensible security headers and something that restarts when the process dies. Tools built for that, and the hosting platforms that do it for you, handle those far better than a small binary should try to.

Keeping that line clear is also what lets serve stay small. Every time a feature request drifts towards production, the answer is that you have outgrown the tool, and that is fine. If you need a proper deployment pipeline or a custom internal tool around one, that is the kind of thing we build under custom development. For everything before that point: start it in the right folder, scan the QR code, and press Ctrl+C when you are done.