Cookbook · 1 of 3
Develop and test
Throwaway OpenWrt routers in Docker, so you can see your package running before anyone else does. You need Docker and nothing else.
Start a router
go install owfeed.org/owlab/cmd/owlab@latest cd my-luci-app owlab up # start it owlab open owrt2512 # LuCI in your browser
Log in as root with an empty password. The router is disposable — break it, then
owlab up --rebuild.
See your changes without rebuilding
owlab sync --watch
Copies your sources into the running router on every save and reloads LuCI. This is the loop you spend the day in; building a package is for later.
owlab copies the same directories luci.mk installs — htdocs,
ucode, luasrc, root. If your package lives in a
subdirectory, say so:
project:
install:
my-package/htdocs: /www
my-package/ucode: /usr/share/ucode/luci
my-package/root: /
Cover both release lines
version: 1
routers:
- id: owrt2512
release: "25.12.5"
- id: owrt2410
release: "24.10.8"
25.12 uses apk, 24.10 uses opkg, and they agree on almost nothing. A package that works on one can fail on the other, so both are worth having open.
snapshot images and
snapshot feeds are rebuilt daily and independently, so installs start failing within
a day. owlab releases tells you when a pin has fallen behind.Make the router look inhabited
An empty router renders almost nothing — no zone badges, no VLANs, no DHCP leases, no tables with rows in them. That is a poor place to develop a page against:
defaults: fixtures: [all]
Individual profiles: networks, clients, wireguard,
portforwards, system, wifi.
The whole check in one command
owlab test --release 25.12.5 --release 24.10.8 \ --install 'dist/*/luci-app-mine-*' \ --assert 'http 200 /cgi-bin/luci/admin/services/mine' \ --assert 'service mined'
Starts the routers, installs, asserts, tears down, exits 0 or 1. This is what CI runs, and running it locally first is how you find out before the pull request does.
Two things it does that a hand-written curl does not. It logs into LuCI
first, because every admin page answers a redirect otherwise — which is why the obvious
check reports 403 on a perfectly healthy page. And it fails a 200 whose body is an error
page: LuCI's dispatcher returns 200 with the traceback in it when it catches an
exception.
Full list of assertion kinds: Commands and config.
When a kernel module is involved
routers:
- id: real
fidelity: vm
release: "25.12.5"
packages: ["+kmod-nft-nat"]
A container shares the host kernel, so kmod-* packages install and do nothing.
fidelity: vm is QEMU with OpenWrt's own kernel — first start takes about a minute and
a half, after that twenty seconds. It also gets simulated radios, so the wireless pages show a
router instead of a config file.