# FrameOS: an operating system for single function smart frames

> FrameOS is an AGPL-3.0 TypeScript and Python project that turns a Raspberry Pi plus a display into a self-contained smart frame. It is built for both 60-second e-ink refreshes and 60-frames-per-second screens, and it is deployed from a dockerized backend over SSH.

**FrameOS/frameos** — Operating system for single function smart frames. Pimoroni e-ink frames Waveshare e-ink Framebuffer HDMI output Web server kiosk mode See the full list here!

- Repository: https://github.com/FrameOS/frameos
- Website: https://frameos.net/
- Stars: 452 · Forks: 13
- Language: TypeScript
- License: AGPL-3.0
- Published: 2026-08-08 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/frameos-frameos

## What FrameOS solves, and who it is actually for

A Raspberry Pi driving a display is easy to wire up and annoying to keep running. You end up with a cron job, a Python script, a systemd unit and no way to see what any of it is doing. FrameOS packages the whole loop: an operating system image for the device, a backend that deploys apps onto frames over SSH, and a set of prebuilt scenes you can push without writing a renderer yourself.

The README frames the target as single function frames, meaning one job per screen. Smart home calendars, meeting room displays, thermostats, industrial dashboards, public advertisement screens. The common thread is that nobody wants to log into the device. You set it up once, it renders forever, and the backend is only needed when you want to change something.

It is not a general purpose Linux distribution. The supported output paths are narrow on purpose: Pimoroni e-ink frames, Waveshare e-ink, framebuffer HDMI output and a web server kiosk mode. If your display is not in that list, the device hardware guide is the place to check before you buy anything.

## How the backend, the frames and the SSH deploy path fit together

The architecture splits into two halves that fail independently. The backend is a dockerized Python application that holds your frame definitions and scenes. The frames are the Raspberry Pi devices, each running FrameOS itself. Deployment happens over SSH from the backend to the device.

That split is the most interesting design decision in the project. The README states that the frames run independently, and that running the backend continuously only matters for log aggregation. If the backend is offline, frames keep rendering whatever was last deployed. For a meeting room display or a wall calendar, that is the right trade-off: a server outage should not blank the screen.

The cost is that the backend is not a live control plane. You cannot push a change while the server is down, and you have no central view of what each device is doing until it reconnects. The README is explicit that you miss out on log aggregation when the backend is offline, but it does not document any queueing or retry behaviour for deployments, so treat an offline backend as a period where changes simply wait.

Underneath, the device side is not only Python. The repository ships a frameos/ directory with Nim code and a frameos/remote/ subtree, and the Dockerfile installs a pinned Nim toolchain (NIM_VERSION 2.2.4) plus ESP-IDF v5.5.4 for embedded firmware targeting esp32s3 and esp32c3. The comment in the Dockerfile says ESP32-S3 boards render locally while ESP32-C3 boards are thin clients. So FrameOS spans three execution environments: the Python backend, the Nim renderer on the Pi, and optional ESP32 firmware.

## Installing the FrameOS backend with the install script or Docker

The README gives two paths. The quick install script is the shortest one for macOS or Debian and Ubuntu Linux:

```bash
bash <(curl -fsSL https://frameos.net/install.sh)
```

Running it downloads and executes the installer from frameos.net. The README does not describe what the script does beyond installing the backend, so if you need to know exactly what lands on disk, use the Docker route instead.

The manual Docker route generates a secret key, creates a local db directory and starts the container on port 8989 with a restart policy:

```bash
SECRET_KEY=$(openssl rand -base64 32)
mkdir -p db
docker run -d -p 8989:8989 \
    -v ./db:/app/db \
    --name frameos \
    --restart always \
    -e SECRET_KEY="$SECRET_KEY" \
    frameos/frameos
```

After this, the backend is listening on port 8989 and its database lives in ./db on the host. Note that SECRET_KEY is generated fresh each time you run the command; if you recreate the container without reusing the same value, treat that as a new install rather than a restart.

There is also a compose file in the repository that builds from the local Dockerfile rather than pulling the published image, and mounts a named volume frameos-db instead of a bind mount:

```yaml
services:
  frameos:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8989:8989"
    volumes:
      - frameos-db:/app/db
volumes:
  frameos-db:
```

That distinction matters when you are choosing between the two: the compose path compiles the Nim toolchain stage, which pulls a prebuilt Nim archive and verifies its sha256 against .github/nim-prebuilt.sha256 before extraction. The published image skips that work.

For development rather than deployment, the repo ships a checked-in Flox environment. Running flox activate bootstraps the toolchains and the activation hook creates a local .venv, installs backend/requirements.txt, runs pnpm install --frozen-lockfile and installs the Nim dependencies for frameos/ and frameos/remote/. If you want Redis managed by Flox, the README gives flox services start redis. Otherwise pnpm dev starts the workspace through mprocs, and individual pieces have their own scripts: pnpm dev:backend, pnpm dev:worker, pnpm dev:frontend, pnpm dev:frame-frontend.

## Building SD card images and what the partition layout commits you to

FrameOS generates SD card images, and the README documents the layout in enough detail to plan around. Four partitions: p1 is FAT32 boot, p2 is the ext4 root filesystem, p3 is ext4 mounted at /srv/frameos, and p4 is FAT32 mounted at /srv/assets.

On first boot the image expands to fit the card, but not uniformly. The root partition stays at the image size. p3 grows to 2 GiB on cards 4 GiB and larger, and p4 is recreated to fill whatever remains. On cards smaller than 4 GiB, p3 stays at 1 GiB and p4 still takes the rest. If you plan to store a lot of assets locally, that 4 GiB threshold is the number to remember, because below it the runtime partition is capped.

