# Terminus and the self-hosted server for TRMNL e-ink dashboards

> Terminus is a Ruby and Hanami web server that manages TRMNL e-ink dashboard devices on your own network or on your own cloud, built as the flagship Build Your Own Server implementation. It is still labelled Beta, it needs Postgres, Valkey, Node and headless Chromium, and it ships as a Docker Compose stack with a one-line install script.

**usetrmnl/terminus** — The flagship TRMNL BYOS application.

- Repository: https://github.com/usetrmnl/terminus
- Website: https://alchemists.io/projects/terminus
- Stars: 756 · Forks: 143
- Language: Ruby
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/usetrmnl-terminus

## A server for e-ink dashboards that stays on your network

Terminus is a web server for managing TRMNL devices, and it can run them on your own local network or on cloud infrastructure you control. The project describes itself as the flagship Build Your Own Server implementation, meaning it is the reference self-hosted option, and it aims to be compatible with the vendor's hosted Core server while deliberately differing from it, because the open source version is able to provide features that the Core application cannot.

The audience is specific. A team or individual who owns TRMNL e-ink display panels and wants to decide where the rendering happens, where the data lives and who can see the result. The privacy framing in the README is that you own the stack and the data, and the feature list leads with running your own private server, network and devices.

The feature comparison in the README is a table of Terminus against the hosted solution, and the picture is close to parity. Dashboard, Devices, Sensors, API, Screens, Playlists, Extensions, Models, Palettes, Firmware and Users are all marked as supported in Terminus. Two rows differ, and both are in Terminus's favour: Accounts is marked supported in Terminus and not supported in the hosted version, and Docker is marked supported in Terminus and not supported in the hosted version. The README notes that while the goal is compatibility with Core, the experience will differ in look, feel and behaviour, so treat the compatibility claim as a functional target rather than a pixel-identical clone.

## The stack behind the dashboard: Hanami, Postgres, Valkey and Chromium

Terminus is a Ruby application built on the Hanami framework, and the README lists its dependencies as a stack rather than a wish list. PostgreSQL holds the database. Valkey is the key/value cache and can be swapped for Redis or Dragonfly if you prefer. Puma is the web server. Rodauth handles authentication, and Sidekiq runs the worker jobs, which is where the screen rendering work belongs. esbuild handles asset management, htmx drives the hypermedia-driven user interfaces, and ImageMagick handles image processing. Overmind runs the multiple processes from a Procfile, and YJIT is supported for the Ruby runtime.

Two of those dependencies explain a lot about the operational shape. The first is headless Chrome, listed as an optional development requirement for screen generation via the headless browser, and installed in the container image alongside Chromium, ImageMagick and Noto CJK fonts. Rendering an e-ink screen is a browser problem, so a real browser is part of the stack rather than an optional extra. The second is the pair of Postgres and a key/value store, which means the server has real state to back up and migrate.

The JavaScript side is small and deliberate. The package.json for the project is marked private, uses ES modules, and depends on esbuild, htmx.org, alpinejs, several htmx extensions, the CodeMirror editor with JSON and Liquid language modes, and hanami-assets:

```json
{
  "name": "terminus",
  "description": "A TRMNL server.",
  "author": "TRMNL",
  "license": "MIT",
  "private": true,
  "type": "module",
```

The presence of a Liquid language mode in the editor hints at templating in the product, and the repository carries a slices/ directory and an app/ directory, which is the usual Hanami layout.

## The quick start script is a demo, not a deployment

The fastest way to see Terminus is a single command that pipes a script from the repository into bash:

```bash
curl https://raw.githubusercontent.com/usetrmnl/terminus/refs/heads/main/scripts/docker/quick.sh | bash
```

Once it finishes, you open http://localhost:2300 in a browser and click the Register link to create an account and sign in. The README is clear that this path is not idempotent, because the database credentials are generated differently on each run, so you cannot run the script twice. It is meant for getting up and running quickly to explore the feature set, and the README points to the Docker documentation for permanent use.

That caveat is the most important line in the quick start, and it is worth repeating to anyone who finds the script on a tutorial page. A generated credential per run means a second execution produces a second database, not a repaired one, and a team that treats the script as an installer will lose time discovering that.

