Skip to content

owenstack/brimble

Repository files navigation

Brimble

A minimal PaaS-in-a-box. Submit a Git URL or upload a tarball, and Brimble will build it with Railpack, run the resulting container with Docker, and expose it through Caddy at a stable URL — with logs streaming live to the UI the entire way.

It is intentionally small: one backend service, one frontend page, one ingress, one database. Everything you need to play with the build/deploy loop and nothing you don't.


Quick start

# bring up the entire stack
docker compose up --build

# open the UI
open http://localhost:5173

On the very first run, the backend-builder container compiles the Encore service into a Docker image (one-shot, ~1–2 min). Subsequent docker compose up calls are fast: the image is reused.

When the UI is open, drop in a Git URL and watch the logs stream. There is also a sample app at examples/hello-node you can deploy by uploading the tarred-up directory:

tar -cf hello-node.tar -C examples/hello-node .
curl -X POST --data-binary @hello-node.tar http://localhost:4000/deployments/upload

Hard requirements — what you actually care about

  • docker compose up brings up the whole thing. Postgres, Caddy, the Encore backend (built on first run), and the Vite frontend.
  • Live log streaming over WebSocket. The backend exposes a streaming endpoint (api.streamOut) at /deployments/:id/logs. Each build, container start, and container stdout line is published through an in-process bus the moment it happens, fanned out to every active subscriber, and persisted in Postgres so you can scroll back later.
  • Railpack does the building. No hand-written Dockerfiles for user apps. The pipeline shells out to railpack build <src> --name <tag> and streams its output into the log bus.
  • Caddy is the only ingress. Backend talks to Caddy's admin API on :2019 to install/update/remove a route per deployment. The routes are rewritten so user apps see clean paths.