Buildroot image generation can use a cached image with dependency packages preinstalled and the Buildroot tarball preloaded at /frameos-buildroot. Several environment variables control it, including FRAMEOS_BUILDROOT_IMAGE_REPO (default frameos/frameos-buildroot), FRAMEOS_BUILDROOT_IMAGE_TAG (default latest), FRAMEOS_BUILDROOT_FORCE_LOCAL_BUILD=1 to force a local rebuild, and FRAMEOS_BUILDROOT_SKIP_PULL=1 to skip pulling from the registry. Two size knobs are exposed directly: FRAMEOS_BUILDROOT_FRAMEOS_PARTITION_SIZE defaults to 1G and FRAMEOS_BUILDROOT_ASSETS_PARTITION_SIZE defaults to 512M.

Cross-compilation follows the same pattern with toolchain containers from frameos/frameos-cross-toolchain. The image name resolves as {repo}:{base}_{version}-{platform}-{tag}, where platform is the Docker platform with the slash replaced by an underscore, so linux_amd64 or linux_arm_v7. You can override with FRAMEOS_CROSS_TOOLCHAIN_IMAGE, FRAMEOS_CROSS_TOOLCHAIN_IMAGE_REPO, FRAMEOS_CROSS_TOOLCHAIN_IMAGE_TAG, FRAMEOS_CROSS_TOOLCHAIN_FORCE_LOCAL_BUILD=1 and FRAMEOS_CROSS_TOOLCHAIN_SKIP_PULL=1. The README notes that SD card image generation works in the default container without Docker privileges, and that enabling Docker access only speeds up source builds by letting FrameOS spin up build environment containers.

## Where FrameOS is the wrong choice

The strongest limitation is the one the README states as a feature: frames run independently. That means the backend is not a source of truth you can query at any moment. If you need to know that a screen is showing the right content right now, FrameOS gives you log aggregation while the backend is up and nothing while it is down. There is no described heartbeat or health endpoint, so building alerting on top of it means adding your own mechanism.

The second limitation is documentation depth on operations. The README covers installation, image building and toolchain overrides in detail. It does not document rollback, does not document how to downgrade a frame after a bad deploy, and does not describe what happens to a frame mid-deployment if SSH drops. Given that deployment is over SSH and releases are frequent (three releases between 2026-08-27 and 2026-08-28), you should expect to manage version drift across devices yourself.

The third is scope. FrameOS assumes a Raspberry Pi and one of the supported display paths. If you have a commercial smart frame, or a display whose driver is not in the device list, this is not the project for you. The README points to frameos.net/devices as the authority and does not claim broader support than that.

Finally, the licence is AGPL-3.0. That is a copyleft licence with a network clause. If you plan to run modified FrameOS as a service other people interact with, the licence terms are worth reading in full before you build a product on it. This is not legal advice; read the LICENSE file in the repository.

## How FrameOS differs from a hosted photo frame service

The obvious comparison is a consumer photo frame that ships with a phone app and a cloud account. Those products put the rendering logic on the vendor's servers and the frame is a thin client that pulls images. You get a polished setup flow and no maintenance, and in exchange you depend on the vendor staying online and on their app continuing to work on your phone.

FrameOS inverts that. The device holds the rendering, the backend deploys to it, and the backend can be a container on your own machine. The README states you can run it continuously on a server or locally on your computer when needed. Nothing about rendering requires an account.

The practical difference shows up in what you can build. A hosted frame gives you a photo slideshow. FrameOS gives you scenes you write yourself, deployed to a device you control, on a display you chose, with the option to render at 60 frames per second over HDMI or at 60 seconds per frame on e-ink. That flexibility is the whole product. The price is that you are the operator: you install the backend, you flash the SD card, you keep the device reachable on the network.

## Conclusion

Adopt FrameOS if you want a frame that keeps rendering when the backend is offline, and you are comfortable with a Raspberry Pi, a Docker container and an SSH-reachable device. Do not adopt it if you want a hosted consumer photo frame with a phone app, or if you need documented rollback and upgrade procedures, because the README does not describe either. Before committing, verify your exact panel against the device hardware guide at frameos.net/devices, and confirm the SD image partition layout fits your card: on cards smaller than 4 GiB the /srv/frameos partition stays at 1 GiB.

## FAQ

### What is FrameOS?

FrameOS describes itself as an operating system for single function smart frames, deployed on a Raspberry Pi and used with e-ink and traditional displays. It targets both screens that update 60 seconds per frame and screens that update 60 frames per second.

### How does FrameOS work?

A dockerized Python backend deploys apps onto individual frames over SSH, and the frames run independently, so they keep rendering if the backend goes offline. The README notes you only miss out on log aggregation while the backend is offline.

### Which displays does FrameOS support on a Raspberry Pi?

The README lists Pimoroni e-ink frames, Waveshare e-ink, framebuffer HDMI output and web server kiosk mode, and points to the device hardware guide at frameos.net/devices for the full list.

### How do I install the FrameOS backend?

The README gives a quick install script for macOS and Debian or Ubuntu Linux, and a manual Docker command that runs the frameos/frameos image on port 8989 with a generated SECRET_KEY and a mounted db directory.

### Does FrameOS need the backend to be running all the time?

No. The README states the frames run independently and that the backend can run continuously on a server or locally on your computer when needed. Running it continuously only adds log aggregation.

## Sources

- [Official documentation](https://frameos.net/)
- [Official README](https://github.com/FrameOS/frameos#readme)
- [Project repository](https://github.com/FrameOS/frameos)
- [Release notes](https://github.com/FrameOS/frameos/releases)

---

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