The repository also ships a Dockerfile and a compose.yml, and the README carries a Deploy to Render button for hosted deployments. A render.yaml file at the root of the repository suggests the Render blueprint is a maintained path rather than a one-off.

## bin/setup and bin/upgrade for local development

For development on your own machine, the README gives a different path. You clone the repository, change into the directory and run the setup script:

```bash
git clone https://github.com/usetrmnl/terminus
cd terminus
bin/setup
```

The README states that this setup script is idempotent, so it can be run multiple times without harm, and that to rebuild a file the script manages you delete that file and rerun setup to recreate it. That is a useful property in a project that generates as much as it does, since the Ruby and Node versions, the assets and the environment files are all things a developer will need to regenerate.

Upgrading a checkout is a separate script:

```bash
cd terminus
bin/upgrade
```

The README's instruction here is to watch the output for changes that need to be addressed, which tells you that the upgrade is not guaranteed to run without manual steps. That is consistent with the Beta warning at the top of the README, which says the project has not reached 1.0.0 status and that although the maintainers strive to reduce the number of changes requiring effort on your part, there will be times when changes cannot be avoided.

For a self-hosted service that other people depend on, that is the sentence to hold on to. The project is explicit that it is pre-1.0 and that breaking changes will happen.

## Three Compose services and the healthcheck that guards them

The compose.yml file shows the intended topology. There is an init-certificates service that runs as root, installs certificates into a shared volume and restarts on failure, with a certificate URL list supplied through the environment. There is a web service and a worker service, both built from the same image at ghcr.io/usetrmnl/terminus:latest, and both wired to a shared certificates volume, a fonts volume and an uploads volume.

The web service block shows the environment contract:

```yaml
  web:
    init: true
    image: ghcr.io/usetrmnl/terminus:latest
    environment:
      HANAMI_PORT: ${HANAMI_PORT}
      API_URI: ${API_URI}
      APP_SECRET: ${APP_SECRET}
      APP_SETUP: true
      DATABASE_URL: postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@database:${DATABASE_PORT}/${DATABASE_NAME}
      KEYVALUE_URL: redis://:${KEYVALUE_PASSWORD}@keyvalue:${KEYVALUE_PORT}/${KEYVALUE_DATABASE}
```

Every one of those values is interpolated from your own environment, so a deployment that omits HANAMI_PORT, API_URI, APP_SECRET, the database variables or the key/value password produces a container that starts and then fails quietly. The Compose file also maps the port, declares the database and key/value services as dependencies with a healthy condition, and gives the web service a healthcheck that curls the /up endpoint:

```yaml
    healthcheck:
      test: ["CMD-SHELL", "curl --fail --silent http://localhost:${HANAMI_PORT}/up"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 45s
```

The resource limits are modest, a 1G memory ceiling and one CPU, which is a sensible starting point for a single-dashboard installation and a number to raise before rendering many screens.

## The container image bundles a browser and a database client

The Dockerfile shows how the runtime image is assembled, and it explains the resource requirements better than any prose would. The base layer is a slim Ruby image, with the version pinned through a build argument:

```dockerfile
ARG RUBY_VERSION=4.0.7

FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

ARG NODE_VERSION=24
```

The apt-get step in the base image installs chromium, curl, fonts-noto-cjk, git, gnupg2, imagemagick, libjemalloc2, locales, lsb-release, npm and tmux, adds the PostgreSQL apt repository and installs postgresql-client-18 along with nodejs from the NodeSource setup script. A build stage then adds build-essential, libpq-dev, libyaml-dev and pkg-config before the gems are installed.

Two details in that list have practical consequences. Chromium is a heavyweight dependency that most other Ruby applications would not need, and it exists for the screen rendering path. fonts-noto-cjk is there so that CJK text renders on the generated screens instead of turning into empty boxes, which is a small detail that only matters if your dashboard shows Chinese, Japanese or Korean content, and a common one for the audience of an e-ink dashboard product.

