npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hafenmeister

v0.1.0

Published

Spin up local Docker Compose environments with routed hostnames

Readme

hafenmeister

Spin up local Docker Compose environments with routed hostnames.

hafenmeister wraps Docker Compose with a shared Traefik reverse proxy and a dnsmasq wildcard resolver, so each project (or branch, or git worktree) comes up under its own domain — e.g. web.myproject.test — without editing /etc/hosts or remembering port numbers.

You describe your services in an ordinary compose.yml and add a small hafenmeister config that says which services to route and on which ports. hm up then:

  • brings up a shared infra stack (Traefik + DNS) once, reused across every project;
  • derives a slug for the environment from the directory name (or --slug);
  • selects the requested services (plus their depends_on closure), injects Traefik routing labels, and starts them on the shared network;
  • prints the URLs each routed service is reachable at.

Install

Requires Node.js ≥ 20 and Docker (or a drop-in like Podman).

npm install -g hafenmeister

This installs the hm command.

Quick start

  1. Add a hafenmeister.ts (or .js) next to your compose file:

    import { defineProjectConfig } from "hafenmeister/config";
    
    export default defineProjectConfig({
      composeFile: "./compose.yml",
      // start `web` on a bare `hm up` (no need to name it)
      alwaysOn: ["web"],
      // expose `web` (which listens on 3000) through Traefik
      routes: { web: 3000 },
    });
  2. Bring it up:

    hm up
    hafenmeister: infra is up
    hafenmeister: myproject up — web
      http://web.myproject.test
  3. Tear it down when you're done:

    hm down

For DNS to resolve *.test to Traefik, point your resolver at the dnsmasq instance hafenmeister runs (default: 127.0.0.1:5053). On macOS the usual approach is a /etc/resolver/test file:

nameserver 127.0.0.1
port 5053

Commands

hm up [services...]

Bring up the shared infra and the named services. With no services named, only the alwaysOn services (and the infra) start.

| Option | Description | | --- | --- | | services... | Service names to start — must match names in the compose file. Their depends_on services are pulled in automatically. | | -p, --path <path> | Path to the project. hafenmeister recurses upward to find the project config. Defaults to the current directory. | | -s, --slug <slug> | Slug identifying the environment. Derived from the project directory name otherwise. | | -d, --dry-run | Print the computed infra and project compose files to stdout without applying them. |

hm down

Tear down the project's stack. The shared infra is left running (it's shared across projects) unless --infra is passed.

| Option | Description | | --- | --- | | -p, --path <path> | Path to the project (recurses upward to find the config). | | -s, --slug <slug> | Environment slug to tear down. Derived from the project directory otherwise. | | --infra | Also tear down the shared infra stack (Traefik, dnsmasq, etc.). |

How domains are built

Each routed service is reachable at <host>.<domain>, where <host> defaults to the service name (overridable per route) and <domain> is assembled from:

<slug>[.<machine-host>][.<project-domain>].<localTld>
  • slug — derived from the project directory, or set with --slug. This is what gives each branch/worktree its own domain.
  • machine-host — the host's short name, included only when includeMachineHost is on (default off).
  • project-domain — the optional domain field in the project config.
  • localTld — the machine-wide TLD, test by default.

So a project in ~/code/myproject with domain: "tester" and a web route resolves to web.myproject.tester.test.

Project config

The project config lives at hafenmeister.ts/hafenmeister.js in (or above) your project. Use defineProjectConfig for type checking.

| Field | Description | | --- | --- | | composeFile | Path to your compose file, or a function returning the parsed compose object. (Required) | | routes | HTTP routes to expose through Traefik, keyed by service name. See below. A service not listed still starts if requested — hafenmeister never guesses ports. | | alwaysOn | Services to start on every hm up, regardless of what's named. | | domain | Extra label folded into the hosted domain (see above). | | env | Environment for the services: true (default) loads .env, false disables, or pass a Record<string, string> or a function returning one. | | localEnvFile | Local env file applied last (so it overrides env). Defaults to .env.local; set false to skip. |

Routes

routes: {
  web: 3000,                          // web.<DOMAIN> → port 3000
  api: { port: 8080, host: "graph" }, // graph.<DOMAIN> → port 8080
  admin: [                            // one service, several routes
    { port: 3000, host: "admin" },
    { port: 9000, host: "admin-metrics" },
  ],
}

Hostnames default to the service name and must all be unique. DOMAIN and SLUG are also injected into the compose environment, so your services can reference them.

Machine config

The machine config is shared across all projects and normally lives at ~/.config/hafenmeister (hafenmeister searches upward from the project for hafenmeister.machine.ts/.js). Every field is optional; the defaults below are what you get with no config at all.

| Field | Default | Description | | --- | --- | --- | | localTld | "test" | The TLD every service is hosted under. | | network | "hafenmeister" | Name of the Docker network Traefik and the project services share. | | http | { port: 80 } | Traefik HTTP listener. Set false to disable. | | https | off | Traefik HTTPS listener — { port?, cert, key }. When set, services are served over HTTPS with the given cert. | | dns | loopback :5053 | DNS resolvers, each answering *.<localTld> on a port (see below). false disables DNS. | | includeMachineHost | false | Fold the host's short name into service domains. | | infraComposeFile | none | Extra compose file whose services are merged into the shared infra stack — for machine-wide services every project shares, like a database. | | docker.command | "docker" | The Docker CLI to invoke (e.g. "podman"). |

DNS resolvers

The default is a single loopback resolver (127.0.0.0/8 on port 5053) for this host's browser. Setting dns replaces the default, so include the loopback entry if you still want it. Add more resolvers to reach services from elsewhere:

import { defineMachineConfig } from "hafenmeister/config";

export default defineMachineConfig({
  dns: [
    { port: 5053, subnet: "127.0.0.0/8" },     // this host's browser
    { port: 53, subnet: "100.64.0.0/10" },     // a Tailscale tailnet
    { port: 5354, answer: "10.0.2.2" },        // the Android emulator
  ],
});
  • subnet finds the host's IP within that CIDR, binds the resolver to it, and answers with that IP.
  • answer binds to loopback and answers with the fixed IP given.

Shared infra services

infraComposeFile lets you run machine-wide services — a shared Postgres, say — once and have every project reach them. The services are joined to the shared network, so a routed project service can reach them by name (postgres:5432). Service names can't collide with the built-ins (traefik, the DNS resolvers).

Development

npm install
npm run build      # compile to dist/
npm run dev        # compile in watch mode
npm run typecheck  # type-check without emitting
npm test           # run the vitest suite

License

MIT