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:
| Field | Value |
|---|---|
| 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.
| Where | Switch | Set it to |
|---|---|---|
| Advanced | Isolated network only | Connect to predefined network |
| General → Git | Deploy 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
| Service | Type | Domain | Port |
|---|---|---|---|
web | https | magnemite.example.com | 3000 |
edge | https | agents.magnemite.example.com | 8090 |
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/magnemiteDeploy
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.zipWindows: ./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 rebootIn 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.
| Changed | Build command | Start command |
|---|---|---|
| Dashboard only | docker compose build web | docker compose up -d --no-deps web |
| Hub, or the Caddyfile | docker compose build hub edge | docker 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| Path | What 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/device | Proxies the socket each box holds open. |
/api/enroll | Proxies 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. |
/healthz | Proxies 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.
Docker, behind a proxy
One virtual host, TLS from your own reverse proxy, three containers and a db you already run.
Provisioning a machine
One click from the moment a provider hands over SSH access — what the bootstrap does to a fresh VPS, what it deliberately leaves alone, and where to look when a step fails.