---
title: "Deploy to a machine"
description: "Put a stream on a server or a small box by the door, keep it running without anyone logged in, update it, and manage it remotely."
type: "docs-page"
url: "https://tatolab.com/docs/run/deploy"
site: "Tatolab"
section: "Run"
---

# Deploy to a machine

> Put a stream on a server or a small box by the door, keep it running without anyone logged in, update it, and manage it remotely.

There's no separate deployment system in Tatolab. A machine runs its own runtime, and a stream you keep with `tatolab run -d` stays running and comes back after every restart. Deploying a stream means getting the project onto the machine and keeping it there.

This page assumes a Linux machine you reach over `ssh`: a server, a small box next to a camera, or a cloud machine with a GPU.

## 1. Install the runtime

On the machine:

```sh
curl -fsSL https://get.tatolab.com/install.sh | sh
```

When the installer offers to turn on **lingering**, say yes. Without it, the runtime runs only while someone is logged in; with it, the runtime starts at boot. You can also turn it on later with `loginctl enable-linger`.

Check it:

```sh
tatolab streams
```

See [Install on Linux](https://tatolab.com/docs/install/linux) for requirements and details.

## 2. Copy the project

A project is just files, so use what you already use: `git clone`, `rsync`, `scp`. Leave `.venv/` out; it belongs to the machine it was made on.

```sh
git clone https://github.com/your-org/door-camera.git
cd door-camera
uv sync
```

`uv sync` builds the project's `.venv` on this machine, from your `pyproject.toml` and `uv.lock`. Commit `uv.lock` so the machine gets exactly the versions you tested with.

If a node needs files that aren't in the repository, such as model weights, put them in place now, so the node doesn't download them while it starts.

## 3. Keep the stream

```sh
tatolab run -d
```

```txt title="Output"
door_camera kept (project /home/ops/door-camera)
```

That's the deployment. The runtime loads the stream now and every time it starts again. To check it survives a reboot, reboot and run `tatolab streams`.

## Machines without a screen

A server has no desktop, so plan the stream for that:

- **Leave out `DisplayWindow`.** With no display, there's nowhere for a window to open.
- **See frames with `exchange`** instead. For any output something reads, `tatolab exchange --channel door_camera/camera/video --out snapshots/` saves full-resolution PNGs you can copy off the machine.
- **Expose** outputs for other streams and agents on the machine, or **record** with `Mp4Sink`. A kept stream starts its recording over each time it reloads, after every reboot, so record from a separate stream, or copy files off; see [Recordings and restarts](https://tatolab.com/docs/build/recording#recordings-and-restarts).
- **Audio:** with no sound devices, `MicrophoneSource` produces silence and `SpeakerSink` discards its input, so the stream still runs.

## Choose the right camera

A machine with several cameras needs to say which one. On Linux, list them with `v4l2-ctl --list-devices` (from the `v4l-utils` package), then set `device_id`:

```python
camera = stream_builder.add(CameraSource, config={"device_id": "/dev/video2", "max_width": 1280, "max_height": 720})
```

A `device_id` that can't be opened is refused by name when the stream starts, instead of silently falling back to another camera. `/dev/video` numbers can change when cameras are plugged in a different order; the stable names under `/dev/v4l/by-id/` don't.

## Update

```sh
cd door-camera
git pull
uv sync
tatolab run -d
```

Running `tatolab run -d` again for the same project and stream replaces the kept record with the new version.

## Manage it from your laptop

Every `tatolab` command works over `ssh`. A command run over `ssh` doesn't get `~/.local/bin` on its `PATH` on Debian and Ubuntu, so give the path where the installer put `tatolab`, relative to your home folder there:

```sh
ssh door-box .local/bin/tatolab streams
ssh door-box .local/bin/tatolab logs --stream door_camera --level warn -f
ssh door-box .local/bin/tatolab stop door_camera
```

To let an agent manage the machine, run `tatolab mcp` over `ssh`:

```sh
claude mcp add door-box -- ssh door-box .local/bin/tatolab mcp
```

Control never travels over the network by itself: the runtime only listens on a local socket, and remote access is always `ssh`, with `ssh`'s keys and permissions.

## Tailscale

If your machines are on a [Tailscale](https://tailscale.com) tailnet, `ssh` to them by their tailnet names, from anywhere, without opening ports. The runtime also uses the machine's tailnet name as its own name, the first part of every address. Tailscale isn't required; without it, the machine is named after its hostname.

## Containers

Where there's no systemd, as in most containers, run `tatolabd` as the container's main process. It runs in the foreground until it gets a stop signal. Giving the container the GPU and camera devices is your container runtime's job (for NVIDIA GPUs, the NVIDIA Container Toolkit). A container that shares the host's network counts as the same machine as the host, so only one of them can run a runtime.

## Keep an eye on it

- `tatolab streams` shows every stream's state. A kept stream that keeps crashing the runtime is marked `failed`, with the reason, and the runtime starts the others without it.
- `tatolab logs --stream <name> --level warn` shows a stream's problems; `journalctl --user -u tatolabd` shows the runtime's own output.
- `tatolab graph` shows whether frames are being dropped, and where.

## Several machines

Each machine runs its own runtime and its own streams. You deploy to each the same way. Streams on one machine read each other's outputs, and streams on your other machines read public ones by URL, through the runtime's MoQ relay. See [Across machines](https://tatolab.com/docs/share/across-machines).

## Checklist

- [ ] Runtime installed, lingering on, `tatolab streams` answers
- [ ] Project copied, `uv sync` run, model weights and other files in place
- [ ] No `DisplayWindow` on a machine without a screen
- [ ] Cameras chosen by stable `device_id`, recording paths absolute, and recordings safe from a reload
- [ ] `tatolab run -d`, then a reboot, then `tatolab streams` shows it `kept` and running
