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 a one-way hash (plaintext is never written to disk), 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 the server must still look up by exact match: the account identifier (email or username), the enrollment device name, and the computer name. After encryption, login and lookup keep working without ever storing the plaintext.
- 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).
How data is encrypted at rest
No sensitive value is ever stored in plaintext:
- Account passwords, session tokens, device tokens, and pairing codes: stored as one-way hashes only; the originals cannot be recovered.
- Account identifiers, enrolled device names, and computer names: encrypted with
MAGICFROG_FIELD_KEY(sign-in and lookup keep working).
The local device credential file on the client (~/.magicfrog/credentials/device.json) is protected by 0600 file permissions. Audit records and logs never carry plaintext credentials or keys.
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).
Downloads and verification
Published artifacts (the magicfrog client and the single-file magicfrog-server) are distributed from the official download origin. Every artifact ships with sha256 and size metadata; any mismatch refuses the install — there is no "install anyway with a warning" path.
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 |