The environment defaults are also explicit. RACK_ENV and HANAMI_ENV are set to production, HANAMI_SERVE_ASSETS is on, and BUNDLE_DEPLOYMENT is set with BUNDLE_WITHOUT listing the development, quality, test and tools groups, which is how the image avoids shipping test dependencies. The .ruby-version and .node-version files at the root of the repository, plus a .hadolint.yml for Dockerfile linting and a .reek.yml for Ruby code smells, show that the container and the code are both linted in continuous integration, with CircleCI configuration at the root.

## Where Terminus sits, and what it asks of the operator

Terminus is not a general-purpose dashboard server. It exists to drive TRMNL devices, and the README links a whole documentation set for that narrower world: doc/api.adoc for the API, doc/configuration.adoc, doc/devices.adoc, doc/screens.adoc, doc/render.adoc, doc/extensions.adoc, doc/jobs.adoc, doc/docker.adoc, doc/raspberry_pi.adoc and doc/internationalization.adoc, with a development guide and a translations guide. The presence of a Raspberry Pi document tells you the intended deployment target for a single panel, and the presence of a translations document tells you the panel software is multilingual.

The alternative to running it yourself is the vendor's hosted offering, and the project publishes a comparison rather than pretending the choice does not exist. The differences are structural: the hosted version is marked as not supporting accounts and not supporting Docker, and Terminus's own account system is one of the features the open source build adds.

The cost of self-hosting is therefore concrete. You run Postgres, a key/value store, Node, a headless browser and image tooling, you own the upgrades, and you own the moment a pre-1.0 release changes something. The project is not archived and the last push was on 2026-10-19, so the code is moving, and the repository publishes no GitHub releases, which means the history of what changed lives in the commit log and in the Docker tags rather than in a release page.

The licence is MIT, recorded in LICENSE.adoc at the root of the repository and in the package metadata. MIT is a permissive licence that allows commercial use and modification, with the obligation to keep the copyright notice, and that obligation matters here because the project is a hosted-service-shaped application that other people may run. Reading the licence file and having it reviewed is the right step rather than treating a summary as advice.

## Conclusion

Adopt Terminus if your goal is to keep dashboard devices, their data and the server that drives them inside your own network, and if you are willing to operate Postgres, a key/value store, Node and a headless browser yourself. Skip it if you want a hosted dashboard with no infrastructure to maintain, since the project's own comparison marks the hosted offering as the supported path for accounts, and skip it if a pre-1.0 project with occasional breaking changes is a problem, because the README says so outright. Verify first that the quick start script gives you a working registration at http://localhost:2300, and that your own deployment sets HANAMI_PORT, API_URI, APP_SECRET, DATABASE_URL and KEYVALUE_URL, since the Compose file expects every one of them from the environment.

## FAQ

### How do I install the Terminus TRMNL server?

The README gives a one-line Docker start using a script piped into bash, which is not idempotent and is meant only for exploring the feature set, and a development setup that clones the repository and runs bin/setup, which is idempotent and can be rerun safely.

### What does Terminus need to run besides Ruby?

The requirements list Git, Ruby, Hanami, Node, PostgreSQL and Valkey, with Valkey swappable for Redis or Dragonfly. Docker is optional when only using Ruby, and a headless Chrome is needed for screen generation.

### Is Terminus compatible with the hosted TRMNL Core server?

The README says Terminus aims to be compatible with the Core server but is also deliberately different, and warns that the experience will differ in look, feel and behaviour. Its comparison table marks Accounts and Docker as supported in Terminus and not supported in the hosted version.

### Which environment variables does the Terminus container expect?

The compose.yml web service expects HANAMI_PORT, API_URI, APP_SECRET, a DATABASE_URL built from the database user, password, port and name, and a KEYVALUE_URL built from the key/value password, port and database, all supplied through the environment.

## Sources

- [Issues](https://github.com/usetrmnl/terminus/issues)
- [License: MIT](https://github.com/usetrmnl/terminus/blob/main/LICENSE)
- [Project website](https://alchemists.io/projects/terminus)
- [README](https://github.com/usetrmnl/terminus/blob/main/README.md)
- [usetrmnl/terminus on GitHub](https://github.com/usetrmnl/terminus)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/usetrmnl-terminus
