Magnemite
Deployment

On a Coolify setup

Two domains, generated secrets, an external database, and deploys that leave the fleet on.

deploy/compose.coolify.yml lets Coolify's proxy terminate TLS and keeps a small Caddy of our own (edge) in front of the hub. It uses two domains, because the dashboard and the boxes are separate audiences, and no database — you point it at one you deploy separately.

Deploying

Create the resource

New Resource → Docker Compose, pointed at this repository, with two fields under Build configuration:

FieldValue
Base directory/deploy
Compose file/compose.coolify.yml

Set two switches before the first deploy

Both live outside the Environment tab, and both are easier to set now than to diagnose later.

WhereSwitchSet it to
AdvancedIsolated network onlyConnect to predefined network
General → GitDeploy on push (webhooks)Manual deployments only

Connect to Predefined Network puts these containers on Coolify's shared network instead of a private one of their own. That is what makes the database resource resolvable by the internal hostname DATABASE_URL uses. Without it the hub boots, fails to make a connection with the database, and the migration never runs.

Manual deployments only stops a push to the default branch from redeploying. Not recommended to keep the default settings (automatically redeploying with every update). Do it yourself when needed.

Add the domains

ServiceTypeDomainPort
webhttpsmagnemite.example.com3000
edgehttpsagents.magnemite.example.com8090

Set the environment

In the Environment tab, the database this stack does not run:

DATABASE_URL=mysql://<user>:<password>@<host>:3306/<database>

The four parts of DATABASE_URL are the database's own user, that user's password, the hostname this stack can reach it on (probably database-<id>), and the database name. Nothing there is a service name from this compose file:

DATABASE_URL=mysql://magnemite:s3cret@magnemite-db-abc123:3306/magnemite

Deploy

Nothing to run by hand: the database migrates itself on boot and the admin account is created when the hub starts. The first deploy must be the full-stack one.

Read the generated admin password

Coolify generates the internal secret, AUTH_SECRET and the admin password, and shows them in the Environment tab. SERVICE_PASSWORD_ADMIN is the first login. If you ever need to change it, change ADMIN_PASSWORD and restart the hub to apply the change.

The stack boots empty apart from the admin: set up an app target and its version sources, a group, and an enrollment token from Settings.

Build the Magisk module

From a clone of this repository on your own machine. SERVER is the hub URL, the edge domain.

make module SERVER=https://agents.magnemite.example.com TOKEN=<enrollment token>
# → dist/magnemite-agent-1.0.0.zip

Windows: ./scripts/build-agent.ps1 -Server https://… -Token <token>.

Flash a box

adb push dist/magnemite-agent-1.0.0.zip /data/local/tmp/
adb shell "su -c 'magisk --install-module /data/local/tmp/magnemite-agent-1.0.0.zip'"
adb reboot

In bulk over the LAN: ./scripts/enroll.sh dist/magnemite-agent-1.0.0.zip -f hosts.txt.

The box enrolls itself on first boot and appears in the fleet table. That is the only time it needs hands-on work, yes! Every later update, the agent's own included, rides the socket it opened. Details in the agent on a box.

Deploying without dropping the fleet

Coolify rebuilds every service with a build: under a tag carrying the current commit.

Narrow it with the resource's Custom Build Command and Custom Start Command (General tab). They run from the base directory, and Coolify injects -f and --env-file itself.

ChangedBuild commandStart command
Dashboard onlydocker compose build webdocker compose up -d --no-deps web
Hub, or the Caddyfiledocker compose build hub edgedocker compose up -d --no-deps hub edge

Clearing both fields restores the full-stack deploy.

--no-deps is not optional

Without it, Compose starts what web declares in depends_on — hub — and recreates it, because its image tag moved with the commit. That is the exact restart this avoids. Never add --remove-orphans or --force-recreate either: both reach past the service you named.

The external database

The URL is resolved inside the container, so it needs the database's internal hostname, which only resolves with Connect to Predefined Network in Advanced settings.

Why there is still a Caddy

Traefik routes; it does not serve files. The artifacts volume belongs to this stack and Coolify's proxy cannot mount it, so without edge every ~170 MB bundle would be read out of Node.

                                    ┌─ /files/*            file_server, read-only volume
  box ──► Traefik ──► edge (Caddy) ─┼─ /ws/device          ┐
      agents domain                 ├─ /api/enroll         │
                                    ├─ /api/logs/*         ├─► hub
                                    ├─ /api/screenshots/*  │
                                    ├─ /healthz            ┘
                                    └─ anything else       404
PathWhat the edge does with it
/files/*Serves the bundle itself: native Range, so an interrupted download resumes. Every request is checked first by forward_auth against the hub's /internal/authz, which validates the device token.
/ws/deviceProxies the socket each box holds open.
/api/enrollProxies first-boot registration.
/api/logs/*Proxies the log bundles a box uploads.
/api/screenshots/*Proxies the screen captures a box uploads, for the reason a bundle goes this way: a socket frame is capped at 1 MB and a 1080p PNG is regularly more.
/healthzProxies liveness.

Everything else is a 404 on purpose

The dashboard lives on the other domain and reaches the hub over Coolify's network, so nothing else belongs here. That default is what keeps /internal/*, reboot, install, rollout control: unreachable from the internet, on the one domain a couple of hundred boxes know the address of.

On this page