mothership v0.1.3
voipappz
The VoIPAppz platform — docker-compose, configs, and the installer media that put them on a machine.
Run it
One command on the mothership machine — no clone, no CLI, no toolchain:
curl -fsSL https://raw.githubusercontent.com/voipappz/mothership/main/installer/install.sh | sh
It installs Docker if needed, downloads this repository's stack, runs the setup wizard (organization, SIP domain, admin email, Let's Encrypt), pulls the images (Docker Hub token, or an image archive — Amazon S3 or a local file), starts the app plane with Compose, creates the first customer and admin login, and prints where everything is. Nothing is written to /opt/voipappz until setup succeeded; a rerun keeps .env and its secrets.
Unattended:
VA_DOMAIN=voip.example.com VA_CF_TOKEN=… VA_REGISTRY_USER=… VA_REGISTRY_TOKEN=… \
VA_ADMIN_EMAIL=admin@example.com VA_ADMIN_PASSWORD=… \
sh -c 'curl -fsSL https://raw.githubusercontent.com/voipappz/mothership/main/installer/install.sh | sh'
| Variable | Meaning |
|---|---|
INSTALL_DIR=/opt/voipappz |
Where the stack lands. |
VA_DOMAIN, VA_SSL_EMAIL, VA_ORGANIZATION_NAME, VA_CF_TOKEN, VA_ACME_ALIAS |
Setup answers (see scripts/setup.sh). |
VA_REGISTRY_USER, VA_REGISTRY_TOKEN |
Docker Hub, for the private nirlevi/* images; used through a temporary Docker config and removed. |
VA_IMAGE_SOURCE=dockerhub|s3|archive |
Where images come from; s3 downloads VA_IMAGES_URL (the latest published archive), archive loads VA_IMAGES_ARCHIVE (path or URL). |
VA_STACK_REF=main |
Which ref of this repository to download; VA_STACK_DIR= uses a local checkout instead. |
VA_ADMIN_EMAIL, VA_ADMIN_PASSWORD |
The first customer's admin login (ONBOARD=0 skips it). |
START=0 |
Install and configure without starting. |
From a clone, the same thing is make install (stack from the checkout), and make images-archive / make images-publish produce the offline image archive the installer can load.
up prints where to find things — make urls prints them again later:
| Admin website | http://127.0.0.1:80 |
| Monitoring | http://127.0.0.1:8080 |
| Alert mailbox | http://127.0.0.1:8025 |
Alerts stop in that mailbox. They reach an inbox only once mailpit is given a real mail server to forward to. Deliberate: a node with none configured used to send nothing at all, silently.
Two things that will bite you
The folder name is your data's name. Docker names volumes after the directory. Clone into mothership and you get a clean slate. Clone into a folder matching one you already ran, and setup hands a newly generated database password to a volume holding the old one — postgres then refuses every connection, and says nothing about passwords or volumes.
Container names are fixed, so two checkouts cannot run at once. make down in the other one first.
Commands
make up start make checks the repo gate
make down stop (keeps your data) make test-lua call-routing logic
make status what is running make test-sip real kamailio: SIP/TLS/WSS
make health health rollup make test-alerting a failure becomes email
make logs follow the logs make act the CI job, locally
make urls where to reach it make image the offline installer ISO
make cert certificate state make deploy provision a remote host
make onboard create the first customer
make down never passes -v, so your volumes survive.
Two planes
app — postgres, redis, nats, the API, admin SPA, Kong, InfluxDB, monitoring, and the ingress kamailio.
voip — the SIP plane in one container: FreeSWITCH, the node agent, and the egress kamailio. It installs separately, from ../va-crystal.
They can share a machine or not. config/services.tsv is the source of truth for which service is where.
The portal (github.com/voipappz/app)
The user-facing web app is its own repo, cloned beside this one. There is no apps/ here: it was a git subtree until 2026-08-31, then briefly a symlink, and both were removed — a committed symlink dangles on every fresh clone, checks out as a text file on Windows, and is simply absent in CI.
git clone https://github.com/voipappz/app ../app # once, beside this repo
voipappz portal finds it there, or at $VA_PORTAL_DIR, or at --path.
make portal-dev # dev stack (Vite :4200 + deno-api :4001)
make portal-print # print the exact deploy commands, change nothing (DEST=)
make portal-deploy # build, push, swap the container (DEST=)
make portal-ship # git push the app repo, then deploy
make portal-status # git + production health + deployed version
make portal-verify # health: deno-api, web, /health dependency report
Those are thin wrappers. The implementation is voipappz portal (cli/src/commands/portal.cr); a second copy of that logic in the Makefile is how the two drift, so there is exactly one and it is in the CLI.
The app builds itself; mothership decides where it lands. The source, the Dockerfile and the app's CI belong to voipappz/app. The deploy policy belongs here:
config/portal/deploy.yml base
config/portal/deploy.{mtn,pbx20,nimbus}.yml per-destination overrides
config/portal/portal-destinations.tsv stop_first + healthcheck per destination
config/portal/.kamal/hooks/ pre-build, post-deploy smoke checks
config/portal/.kamal/secrets{,-common} gitignored; secrets.example is tracked
Choosing where the portal ships needs the view of every destination at once, and this is the only repo that has it. portal-destinations.tsv is a CATALOG the CLI reads — not conditionals re-encoded elsewhere. Two of its rows record production incidents; the file says which and why. An unlisted destination is an error, not a default.
Kamal runs containerised over two repos at once: the app checkout as /workdir (build context and .git, so VITE_APP_VERSION is stamped from the app's sha), with config/portal mounted read-only over /workdir/config and /workdir/.kamal. Nothing in the app repo needs to know about it.
Note kamal deploy -d <dest> reads .kamal/secrets-common and .kamal/secrets.<dest> — never .kamal/secrets. voipappz portal deploy checks what kamal will actually read and fails early with the fix; a Makefile recipe that checked .kamal/secrets passed its own precheck and died inside the container. Use make portal-print to see exactly what will run.
A repo boundary now runs through a contract that used to be one commit: deploy.yml sets STATIC_DIR, EVENT_STORE_PATH, NATS_CDR_SUBJECTS, and the app's api/*.ts reads them. scripts/check-portal-contract.sh catches a rename that loses its other half; it runs in make checks and skips when no portal checkout is present. Portal CI lives in the app repo. The root make deploy still means "provision a remote host" and is never overloaded.
Install on a server
curl -fsSL https://raw.githubusercontent.com/voipappz/installer/main/install.sh | sh
Ensures Docker, installs into /opt/voipappz, then setup → up -p app → up -p voip → health. Tune with INSTALL_DIR, VERSION, BOOTSTRAP=0 (install only), SKIP_VOIP=1 (app profile only).
To pick your own answers rather than auto-detected ones, fill in an answer file — every VOIPAPPZ_* key answers one prompt. It holds credentials, is git-ignored, and should be deleted afterwards:
cp installer.env.example installer.env # domain, Cloudflare token, SMTP
voipappz setup --env-file installer.env
TLS
Certificates come from acme.sh over DNS-01, so no inbound port need be open — which is the point, since voip nodes have no :80. It needs one static DNS record delegating the challenge into the zone we control:
_acme-challenge.<your-domain> CNAME _acme-challenge.<VA_ACME_ALIAS>
Setup asks for a Cloudflare API token (Zone:DNS:Edit) and verifies it live.
Without a token the node runs on a self-signed placeholder. TLS still negotiates, so a fresh node looks fine while real clients reject it. Check make cert before going live.
Deploy a remote host
export VA_REGISTRY_PASSWORD=<registry-token>
make deploy ARGS="--host <ip> --user <u> --key ~/.ssh/<key> --check" # preflight
make deploy ARGS="--host <ip> --user <u> --key ~/.ssh/<key>"
Configuration
make setup writes .env (config plus generated secrets, mode 0600) and config/va.yaml. Both are git-ignored and never leave the machine.
Build from source
The CLI is built here, from cli/. It compiles in Docker against a pinned Crystal image, so a fresh clone needs nothing but Docker — no toolchain, no sibling checkout, no registry login.
make build # compile cli/ (static) and put it at bin/voipappz
make test # its spec suite
The same build produces the release assets, so a binary you compile here and one you download from a release are the same binary.
mothership
- 0
- 0
- 0
- 0
- 2
- about 3 hours ago
- July 21, 2026
Tue, 01 Sep 2026 01:14:44 GMT