Architecture

                 ┌────────────────────────────────────────────┐
                 │  Browser                                   │
                 │  http://localhost:5173                     │
                 └─────────────┬──────────────────────────────┘
                               │  REST + WebSocket
                               ▼
   ┌──────────────────────────────────────────────────────┐
   │  backend (Encore.ts)                                 │
   │  - POST   /deployments               git URL         │
   │  - POST   /deployments/upload        tarball         │
   │  - GET    /deployments               list            │
   │  - GET    /deployments/:id           detail          │
   │  - GET    /deployments/:id/builds    history         │
   │  - POST   /deployments/:id/redeploy  rebuild         │
   │  - WS     /deployments/:id/logs      live logs       │
   │                                                      │
   │  pipeline:  source → railpack → docker run → caddy   │
   └─────┬──────────────────┬─────────────────────┬───────┘
         │ pg            docker.sock         caddy admin
         ▼                  ▼                     ▼
   ┌──────────┐    ┌────────────────┐    ┌─────────────────┐
   │ postgres │    │ user containers │    │ caddy (:8080)  │
   └──────────┘    │ brimble-<id>    │◀───│ /d/<id>/* →    │
                   └────────────────┘    │   container:port│
                                         └─────────────────┘

Why these pieces

  • Encore.ts for the backend. It gives type-safe endpoints, native WebSocket streaming (api.streamOut), automatic Postgres provisioning and migrations, and a generated client for the frontend. The trade is that "build a docker image" requires a one-shot init container with the host docker socket. That's encapsulated cleanly in backend-builder so the user still only types docker compose up.

  • Postgres (the bundled encoredotdev/postgres image) for state. Two tables: deployment and build, plus a deployment_log table that backs the persistent log buffer. Nothing here demands Postgres specifically — the schema is plain SQL and SQLite would work — but Encore's first-class Postgres support made it the path of least resistance.

  • Railpack as the builder. Replaces handwritten Dockerfiles with source detection. Output is a normal OCI image we can docker run.

  • Caddy as the ingress. The admin API makes per-deployment routes trivial: a single POST /id/<route-id> adds or replaces a route, no config files to edit and no reload.

Logs

The most interesting bit. Three layers, each does one job:

  1. lib/cmd.ts — a thin spawn wrapper that line-buffers stdout/stderr and invokes a callback per line. Used by every shell-out (git, railpack, docker run, docker logs).
  2. lib/logbus.ts — an in-process pub/sub keyed by deployment id. Every published line is persisted to deployment_log and fanned out to every subscribed stream. New subscribers can ask to replay from a seq cursor for scroll-back.
  3. api.ts logs endpoint — api.streamOut. On connect: replay history from the requested seq, then live-tail. Replay and live tail are de-duplicated by seq so subscribers can't miss or double-see a line.

This is single-process by design. Scaling out would mean swapping the in-memory bus for Postgres LISTEN/NOTIFY (or a real broker), which is a one-file change.

Project layout

backend/                       # Encore.ts service
  deployments/
    api.ts                     # public HTTP / WS surface
    pipeline.ts                # build/deploy orchestration
    store.ts                   # DB queries
    db.ts                      # SQLDatabase definition
    encore.service.ts
    lib/
      cmd.ts                   # streaming spawn()
      config.ts                # env-driven runtime config
      logbus.ts                # in-process log fan-out + persistence
      source.ts                # git clone / tar extract
      railpack.ts              # railpack CLI wrapper
      docker.ts                # docker CLI wrapper
      caddy.ts                 # caddy admin API client
    migrations/
      1_init.up.sql
  Dockerfile                   # tools layer on top of encore image
  infra.json                   # Encore self-host config (postgres, etc.)
caddy/
  Caddyfile                    # admin-only bootstrap
examples/
  hello-node/                  # smallest deployable app
docker-compose.yml             # the whole stack
frontend/                      # Vite + TanStack one-pager (separate)

Configuration

Defaults are baked in. Override via env vars (or a .env next to docker-compose.yml):

Var Default Purpose
BRIMBLE_PUBLIC_HOST localhost Hostname Caddy is reachable at
BRIMBLE_PUBLIC_PORT 8080 Public ingress port
BRIMBLE_DOCKER_NETWORK brimble Docker network for spawned containers
BRIMBLE_APP_PORT 3000 PORT baked into user containers
BRIMBLE_IMAGE_PREFIX brimble/ Image namespace
POSTGRES_PASSWORD brimble Postgres password

Things I'd do with another weekend

  • Authentication. Right now everything is open. Encore's authHandler would slot in cleanly.
  • Build cache reuse. Railpack supports BuildKit's cache mounts; exposing a persistent /root/.cache volume to the backend container would cut rebuild times dramatically.
  • Zero-downtime redeploys. Today's stopAndRemove then runContainer causes a ~1 s outage. Better: start the new container, wait for a health check, swap the Caddy upstream, then drop the old container.
  • Scoped deploy URLs. Path routing (/d/<id>/) is the simplest thing that works but breaks user apps with absolute paths in HTML. Subdomain routing via wildcard DNS (<id>.localhost) is one PR.
  • Tests. The pipeline modules are mockable — cmd, caddy, and docker are the seams I'd cover first.

Things I'd rip out

  • The custom layered Dockerfile / backend-builder sidecar dance. It exists only because encore build docker needs the docker daemon at build time. If we eject and own the runtime ourselves we can drop a good 60 lines of orchestration. Worth it the day Encore stops paying for itself.
  • Persisting log lines individually in Postgres. For long-running containers the deployment_log table will grow. A chunked layout (or pushing them out to S3/MinIO) is the right answer above toy scale.

Development (without Docker)

docker compose up is the supported path. If you want to iterate against encore run directly, the backend shells out to a few tools that you need on your PATH:

Tool Why Install
git clone source repos apt install git / brew install git
docker run / stop user containers https://docs.docker.com/get-docker/
railpack build OCI images from source trees curl -fsSL https://railpack.com/install.sh | bash

You also need a Caddy instance with the admin API on :2019 if you want the routing layer (e.g. caddy run --config caddy/Caddyfile --adapter caddyfile). Without it the build/deploy will succeed up to the routing step.

# in one terminal
cd backend && encore run

# in another
cd frontend && bun install && bun run dev

encore run provisions Postgres for you. The backend will fail with a clear "X not found on PATH" message if any of the above tools are missing.

Tests

cd backend && bun test

(Currently a thin smoke layer — see "Things I'd do with another weekend".)

Brimble (the company) deploy + feedback

See BRIMBLE_FEEDBACK.md for the deploy URL and write-up.

Time spent

Roughly one focused weekday. Most of it on the pipeline and the docker-compose dance around encore build docker. The endpoint surface, log bus, and Caddy integration came together quickly; the Dockerfile is the part that took the most "huh, does this actually work?" iterations.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors