Self-hosting is the deployment model for MagicFrog's control plane. Two forms are supported, sharing one rule: there is no hosted service and no other deployment topology. The single-file server is the primary path — one downloaded binary, one SQLite file, no container runtime required. Docker Compose remains available as an optional alternative.
Quick start — single-file server
Three commands bring up a working control plane:
curl -fsSL https://www.magicfrog.ai/install-server | bash
chmod +x ~/.magicfrog/bin/magicfrog-server
MAGICFROG_PASSWORD='<server-api-password>' \
MAGICFROG_ADMIN_IDENTIFIER='admin@example.com' \
MAGICFROG_ADMIN_PASSWORD='<initial-admin-password>' \
MAGICFROG_FIELD_KEY='<64-hex-chars>' \
./magicfrog-server serve --hostname 0.0.0.0 --port 4096
The installer places the binary in ~/.magicfrog/bin — the same location as the magicfrog client CLI. Run it from that directory or add it to your PATH; a different data directory can be set with --data <dir> or MAGICFROG_DATA_DIR.
Required environment variables
All four variables are required — the server fails startup explicitly when one is missing, rather than starting with empty credentials:
| Variable | Meaning |
|---|---|
MAGICFROG_PASSWORD |
The client --server connection password |
MAGICFROG_ADMIN_IDENTIFIER |
Initial administrator (email or username) |
MAGICFROG_ADMIN_PASSWORD |
Initial administrator password |
MAGICFROG_FIELD_KEY |
Field encryption key, 64 hex characters (openssl rand -hex 32) |
MAGICFROG_ADMIN_IDENTIFIER and MAGICFROG_ADMIN_PASSWORD must be injected as a pair — providing only one makes the server fail explicitly at startup. MAGICFROG_FIELD_KEY can alternatively be supplied as MAGICFROG_FIELD_KEY_FILE (volume/secret mount); when both are set the direct value wins.
First boot and the initial administrator
On first boot the server writes one account with the given identifier and initial password into SQLite, marks it role=owner with must_change_password set, and skips the write if the account already exists (restarts do not overwrite it). The initial password is deployment-state-only and one-time.
Forced password change
Sign in with the initial password through POST /v1/account/login (the admin console sign-in at /admin is the same gate). The response carries mustChangePassword=true.
While that flag is set, every business endpoint except login and password change rejects requests with 403 password_change_required. The client surfaces the change-password screen instead of the workspace. Submit the old and new password through POST /v1/account/change-password: the server verifies the old password, stores the new one as an argon2id PHC hash (plaintext is never persisted, and there is no reversible or weaker second hash channel), and clears the flag. The initial password is dead at that point.
Scope: one server
The control plane is deliberately single-server: it provisions and manages cloud computers only on the host it runs on, through that host's container runtime. There is no remote Docker daemon setting (DOCKER_HOST-style), no multi-host node pool, and no cross-machine scheduling or cluster mode. Persistence follows the same rule — one SQLite file, with no external or shared database option. The full boundary is described in Cloud Computers.
Machines you enroll yourself with the CLI are outside this scope: the control plane sends them tool calls over their outbound daemon connection, but never provisions, starts, stops, or destroys them.
With the single-file server, mutable state lives in the data directory (MAGICFROG_DATA_DIR or --data; the SQLite database is MAGICFROG_DB):
| Content | Location |
|---|---|
| SQLite database | <data>/magicfrog.db (MAGICFROG_DB) |
| Data / state / cache / config | <data>/xdg/{data,state,cache,config} |
| CLI credentials | <data>/.magicfrog/credentials |
Field encryption key
MAGICFROG_FIELD_KEY encrypts the personal-data columns the server must still resolve by exact match: the account identifier (email or username), the enrollment device name, and the computer name. Encryption is AES-256-GCM with a deterministic synthetic nonce, so login and lookup keep working without ever storing the plaintext. It is a symmetric key, not a password hash: the server needs it at runtime.
- Generate one per deployment:
openssl rand -hex 32(64 hex characters, 32 bytes). - Store it with the data directory, not in the image: export it alongside the other secrets, or mount it from your secret store via
MAGICFROG_FIELD_KEY_FILE(the direct value wins when both are set). - If it is missing, empty, not 32 bytes, or unreadable, startup fails. The server never falls back to plaintext for these columns. That fail-closed behaviour is deliberate.
- If it is lost, the encrypted columns cannot be recovered. Back it up with the data directory.
- Rotating it requires re-encryption. Keep the previous key available until the migration has run; do not delete it the moment you switch. The re-encrypt pass decrypts every stored value with the outgoing key and rewrites it with the incoming one; there is never a window in which a value is written as plaintext. A server started with a key that cannot decrypt the rows already on disk fails at startup (it does not serve empty results or wrong-password errors).
Encryption by data class
Every sensitive value has one declared storage form, and none of them is plaintext:
| Data | Stored as | Reversible |
|---|---|---|
| Account password | argon2id PHC hash | No |
| Session access and refresh tokens | SHA-256 hash | No |
| Device token | SHA-256 hash on the server | No |
| Pairing code (pairing grant) | SHA-256 hash, compared by hash | No |
| Account identifier, enrollment device name, computer name | AES-256-GCM with the field key (enc:v1: envelope) |
Only with MAGICFROG_FIELD_KEY |
| Device fingerprint, registered capabilities | Digest and capability set, not personal data | Not encrypted by design |
Two boundaries are deliberate. The device fingerprint is a truncated SHA-256 derivation used for device uniqueness, so it is a one-way value rather than an encrypted one. The client's local ~/.magicfrog/credentials/device.json holds the device token the process must present, protected by file mode 0600; the hash-only rule applies to the server's persistent row. Audit records and logs never carry plaintext credentials, pairing codes, or provider keys, only a truncated non-sensitive projection of request parameters.
Docker Compose (optional path)
Compose runs the same server as a container: one magicfrog service backed by a SQLite database on a Docker named volume. The image comes from the GHCR release surface, ghcr.io/gsc-org/magicfrog:<version> — image publishing on GHCR is not part of the current release surface and will be announced separately; use the single-file server until then. Persistence and backups are volume operations: find the host location with docker volume inspect magicfrog_magicfrog-data. docker compose down keeps the volume; down -v deletes it.
First boot with Compose exports the same variables plus a version tag, then starts the stack:
export MAGICFROG_VERSION=<published-version> # a real GHCR release tag
export MAGICFROG_PASSWORD='<server-api-password>'
export MAGICFROG_ADMIN_IDENTIFIER='admin@example.com'
export MAGICFROG_ADMIN_PASSWORD='<initial-admin-password>'
export MAGICFROG_FIELD_KEY='<64-hex-chars>'
docker compose up -d
docker compose logs -f magicfrog
All five variables are required: Compose interpolates them strictly and exits with an error if any one is missing, so the stack never starts with empty credentials. The port defaults to 4096; override the host mapping with MAGICFROG_PORT. Inside the container, all mutable state lives under /data (mapped to the named volume).
Download distribution
Published artifacts — the magicfrog client and the single-file magicfrog-server — are served from a dedicated R2 bucket, magicfrog-downloads, which clients download from directly — not through the files Worker proxy. This bucket is separate from the existing files buckets and does not share an object key namespace with them:
| Surface | Bucket | Key namespace | Read path |
|---|---|---|---|
| Files service (existing, unchanged) | magicfrog-dev-files / magicfrog-production-files |
bin/, releases/, videos/ |
Worker route /files/* |
| Download distribution | magicfrog-downloads |
downloads/<version>/ |
R2 public-read direct |
One-time setup and publishing:
bunx wrangler r2 bucket create magicfrog-downloads
# Enable public read on the bucket (custom domain or r2.dev dev domain) to get the origin:
export MAGICFROG_DOWNLOAD_BASE_URL='https://<public-read-origin>'
MAGICFROG_DOWNLOAD_BASE_URL='https://<public-read-origin>' bun packages/cli/script/publish.ts
The publish script writes objects under downloads/<version>/<filename> with a url pointing at <origin>/downloads/<version>/<filename>. Publishing without MAGICFROG_DOWNLOAD_BASE_URL fails explicitly — it never writes placeholder domains or falls back to the files buckets. Do not add magicfrog-downloads to services/files' wrangler.jsonc r2_buckets; that Worker only binds the existing files buckets.
Integrity verification
The publish step records sha256 and size for every artifact file, computed from the uploaded bytes, alongside the url:
{ "url": "https://<origin>/downloads/<version>/<filename>", "sha256": "<64 hex>", "size": 12345678 }
A client download must recompute both values and compare them against the metadata; any mismatch rejects the installation outright. There is no degraded "install anyway and warn" path and no placeholder success.
Unsigned artifacts
Windows and macOS artifacts are legitimately deliverable without code signatures. Signing and notarization are not a release gate: the publish pipeline does not check signature status and prints an explicit unsigned notice. Windows signing via Azure Trusted Signing is an optional step that is skipped with a printed reason when unconfigured; macOS notarization depends on Apple credentials injected into the release workflow and is likewise optional. SmartScreen and Gatekeeper "unknown developer" prompts are host-OS behavior, not product failures, and are not treated as blockers in acceptance.
Client discovery
A desktop or web client starting against a self-hosted deployment resolves the server explicitly. When no server is configured, it shows a not-configured state with an input for the server address rather than silently proceeding. An explicitly entered server address always wins over automatic discovery.
For the admin console (the /admin management surface), set VITE_ADMIN_CONTROL_PLANE_URL to the control plane origin at console build time. If the variable is unset, the admin login gate reports the missing configuration instead of aiming requests at a guessed origin. When the console is served from a different origin than the control plane, allow the console origin on the server with magicfrog serve --cors <origin>.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Server startup fails reporting a missing required variable | One of MAGICFROG_PASSWORD / MAGICFROG_ADMIN_IDENTIFIER / MAGICFROG_ADMIN_PASSWORD / MAGICFROG_FIELD_KEY was not set; see Required environment variables |
First-boot log reports that MAGICFROG_ADMIN_IDENTIFIER and MAGICFROG_ADMIN_PASSWORD must be injected together |
Only one of the two admin variables was provided; inject both |
| Startup fails on the field encryption key | MAGICFROG_FIELD_KEY is missing, empty, or malformed — generate one with openssl rand -hex 32; see Field encryption key |
Business endpoints return 403 password_change_required |
The forced password change has not been completed; see Forced password change |
docker compose up reports set MAGICFROG_VERSION … |
A required variable was not exported; see Docker Compose |
Publishing reports MAGICFROG_DOWNLOAD_BASE_URL is required |
The download origin was not configured; see Download distribution |
| Client download returns 404 | The object key must be downloads/<version>/<filename> and public read must be enabled on the bucket |
POST /v1/admin/computers returns 503 with a message about iptables and Permission denied |
The host runs the pool as a non-root user, so the per-instance egress policy cannot be installed. Either run the pool as root, or grant passwordless sudo for iptables and set MAGICFROG_POOL_HOST_COMMAND_PREFIX="sudo -n". The request fails closed — no computer row and no audit entry are written, so retrying after the fix is safe |