Two deployment mistakes I made in one afternoon, and the boring setup that replaced both.
War story 1: I almost ran a server inside byobu
My first instinct was to start opencode serve in a byobu/tmux session and
detach. It works — until you think about it. byobu is an interactive workspace.
It is not a supervisor:
- no restart when the process dies
- no start at boot
- it dies with the session
If a long-running service needs “a terminal someone remembers to keep open,” that’s the signal it should be a systemd unit. A commenter put it perfectly: “If your README says ‘open a tmux session and run this,’ that process wants to be a service.”
The correction: systemd, not byobu. Which systemd, though?
War story 2: my containerization instinct was wrong twice
My reflex was “containerize it.” Then I listed what the server actually needs:
| Need | Container implication |
|---|---|
project trees under /home/<user>/projects and a second mount | bind mounts at identical paths, or every session path breaks |
| run shell, git, LSPs, MCP tools | every toolchain baked into the image |
~/.config/opencode + the shared 1.2 GB opencode.db + auth.json | more mounts, shared DB with host TUIs |
| LAN model servers, MCP gateway, SSH, docker stacks | docker.sock (a trivial root escape), key mounts |
There’s no isolation to be had: the moment you grant the server what it needs, it’s “host access with extra mount plumbing.”
But the stronger argument is version skew. Every opencode instance on a box shares one config and one database, and migrations run on version change. A container built from a different opencode version than the host TUIs can migrate that shared state into a shape the other can’t read. Corruption.
Rule I landed on: every opencode instance sharing one DB/config must be the same version. Since my TUIs are native, the server must be native too.
The useful reframe: the bridge (the thing that talks to chat channels) is a good container — it’s thin, HTTP-only, and needs no project mounts. The server is not. Process-to-process over HTTP means the bridge never touches your files; only the server does.
bridge container ──HTTP(x-opencode-directory)──► native server ──► projects
(thin, no mounts) (owns the files)
The boring setup that won
A native systemd unit, running as the same user as my other sessions:
[Service]
ExecStart=%h/.opencode/bin/opencode serve --hostname 0.0.0.0 --port 4096
EnvironmentFile=%h/.config/opencode/server.env
Restart=always
RestartSec=3
Two subtleties:
- System unit vs user unit. A user unit (in
~/.config/systemd/user/) needsloginctl enable-linger <user>or it dies at logout — a headless box doesn’t keep your session alive. I enabled linger and kept everything in my home directory, no root needed for the day-to-day. - Hardening you can’t apply. The usual
ProtectHome=yes/ProtectSystem=strictwould break the server — it must read and write your project trees. For a single-user homelab, “run as a user” is about identity sharing (same config/DB/auth), not privilege separation.
Takeaways
- A daemon belongs to systemd, not a terminal multiplexer.
- Don’t containerize the thing that needs host access; containerize the thin thing that talks to it.
- Shared config + shared DB = shared version. This one bites silently.
Linger=yesis the difference between “runs at boot” and “runs until you log out.”