Build and deploy
This section documents MESH's containerization strategy and its build and release pipelines. It's written for analysts, contributors, and anyone auditing the supply chain. For end-user setup, see the Setup guide.
What's in this section
- Runtime stack: the
compose.ymlstack and everyDockerfilethat ships in the repo. - Build orchestration: the Task-based build system (
Taskfile.yml,Taskfile.dev.yml), the analyst build script, and the VS Code devcontainer. - CI and release: the Android release workflow and
scripts/tag-release.sh. - Build troubleshooting: build and container-layer issues. For network, auth, and runtime problems, see Reference Troubleshooting.
Key design decisions
1. Docker Compose for the runtime, go-task for orchestration
compose.yml defines three services (headscale, ui, analyst) and is invoked via docker compose directly. All orchestration lives in two Taskfiles rather than make or shell scripts. This includes interactive setup prompts, .env management, templated config generation, and release tagging. go-task is a better fit than make for this job: tasks are declarative, dependencies between them are explicit, and the built-in prompt: directive handles interactive setup nicely.
2. Multi-stage Dockerfiles with pinned base images
Every Dockerfile uses multi-stage builds and pins its base images in the source files that actually consume them. Build-time downloads such as the Android command-line tools are checksum-verified where the Dockerfiles fetch them.
3. Ephemeral-first analyst container
The analyst container generates a random hostname on every start. It accepts credentials only via LOGIN_URL and AUTH_KEY environment variables, and stores per-session state in a named Docker volume. Rotating the auth key with task setAuthKey stops the analyst and removes that volume before prompting for the new key. No long-lived identity is baked into the image.
4. Reproducible Android releases
The Android release pipeline rebuilds the unsigned APK on a second, isolated runner and fails the release if the SHA-256 hashes differ. SLSA Level 3 provenance is generated via the slsa-github-generator reusable workflow. Go comes from go.mod, and the Java/Android toolchain is pinned in the Dockerfiles and workflow files that install it.
Reproducibility is a security benefit on its own. F-Droid has argued that VPN apps require an unusually high trust baseline that reproducible builds help establish. See F-Droid's VPN trust requires Free Software post.
5. Supply-chain hardening
- GitHub Actions are pinned to commit SHAs via frizbee, refreshed on demand with
task pinGithubActions. - Go vulnerability scanning (
govulncheck) and linting (golangci-lint) run on pushes tomainand on PRs that modify Go sources. - The tailscale submodule is tagged separately with a
mesh-v*prefix to prevent collisions with upstream Tailscale tags in a shared remote.
6. Config via .env, never committed
All runtime configuration (CONTROL_PLANE_DOMAIN, LOGIN_URL, AUTH_KEY, CORS_ORIGIN, NGINX_MODE) is read from .env at the repo root. The file is created on first task run with chmod 600. There is no committed .env.example. Instead, the Taskfile prompts define the variables.
Assumptions
- You have Docker Engine with the Compose V2 plugin (
docker compose, notdocker-compose). - You have the go-task CLI installed. Most tasks work without it via plain
docker compose, but interactive setup does not. - You're on Linux, macOS, or WSL. Native Windows works for many tasks but is untested in CI.
- For Android work: Go matching
go.mod, plus the Java/Android toolchain declared in.devcontainer/Dockerfileand.github/workflows/android-release.yml. Or just use the devcontainer. - For release work: the
tailscale/submodule is initialised. Clone withgit clone --recurse-submodules https://github.com/BARGHEST-ngo/MESH.git, or on an existing clone rungit submodule update --init --recursive.
Next: Runtime stack