Skip to content

Apps

works, rough edges

An app is any Dockerfile. Hobbyist builds it, gives it a hostname, and sleeps it when nothing is asking for it. Cold start measured at p50 121ms, p95 133ms on an Apple M5 Pro, 2026-08-10.

Terminal window
cd ./my-app
hobby deploy

The kind is detected from what is in the directory rather than asked for, because you already said which you meant by what you wrote: a Dockerfile means an app, a wrangler.toml or wrangler.jsonc means a worker. Both present is an error that asks which, rather than a guess, because guessing wrong means building the wrong thing and finding out when it fails to serve.

Terminal window
hobby deploy ./my-app --project blog --name site --port 8080
Flag What
--project <p> Which project it belongs to
--name <n> The resource name
--port <n> The port your process listens on inside the container
--env-file <f> Variables from a dotenv file, see below
--kind app|worker Only needed when the directory is ambiguous
--json Machine-readable output
Terminal window
hobby deploy ./my-app --project blog --env-file .env.production

The file is read on the machine you run hobby from and sent to the daemon, so it never has to exist on a remote box. One KEY=VALUE per line. Blank lines and # comments are skipped, an export prefix is allowed, and one pair of matching quotes around a value is stripped. Nothing is interpolated: $OTHER arrives as written.

On a redeploy the file replaces the app’s variables as a whole, so a key you delete from it is gone afterwards. A redeploy without --env-file keeps what the app already has. Values are never printed, only a count.

A database bound with --database always supplies DATABASE_URL, even if the file sets one too. Workers take their variables from [vars] in their wrangler config instead, and --env-file refuses them.

Nothing special is required. The one thing that matters is that your process listens on the port you told Hobbyist about, and that it binds 0.0.0.0 rather than 127.0.0.1, because a process bound to loopback inside a container is unreachable from outside it.

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]
Terminal window
hobby create app site --project blog

That writes a resource row with an id and a hostname and no code, resting in the state undeployed. hobby ls marks it (no code yet).

This exists because creating a resource and deploying code to it are two acts, not one. Before the split, the daemon refused to write a row without a build source, which meant Studio and MCP could not create an app at all, having no filesystem path to hand over. ADR 0014.

There is deliberately no --port on hobby create. A port describes your code, and your code does not exist yet.

An app has no data directory. It gets its persistence from Postgres. Volumes are Phase 3, which keeps volume lifecycle out of the phase that was already the hardest. ADR 0007.

An app is reached by hostname rather than by port, because its port is an implementation detail nobody wants to type. In front of that sits either Caddy for public HTTPS, or Tailscale for a private lane.

Deliberately. A push-to-deploy flow needs a receiving repository, a hook, and a build triggered by someone else’s commit, none of which is one box’s worth of complexity. hobby deploy from the directory you are already standing in is the same number of keystrokes.