~/workspace. Tesserc creates boxes, syncs your worktree to them, runs
commands on them, and puts them to sleep when they are idle. You do not deal
with instance ids, SSH keys, or IP addresses; tesser status shows the
instance id and IP addresses if you want to see them.
Each box has 2 vCPU, 8 GB of memory, and a 40 GB disk by default. --size
picks small (1 vCPU, 4 GB) through xlarge (8 vCPU, 32 GB), with large and
xlarge on Pro and Team. Every box runs Ubuntu 24.04 with Node (22 and 24, via fnm), Bun, rsync, and Docker (compose
and buildx included) installed. The ubuntu user has passwordless sudo and is in the
docker group, so sudo apt-get install and docker compose up both just work.
You can check what else is available with tesser exec -- which pnpm and
install what your project needs with tesser exec.
Two kinds of box
A workbench holds your worktree and runs nothing on its own. It is the boxtesser exec uses when you do not name one, and it is where tests,
typechecks, builds, and one-off scripts run. Each worktree gets a workbench
the first time it needs one.
An instance box runs one service’s dev server. tesser dev <service>
creates one for each combination of worktree and service, so a repository
with a frontend and a backend ends up with two instance boxes and one
workbench.
Keeping these separate means a test run never competes with the dev server
for CPU, and an install on the workbench never replaces the dev server’s
node_modules while it is running.
Creating a box
Most of the time you do not create boxes directly, becauseexec creates the
workbench and dev creates the instance box. tesser make exists for when
you want a box before running anything on it: tesser make creates a
workbench and tesser make <service> creates an instance box. Both print the
new box id.
When a warm box of the requested size is available in the pool, creating a
box takes a second or two. Otherwise a new machine is launched, which takes
about 90 seconds. tesser pool target keeps a couple of warm boxes
of a size ready, and lets them expire an hour after the last box they could
serve. Pool boxes belong to no repo: any make, exec, or dev in the org
claims one of its size.
Every org has a box cap from its plan: 3 boxes on Hobby, 10 on Pro, none on
Team. When make, exec, or dev fails with box_limit, remove an idle box
with tesser rm or upgrade. tesser sleep does not help: the cap counts
boxes, not awake ones.
Pro and Team bill usage past what the plan includes. An admin can cap that
with tesser spend-limit 50 or on the dashboard’s Billing tab: when the org
reaches it, its awake boxes sleep, and launches and wakes fail with
credit_exhausted until the limit is raised or the month resets.
Snapshots
A snapshot is a disk image of a repo taken after every service’ssetup
has run. When a repo has one, every new box made from it starts from the
repo’s current snapshot instead of an empty home directory. A pool box gets
the snapshot when it is claimed: Tesserc mounts it on the claimed box, and the
box starts from it while the rest fills in. If the mount fails, the box
starts without the snapshot. The current snapshot is the
newest ready one of a commit on the default branch. The box reads the image
as it needs it while the rest streams in behind, so it does not wait for the
whole disk. The first sync then sends only what changed since the snapshot’s
commit. Its package stores and node_modules are already in place, so the
first install is incremental. Without a snapshot, a new box’s first setup
installs from scratch. Boxes do not copy installs from each other.
tesser ls --json shows which snapshot each box started from.
tesser dev --snapshot <snapshot_id> starts a new box from a specific
snapshot of the repo instead, and tesser dev --no-snapshot starts one
without a snapshot.
Run tesser snapshot create inside the repo to build a snapshot of HEAD, or
of another commit with --sha <commit>. Tesserc launches a builder box at the
largest size any of the repo’s services uses, starts it from the repo’s
current snapshot when there is one, syncs the commit, and runs each service’s
setup with the values set by tesser env set. It then captures
/home/ubuntu, /var/lib/docker, and /var/lib/containerd. Before the
capture, the builder deletes ~/.tesser, ~/.ssh/authorized_keys,
~/.ssh/known_hosts, ~/.bash_history, ~/.gitconfig,
~/.git-credentials, ~/.netrc, ~/.config/tesser, ~/.config/gh,
~/.aws, ~/.claude, ~/.claude.json, and ~/.codex, so no one’s
credentials end up in other people’s boxes. A repo builds one snapshot at a
time, a build has 60 minutes to finish, and the builder needs room under the
org’s box cap. --wait follows the build and streams the setup log from the
builder over SSH until the snapshot is ready or fails. The setup log stays
on the builder, so --wait is the only way to see it. Without --wait,
tesser snapshot show <snapshot_id> shows the build’s state.
tesser dev also builds a missing snapshot for you. When you run it for a
service without --snapshot or --no-snapshot, it checks the repo’s
snapshots once it has a box. It compares them against the tip of the default
branch as your clone last fetched it (origin/HEAD, or else origin/main or
origin/master). If that commit needs a snapshot, tesser dev starts
tesser snapshot create for it in the background and prints
tesser: building a snapshot of <repo> at <sha> in the background on stderr,
with the path of its log, snapshot-build.log in the
state directory. tesser dev does not wait for the build, and
pressing Ctrl-C does not stop it. It builds nothing when no service has a
setup at that commit, when a snapshot of the repo is already building, or
when the repo has a current snapshot that is not stale. It also builds nothing
when the last build at that commit failed (it tries again once the tip moves),
or when the newest ready snapshot of the default branch is at a commit the tip
does not contain. A background build also leaves room for one more box under
the org’s box cap. It stops quietly when the org has no room for it, has run
out of credit, has reached a plan limit, or keeps no snapshots.
tesser snapshot ls lists the org’s snapshots. A snapshot of a commit that is
not on the default branch is marked branch, and new boxes start from it
only with --snapshot. tesser env set marks the repo’s snapshots stale, because their setup
ran with the old values. A stale snapshot is still used until you build a new
one. Tesserc keeps the newest ready snapshot of the default branch and the
newest of other branches, and deletes older ones once no box is still reading
from them. tesser snapshot rm <snapshot_id> removes one sooner; boxes
already made from it keep running. If a box cannot read its snapshot, it
starts on an empty home directory instead.
An org that runs boxes in its own AWS account
keeps snapshots in an S3 bucket of its own and has none until it names one.
Syncing
Everyexec and dev copies your worktree to the box before running. Only
changed files are transferred, and .gitignore is respected, so
node_modules, .env.local, and build output stay on your laptop. The box
gets its own node_modules when the setup command runs. If a sync would
delete most of what is on the box, Tesserc stops and reports it, because that
usually means the box was created from a different worktree. In that case,
create a new box rather than passing --force.
The copy on the box is a real git repository with a single commit, your
current HEAD. git status and git diff work there, and git log shows one
entry. Anything you commit on the box is lost at the next sync, so always
commit on your laptop.
Sleep, wake, and removal
Idle boxes go to sleep on their own. A sleeping box keeps its disk and its id and costs a few cents a day. Any command that targets a sleeping box wakes it first, which takes about a minute. Opening a sleeping box on localhost shows a Wake button that does the same. Tesserc hibernates a box when it sleeps: the box writes its memory to its disk, and on wake its processes carry on where they stopped, so a running dev server keeps its state and does not runsetup again. After the wake, boxd waits up
to 60 seconds (or the service’s [health] timeout, if shorter) for the server
to answer its health check. A server that does not answer is started again
from setup. Connections between boxes are closed on wake, so your app
reconnects to its dependencies. Some boxes get a plain power-off instead, and
their dev server is started again on wake: a box that is still streaming in
its snapshot, a box put to sleep in its first minute, and a box
whose image or AWS settings do not support hibernation.
tesser sleep-after 30m sets your own idle time for the instance boxes you
own in the org, anywhere from 5 minutes to 2 hours, or up to 24 hours on Pro
and Team; --default goes back to 2 hours. tesser dev --sleep-after 30m, or tesser sleep-after 30m --box <box_id>, sets one box’s idle time instead, over its owner’s.
A box counts as idle when nobody is using it. Using it means one of these:
a browser tab showing it that is visible, a request from something other
than a browser (curl, a script, a test run), a CLI command, or an SSH session
where someone is typing or something is running or printing. A background tab, or a
shell left open with nothing running in it, does not keep a box awake.
Traffic between boxes does not count on its own, but a box you are using
keeps the boxes it depends on awake, so a server and its database idle
together. When an instance box starts or wakes, it also wakes the boxes it
depends on, as long as you own them or they are the org’s shared instances.
A dependency wired to a teammate’s box is never woken this way.
You can also act immediately: tesser sleep <box_id> powers a box down, and
tesser rm <box_id> removes the box permanently. Neither asks for
confirmation.
A box that must never go down on its own, such as a database the rest of the
org depends on, is kept (on Pro and Team): tesser keep <box_id> takes it off the idle clock
entirely, so it neither sleeps nor expires, and bills for every second it is
awake. tesser keep <box_id> --off puts it back. Explicit sleep and rm
still work on a kept box. When a coding agent such as Claude Code or Codex
runs tesser keep, Tesserc refuses unless the agent also passes --yes, which
states that you asked for the box to stay up.
Finding a box
tesser ls lists every box in the org. Piped, it prints one line per box
with its kind, service, size, power state, whether it is currently online,
when it was made, its IP address, and the local worktree it was made from. In
a terminal it opens a live table instead, with service, worktree, owner, size,
state, created, idle, and box columns: press a column’s number or click its
header to sort by it, press / to fuzzy search, and click
a box twice to use it on localhost.
tesser status <box_id> shows the details
of one box, including its IP addresses and when it was last active. Both
accept --json.
Looking inside a box
tesser ssh <box_id> opens a shell in ~/workspace, and
tesser ssh <box_id> -- <cmd> runs a single command there without syncing
first. When you run it from a terminal, the command gets a terminal on the box
too. Use these for inspection rather than editing, since an edit made on
the box is overwritten by the next sync. To read the dev server’s output, use
tesser logs <box_id> for the last 200 lines or tesser logs <box_id> -f to
follow it. -n sets how many lines to show. When the manifest’s health check
is a GET on a path other than /, tesser logs leaves out the GET
request lines for that path, and --health keeps them.
Each tesser dev or restart starts a new log, and so does a run whose log
passes 10 MB. The box keeps the two logs before the current one, as
~/.tesser/dev.log.1 and ~/.tesser/dev.log.2. --grep takes an extended
regular expression and searches all three logs, oldest first, prefixing lines
from the older logs with their file name. It exits 1 when no line matches: