Getting Started
ZoneRails is a self-hosted server for managing DNS records across Cloudflare and Active Directory DNS, used from your browser. Install ZoneRails Server on Linux, Windows, macOS, or Docker, open http://your-server:3000, and sign in.
First Steps
- Sign in with the default admin account and change the password immediately.
- Open the 07 // ADMIN tab → Connections and add your first DNS provider connection (Cloudflare API token or AD DNS server). The first connection becomes active automatically and zones load right away.
- Add user accounts and assign roles under Admin → Users.
- Optionally register for a free Pro license to unlock every feature for 5 named seats.
Desktop app: the macOS/Windows desktop application is discontinued and no longer updated. Use ZoneRails Server for new setups.
Cloudflare API Token
Create a scoped API token at dash.cloudflare.com → My Profile → API Tokens → Create Token.
Required Permissions
| Permission | Access | Purpose |
|---|---|---|
| Zone | Read | List zones |
| DNS | Read + Edit | Manage DNS records |
| Load Balancers (Zone) | Read + Edit | Manage load balancers (Load Balancing tab) |
| Load Balancing: Monitors and Pools (Account) | Read + Edit | Manage pools, monitors, and pool health (Load Balancing tab) |
| Cloudflare Tunnel (Account) | Read + Edit | Manage tunnels, ingress, routes, and virtual networks (Connectors tab) |
These are Cloudflare's permission-group names exactly as the token editor shows them; the API reports the Edit variants as "Write". Zone Read and DNS are required. The load balancing and tunnel groups are optional: ZoneRails reads the token's policy when a connection is added or tested, and a token missing them makes the Load Balancing or Connectors tab say which groups to add, rather than showing an empty list.
Tip: Set the Zone Resources to "All zones" or specific zones you want to manage. Scoped tokens are more secure than Global API Keys.
Load balancing note: pools and monitors are account-scoped, so a token limited to specific zones cannot list every load balancer in the account. On such a token, deleting a pool requires a written override because ZoneRails cannot prove nothing still references it.
Active Directory DNS
Connect to Windows DNS servers via WinRM (Windows Remote Management).
| Field | Example | Notes |
|---|---|---|
| Label | DC01 — Corp DNS | Display name for the connection |
| Server Address | dc01.corp.local | DNS server hostname or IP |
| Port | 5986 | WinRM HTTPS port (default) |
| HTTPS | Checked | Use TLS for WinRM |
| Username | DOMAIN\admin | Domain credentials |
| Password | •••••• | Domain password |
Prerequisite: WinRM must be enabled on the DNS server. Run winrm quickconfig on the server if not configured.
The account needs to be able to change DNS on that server, for example as a member of DnsAdmins. A DOMAIN\user or user@domain name signs in with NTLM; a bare name uses Basic authentication.
ZoneRails runs each change on the DNS server as one PowerShell script and reads back the server's answer. If Windows refuses a change (no permission, a record that already exists, a zone it doesn't host), you see the server's own message and nothing is recorded as done. A domain controller that can't be reached is reported as such, with its address and port.
A read-only account. An account that can view DNS but not change it needs three things on the DNS server:
- WinRM access: membership of Remote Management Users. Without it, the connection test reports the sign-in was refused (401).
- Read on the DNS server, in DNS Manager → server Properties → Security.
- Enable Account and Remote Enable on the WMI namespaces
Root\Microsoft\Windows\DNSandRoot\MicrosoftDNS(wmimgmt.msc→ WMI Control → Properties → Security). Without them, the test reports "Cannot connect to CIM server. Access denied".
Changes made through such a connection are refused by Windows, and ZoneRails shows that refusal; nothing is recorded as done.
To change a connection, edit it under Admin → Connections. The form shows the current server, port and username. Leave the password blank to keep the current one.
AD DNS Zone Delegations
A zone delegation carves a subdomain out of a parent AD DNS zone and points it at a separate set of nameservers — commonly used to hand off a child zone (e.g. dev.example.com) to a different team, external provider, or DNS appliance while keeping the parent zone authoritative.
ZoneRails exposes the full delegation lifecycle for AD DNS connections:
- List — see all delegations in a selected parent zone, with their nameservers and glue IPs.
- Create — add a child delegation with one or more nameservers, each with a glue IP address. Windows requires an address for every nameserver of a delegation; if you leave one blank, the DNS server looks the nameserver up itself.
- Delete — remove a delegation with a confirmation prompt; the NS records and glue records in the parent zone are removed together.
How to use
- Switch to an AD DNS connection (the Delegations button on the DNS tab is only visible for AD DNS).
- Click Delegations and select the parent zone from the dropdown.
- Click New Delegation — enter the child name (e.g.
devbecomesdev.example.com), add one or more nameservers with their glue IPs. Click Create Delegation. Windows gives the delegation's records the zone's default TTL; a TTL entered in the dialog is not applied. - Existing delegations appear in the list with a Delete action.
Under the hood
ZoneRails issues the standard Windows DNS PowerShell cmdlets over WinRM, one Add per nameserver:
Add-DnsServerZoneDelegation -Name <parent> -ChildZoneName <child> -NameServer <ns> -IPAddress <ip>Get-DnsServerZoneDelegation -Name <parent>Remove-DnsServerZoneDelegation -Name <parent> -ChildZoneName <child> -Force
ZoneRails Server runs them against the connection's DNS server; your browser never connects to the domain controller itself. Audit entries land in the Activity Log with record type DELEGATION.
Cloudflare note: The Delegations UI is hidden for Cloudflare connections. To delegate a subdomain on Cloudflare, create NS records in the parent zone via the regular record editor.
Signing In
ZoneRails runs in your browser, served by ZoneRails Server itself. Open the server's address — http://your-server:3000 by default, or its HTTPS address once HTTPS is set up — and sign in. There is nothing to install on your own machine and no server URL to enter: the web app only ever talks to the server that served it.
| Field | Example |
|---|---|
| Username | Your AD username or local account |
| Password | Your password |
The sign-in screen offers whichever methods the server has configured: local accounts, Active Directory, and single sign-on.
Search & Browse
The Search tab lets you find DNS records across all your zones. Type a record name or content value and press Enter or click Search.
Recent Searches
The last 10 search terms are cached and shared between the main search and the deployment search. Click into the search box to see the dropdown, or start typing to filter. Click Clear history to wipe the cache. A clear button (X) appears inside the input when text is present.
Filters
- Provider Filter — dropdown to show only Cloudflare or AD DNS records (appears when multiple providers are connected)
Results Table
Results show 50 records per page with pagination. Each row displays:
| Column | Description |
|---|---|
| Provider | CF or AD badge |
| Type | Record type (A, AAAA, CNAME, etc.) |
| Name | Record name + tunnel/lock badges |
| Content | IP address, hostname, or value |
| Zone | Parent zone name |
| Proxied | Cloudflare proxy status |
| TTL | Time to live (Auto = 1) |
Double-click a record to open it for editing. If no records match your search and the name belongs to a zone you own, a NEW row appears — double-click it to create the record. If the same zone exists on multiple providers (e.g. Cloudflare + AD DNS), you'll see one NEW row per provider with its badge so you can choose where to create.
For AD DNS zones, a DELEGATE row also appears — double-click it to create a zone delegation instead of a record.
Edit / Create / Delete
The Edit tab opens when you double-click a search result or click the NEW row to create a record.
Creating a Record
- Search for the record name in Tab 1
- If it doesn't exist, double-click the NEW row
- Select the record type, fill in content and other fields
- Click Create Record
Editing a Record
- Search for the record in Tab 1
- Double-click the record row
- Modify the fields you want to change
- Click Update Record
Deleting a Record
- Open the record for editing (double-click from search)
- Click Delete Record
- Confirm the deletion
AD DNS-Specific Behavior
- The Proxy status toggle is hidden for AD DNS zones (AD DNS doesn't support Cloudflare proxying)
- The TTL defaults to 300 seconds (5 minutes) instead of Auto
- A Create Delegation Instead button appears in the form footer — click it to create a zone delegation rather than a record
- AD DNS supports A, AAAA, CNAME, MX, TXT, NS, PTR and SRV records. Every other type is unsupported: CAA as well as the advanced types (HTTPS, SVCB, TLSA, etc.)
- Editing a record changes it in place on the DNS server; renaming it adds the new name and then removes the old one. A record that has changed or gone since you loaded it is refused rather than duplicated.
- TXT values are sent exactly as typed, including spaces, quotes and non-English characters.
Note: All changes are logged with before/after snapshots. With a Pro license, you can review them in the Activity Log and roll them back.
Deployments
Deployments let you batch multiple DNS changes together and execute them as a single operation. This is safer than making changes one at a time.
Creating a Deployment
- Go to the Deploy tab and click New Deployment
- Give it a name and optional ticket number
- Click Search to Add to find records
- Double-click a record to add an edit or delete, or click the NEW row to add a create
- Repeat for all changes needed
Executing a Deployment
- Review all items in the deployment
- Click Deploy Now
- Confirm the changes in the dialog
- Watch the progress view for per-item results
- With a Pro license, DNS verification runs automatically after deployment
Scheduling a Deployment
Click Schedule instead of Deploy Now. Pick a date and time; the server executes it automatically — no browser or laptop needs to be open. At fire time the server re-checks that the scheduling user still exists, is enabled, and still holds deploy:execute; if not, the deployment fails closed with the reason recorded in its results and the audit log.
Pre-Flight Validation
Click Validate before deploying to run 12 checks against each DNS record without making any changes:
- Connection — can we reach the provider?
- Zone accessible — does the target zone exist?
- Record exists (update/delete) — is the record still there?
- Drift detection (update/delete) — has it been modified since the snapshot?
- Content validation (create/update) — will the provider accept these values?
- CNAME conflicts (create) — incompatible record type at the same name?
- Duplicate detection (create) — does this record already exist?
- DNSSEC placement (create/update) — is a DNSKEY at the zone apex, and does a DS sit where an NS record delegates the name?
- Load balancer collision (create) — does a load balancer already serve this name?
- Snapshot completeness (update/delete) — will rollback be possible?
- Protected record — is the record marked protected?
- Locked record — is the record already in another active deployment?
Results show per-item pass/warning/error. Click items with issues to expand detailed check results. Fix any errors before deploying. Validation is included on every tier. Load balancing items run their own set of checks — see Load Balancing.
Real-Time Progress
During execution, the progress bar advances in three phases:
- 0–50% — Execution: each item's Status cell flips from Pending to Success/Failed in real-time
- 50–60% — Status review: a 3-second pause so you can read the results before verification begins
- 60–100% — Verification: DNS propagation checked per item (6 attempts, 10 seconds apart; Pro)
Viewing Results
Click any deployed, failed, partial, or rolled-back deployment in the list to see the results view with per-item status and DNS verification results. With a Pro license, click Verify DNS to re-check propagation.
Deployment Rollback
Rollback is a Pro feature. Click Rollback Deployment on any deployed or partial deployment to reverse all successful changes. The impact modal shows:
- Per-item risk assessment: Safe, Modified (record changed since deployment), or No snapshot (item will be skipped)
- Downstream deployments that later modified the same records
- Click any risk badge to expand details showing before/current/deployed state
Rollback reverses items in reverse order (last change undone first) with a full audit trail. The deployment status changes to ROLLED BACK or PARTIAL ROLLBACK if some items fail.
Duplicate Prevention
Records already in the current deployment show an In Deployment badge in the deploy search results. Attempting to add the same record (name + type) twice is blocked with an error message. Records in other active deployments show In Draft or Scheduled badges and cannot be added.
Deployment Statuses
| Status | Meaning |
|---|---|
| DRAFT | Changes planned but not yet executed |
| SCHEDULED | Scheduled to execute at a specific time |
| DEPLOYING | Currently executing changes |
| DEPLOYED | All changes applied successfully |
| FAILED | No changes applied — all failed |
| PARTIAL | Some changes succeeded, some failed |
| CANCELLED | User cancelled before execution |
| ROLLING BACK | Rollback in progress |
| ROLLED BACK | All successful changes reversed |
| PARTIAL ROLLBACK | Some changes reversed, some rollback items failed |
Connectors & Tunnels
The Connectors tab manages Cloudflare Tunnels, ingress rules, private network routes, and virtual networks. It is a Pro feature: an unregistered server has no tunnel access at all.
Tunnels
View, create, and delete Cloudflare Tunnels. Each tunnel shows its health status, number of active connectors, and creation date. Click a tunnel to view its token and connector details.
Ingress Rules (Config)
Configure which hostnames route to which backend services through a tunnel. Each rule maps a hostname + optional path to a service URL.
| Field | Example |
|---|---|
| Hostname | app.example.com |
| Service | http://127.0.0.1:8080 |
| Path (optional) | ^/api/.*$ |
Routes
Create routes to private networks via tunnels. Routes map CIDR blocks to tunnels and virtual networks.
Virtual Networks
Create and manage virtual networks (VNets) for organizing private network routes.
Load Balancing
The 03 // LOAD BALANCING tab manages Cloudflare Load Balancing: monitors (health checks), pools (groups of origins, each optionally watched by a monitor), and load balancers (a hostname that steers traffic across pools). It is a Pro feature and is hidden for Active Directory DNS connections, which have no load balancing.
Monitors and pools belong to the Cloudflare account; load balancers belong to a zone, because a load balancer's name is a DNS hostname. The tab shows an account selector always, and a zone selector on the Load Balancers sub-tab.
Two ways to change things
- Direct management — the create and edit forms on each sub-tab save straight to Cloudflare, gated by the
lb:create,lb:update, andlb:deletepermissions. Every change is written to the Activity Log with its resource type. - Through a deployment — the same forms offer Add to deployment, and the deployment items view has an Add load balancing button that switches to this tab with the target deployment shown in a banner. Items then flow through the full pipeline: plan, validate, schedule, execute, verify, rollback.
Monitors
A monitor defines how origins are health-checked: type, path, method (GET or HEAD), expected codes, response body, interval, timeout, retries, request headers, and an optional simulated zone. HTTP, HTTPS and TCP monitors are available on every Cloudflare plan; UDP ICMP, ICMP Ping and SMTP require an Enterprise plan and Cloudflare refuses them otherwise. TCP, UDP and SMTP monitors need a port; HTTP and HTTPS default to 80 and 443. Cloudflare does not require a monitor description and does not keep it unique, but ZoneRails does: the description is the monitor's identity for protection, drift detection, and the audit trail, so it is required and a duplicate raises a warning.
Pools
A pool lists one or more origins (name, address, weight, enabled), an optional monitor, a minimum number of healthy origins, and notification settings. The origins editor adds and removes rows in place. Live pool health from Cloudflare is shown beside each pool; a freshly created pool is legitimately unhealthy for one monitor interval.
Load balancers
A load balancer has a hostname, TTL, proxy flag, steering policy, ordered default pools, a fallback pool, and session affinity. Region, country, and POP pool maps and custom rules configured in the Cloudflare dashboard are preserved untouched when you edit a load balancer in ZoneRails.
References between resources
Pools reference monitors and load balancers reference pools, by Cloudflare-assigned id. Inside one deployment, an item may reference another item that does not exist yet: the pickers list pending creates from the same deployment with an amber pending label, and the real id is substituted at execute time. A pending reference can only point into the same deployment, and choosing one disables direct save.
Execution order is derived from these references, not from the order you added items: creates and updates run in dependency order, deletes run in reverse, and a load balancer create waits for any DNS record delete at the same name in the same deployment. Rollback walks the same order backwards.
Guardrails
Load balancing items are validated by the same policy engine as DNS records, with checks specific to the resource type:
- Name collision — a load balancer cannot be created where a DNS record already lives, and a DNS record cannot be created where a load balancer already lives. Both directions are checked.
- Protected names — a load balancer that would shadow a protected record is blocked, whatever the record's type.
- Dangling references — a pool pointing at a missing monitor, or a load balancer pointing at a missing or unusable pool, is blocked.
- Referenced deletes — deleting a monitor that a pool uses, or a pool that a load balancer uses, is blocked. Disabling a pool warns when other enabled pools remain and blocks when none do.
- Drift — an update or delete is blocked when the live resource no longer matches the snapshot the item was planned from.
- Irreversible delete — deleting a pool or monitor raises a warning at plan time, for the reason described under rollback.
As with DNS records, a blocked item can be executed with a written, audited override. Checks that cannot fetch the facts they need fail closed rather than passing.
Verification and rollback
After execution, monitors and pools are verified by reading them back from the API and comparing fields; load balancers additionally go through DNS propagation verification because their name resolves. The results view states which method verified each item. Pool health is reported alongside but never gates the verdict.
Rollback fidelity depends on the resource. Reversing a create deletes the resource, and reversing an update restores the snapshot with the same id, so both are complete. Reversing the delete of a pool or monitor is only partial: Cloudflare assigns a new id on recreate. Load balancers that referenced a recreated pool, and pools that referenced a recreated monitor, still point at the old id until ZoneRails repairs them. ZoneRails records the referencing resources at execute time and repoints each one after the recreate, reporting each repair in the rollback's result; if any repair fails, the item is reported as partial and names the resource left pointing at the old id. A referrer created after the deployment ran is not covered. Before a rollback deletes a pool or monitor, the reference checks run again so a resource that a later deployment attached to something live is refused, item by item, rather than the whole rollback. An individual load balancing change can also be rolled back from the Activity Log.
Plan limits
Cloudflare plans cap the number of pools and load balancers. When Cloudflare refuses a create for that reason, ZoneRails reports it as a plan-quota problem with Cloudflare's own wording, not as a token-permission problem, so you are not sent to the wrong settings page.
Calendar
The 04 // CALENDAR tab shows every scheduled and completed deployment by day. It is a Pro feature; on a Pro server, anyone with View deployments (deploy:read) sees it.
- Agenda (the default) lists deployments under day headers, with a Now line between what has run and what is coming. Click a deployment to see each change and its result. Scheduled by names who scheduled it; a change's Executed by names who ran it by hand, and is blank when the scheduler ran it.
- Month shows a month grid. Click a deployment to see the same detail beside the grid.
Scheduled deployments have a dashed outline. Completed ones are coloured by result: green deployed, amber partial, red failed, purple rolled back.
A scheduled deployment is placed at its scheduled time. A completed one is placed at the time it finished, so a deployment run early by hand shows on the day it actually ran.
Rescheduling
With Schedule deployments (deploy:schedule), open a scheduled deployment and choose Reschedule…. In the month view you can also drag it to another day; it keeps its time of day. You become its scheduler, so it runs with your permissions: you also need Execute deployments (deploy:execute), or the scheduler refuses it when it comes due.
Reschedules and cancellations appear in the Activity Log under Deployments, with the previous time and scheduler.
Activity Log
Every DNS and load balancing change is logged with a full audit trail including before/after snapshots, the user who made the change, and a timestamp. The Activity Log is a Pro feature: listing, exporting and rolling back entries all need it.
Filtering
- Resource Type — All Types, DNS Records, Load Balancers, Pools, Monitors
- Action Type — All, Create, Update, Delete
- Search — Matches the record name, zone, or type
Rollback
Click the rollback button on an audit entry to reverse that change. The rollback itself is also logged in the activity log.
Load balancing entries roll back too. Undoing a create deletes the monitor, pool or load balancer; undoing an update puts its previous settings back on the same id; undoing a delete recreates it from its snapshot. The reversal clears the same guardrails as making the change directly, against the account as it is now: a pool that a load balancer now uses is not deleted, and a resource changed since the entry is not overwritten. Cloudflare gives a recreated pool or monitor a new id, so ZoneRails repoints the load balancers or pools still using the old one and names any it could not. Rolling back needs the permission making the change would: lb:delete to undo a create, lb:update to undo an update, and lb:create to undo a delete, plus lb:update for a pool or monitor.
Export
Click Export to download the full audit log as a JSON file for compliance or archival.
Settings
Click the gear icon in the title bar to open the Settings drawer.
Connections
Provider connections are shared with all users. Administrators manage them in Admin → Connections (see Admin Dashboard), and anyone whose role includes the connections:manage permission, Operators among them, can also add, edit and delete them here. A second connection needs a Pro license. Credentials are write-only in the UI and encrypted at rest.
Cache & Refresh
- Cache TTL — How long zone/record data is cached locally (default: 5 minutes)
- Auto-refresh — Toggle automatic data reload on a timer (Pro; an unregistered server keeps it off)
- Interval — Auto-refresh interval in minutes
License
Enter a server license key to unlock all features (see Licensing & Seats). An unregistered server runs the Standard feature set — one zone, one connection.
Data & Privacy
Clear All Data is an irreversible wipe of the whole server, for every user. It deletes every provider connection, the DNS and zone caches, the audit log, every deployment, and the protected-record flags; users, roles and settings are kept, and you are signed out. Only an account holding admin:manage (Server maintenance) sees it or can run it — the Admin role has it.
ZoneOps: Record Protection
Mark critical DNS records as protected to prevent accidental modification or deletion. When editing a protected record, a confirmation dialog appears before any change is applied.
Toggle protection using the shield icon in the edit form footer.
ZoneOps: Conflict Detection
Before creating or updating a record, ZoneRails checks for conflicts:
- CNAME conflicts — CNAME records cannot coexist with other record types at the same name
- Duplicate detection — Warns about duplicate name + type combinations
- MX validation — Warns if an MX record points to an IP address instead of a hostname
- DNSSEC placement — A DNSKEY must be at the zone apex, and a DS needs an NS record at the same name
ZoneOps: Drift Detection
When editing a record, ZoneRails compares the loaded state against the current live state. If someone else changed the record since you loaded it, a drift warning appears so you don't accidentally overwrite their changes.
Drift monitoring
Drift monitoring is a Pro feature (drift-monitor). For the zones you choose, ZoneRails keeps a baseline of every record set and compares it with the live zone on each DNS cache refresh, so a change made in the provider's dashboard, by a script or by another tool shows up as a finding. Changes made through ZoneRails update the baseline as they are written and are never reported.
The 05 // DRIFT tab has two views; the tab shows the number of open findings.
- Findings — what changed outside ZoneRails, high severity (protected records) first, then the most recently seen, a hundred at a time (Show more adds the next hundred, up to 500; narrow the list by status or severity to see past that). Expand a finding to compare the expected and live records. Accept as the new baseline makes the live state the expected one; it needs a note, which goes in the Activity Log. Scan now starts a DNS cache refresh, which is a scan (one a minute).
- Monitoring scope — which zones are monitored, and which names inside them are left alone. A monitored zone is watched in full unless a rule excludes part of it:
@is the apex,*.previewone label,**.previewone or more labels,api-*a match within a label. The most specific pattern wins, and on a tie exclude wins. Presets add rules for ACME challenges and preview environments, and Preview counts what the rules catch before you save. SOA is never monitored, and a protected record always is. A newly monitored zone adopts its current state as the baseline at the next scan. Turning monitoring off forgets the zone's baselines and closes its open findings as out of scope; its rules are kept for when it is turned back on, and can still be edited while it is off.
Permissions: drift:read to see findings and scope, drift:manage to accept and scan, drift:configure to choose zones and rules. Set the server setting drift.enabled to false to pause monitoring.
ZoneOps: Propagation Verification
Propagation verification is a Pro feature. After deploying DNS changes, ZoneRails verifies each item — and "Verified" means the DNS system actually serves what you deployed, not merely that the API accepted it:
- Cloudflare — two-stage: an API read-back confirms the configured record (name, type, and content), and a live DNS-over-HTTPS query (1.1.1.1) confirms what actually resolves, content-matched per record type (MX priority prefixes, SRV targets, TXT quoting, and trailing dots are normalized). Proxied records are content-checked via the API and resolution-checked via DNS (they serve Cloudflare edge addresses); proxied CNAMEs are queried as A records since Cloudflare flattens them.
- Deletes — verified as absence: the deleted content must no longer appear in the API or in live DNS. Sibling records at the same name and type (common for TXT) don't block it.
- AD DNS — queried against the connection's own domain controller (not the app host's resolver), so internal-only zones verify correctly. NXDOMAIN/no-data counts as absence for deletes.
Verification runs automatically after deployment (6 attempts over ~60 seconds, converging as propagation completes) and can be re-run any time with the Verify DNS button. Advanced record types (HTTPS, LOC, NAPTR, …) are auto-verified from the API response, since their DNS answer formats aren't comparable.
ZoneOps: Tunnel Guardrails
Tunnel guardrails are part of Connectors, a Pro feature. ZoneRails monitors your Cloudflare Tunnel DNS records for issues:
- Orphaned records — DNS records pointing to tunnels that no longer exist
- Dangling routes — Tunnel DNS records with no matching ingress rule
- Health mapping — Shows tunnel health status on DNS records in search results
Orphan alerts appear in the status bar. Click to view and clean up affected records.
ZoneRails Server: Installation
ZoneRails Server ships as DEB and RPM packages for Linux, a signed installer for Windows, a signed and notarized pkg for macOS, and a Docker image. The packages carry their own Node.js runtime, so nothing else needs installing; only running from source needs Node.js 18 or newer. Each installer keeps config.json, the database, the logs and the licence cache in a data directory apart from the program, so an upgrade replaces the program and leaves your data alone.
Every file is published at https://download.getzonerails.com/zrsv/1.8.1/, with a SHA256SUMS file covering them all. Each section below downloads its file, checks it against its SHA256SUMS entry, and installs only if the check passes.
Linux (DEB / RPM)
curl -fLO https://download.getzonerails.com/zrsv/1.8.1/SHA256SUMS
curl -fLO https://download.getzonerails.com/zrsv/1.8.1/zonerails-server_1.8.1_amd64.deb # or _arm64.deb
grep ' zonerails-server_1.8.1_amd64.deb$' SHA256SUMS | sha256sum -c - \
&& sudo dpkg -i zonerails-server_1.8.1_amd64.deb
sudo systemctl start zonerails-server
# RPM-based distributions:
curl -fLO https://download.getzonerails.com/zrsv/1.8.1/zonerails-server-1.8.1.x86_64.rpm # or .aarch64.rpm
grep ' zonerails-server-1.8.1.x86_64.rpm$' SHA256SUMS | sha256sum -c - \
&& sudo rpm -i zonerails-server-1.8.1.x86_64.rpm
The package creates the zonerails service account, runs the migrations, seeds the roles and the local admin, and enables the zonerails-server systemd unit. The program lives in /opt/zonerails-server, the config and database in /var/lib/zonerails, and the logs in /var/log/zonerails.
systemctl status zonerails-server
systemctl restart zonerails-server
journalctl -u zonerails-server -f
Windows
Download zonerails-server-1.8.1-setup-x64.exe and SHA256SUMS, and check the installer before running it. In PowerShell, this stops with an error unless the installer matches its entry:
$file = 'zonerails-server-1.8.1-setup-x64.exe'
Invoke-WebRequest https://download.getzonerails.com/zrsv/1.8.1/$file -OutFile $file
Invoke-WebRequest https://download.getzonerails.com/zrsv/1.8.1/SHA256SUMS -OutFile SHA256SUMS
$want = (Select-String " $([regex]::Escape($file))$" SHA256SUMS).Line -replace '\s.*$', ''
if (-not $want) { throw "SHA256SUMS has no entry for $file" }
if ((Get-FileHash $file -Algorithm SHA256).Hash -ne $want) { throw "SHA256 mismatch for $file" }
Then run it. It installs to C:\Program Files\ZoneRails Server, registers the ZoneRails Server service running as NT AUTHORITY\LocalService, and can open TCP 3000 on the domain and private firewall profiles. Data and logs live in %ProgramData%\ZoneRails, readable only by SYSTEM, Administrators and the service account. The installer is signed by A Holdings Company LLC, so Windows names the publisher. For an unattended install:
.\zonerails-server-1.8.1-setup-x64.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART
Uninstall from Apps & Features. The uninstaller asks whether to delete %ProgramData%\ZoneRails too; No is the default. Keep it if you are reinstalling: config.json there holds the encryption key, and without it the credentials stored in the database can never be read again.
macOS
curl -fLO https://download.getzonerails.com/zrsv/1.8.1/SHA256SUMS
curl -fLO https://download.getzonerails.com/zrsv/1.8.1/zonerails-server-1.8.1-arm64.pkg # Intel: -x64.pkg
grep ' zonerails-server-1.8.1-arm64.pkg$' SHA256SUMS | shasum -a 256 -c - \
&& sudo installer -pkg zonerails-server-1.8.1-arm64.pkg -target /
The pkg is signed with a Developer ID and notarized by Apple, so it also opens with a double-click. The program lives in /opt/zonerails-server, the launchd job in /Library/LaunchDaemons/com.aholdings.zonerails-server.plist, data in /Library/Application Support/ZoneRails, and logs in /Library/Logs/ZoneRails. The daemon starts at install and on boot.
Docker
docker run -d --name zonerails-server -p 3000:3000 \
-v zonerails-data:/var/lib/zonerails \
-v zonerails-logs:/var/log/zonerails \
cr.getzonerails.com/zonerails-server:1.8.1
The /var/lib/zonerails volume holds the database and config.json, and /var/log/zonerails the logs. Keep the data volume when you replace the container: config.json holds the encryption key. For a host that cannot reach the registry, download zonerails-server-1.8.1-image.tar.gz and check it against SHA256SUMS as above. It loads under the same name the registry serves, so run it as :1.8.1:
docker load < zonerails-server-1.8.1-image.tar.gz
docker run -d --name zonerails-server -p 3000:3000 \
-v zonerails-data:/var/lib/zonerails \
-v zonerails-logs:/var/log/zonerails \
cr.getzonerails.com/zonerails-server:1.8.1
First Run
On first startup, the server:
- Generates JWT secret and encryption key (saved to
config.json) - Runs database migrations
- Creates default roles (Admin, Operator, Deployer, Planner, Viewer)
- Creates a local admin account (default:
admin/admin) - Marks the installation as awaiting setup
The setup wizard
Sign in at http://server:3000 as admin and the setup wizard opens.
It takes the installation from running to usable in one pass:
- Administrator password — replaces the one the installer printed. Required.
- Licence — stay on the free Standard tier, paste a key you already have, or choose Register for a free Pro key: enter your organisation and email, and the key is normally issued and installed there and then, with a copy emailed to you. There is nothing further to request (see Licensing & Seats).
- Sign-in method — local accounts, Active Directory over LDAP, or an OpenID Connect provider. Both directory options can be tested before they are saved.
- DNS connection — a Cloudflare API token or a Windows DNS server over WinRM, saved and then tested. A connection that fails its test can be deleted and re-entered without using up the free tier's single connection slot.
- Server settings — listening port, bind address and HTTPS. These are written to
config.jsonand take effect when the service restarts; a field set by an environment variable in your deployment is shown but cannot be changed here.
Only the password and the agreements are required. Everything else can be skipped and finished later from the Admin tab and the Settings drawer. An administrator who accepts the terms and sets a password can finish setup; anyone else signing in before then is asked to wait.
The wizard is a convenience, not a security control. The seeded
admin / admin credentials keep working at the login endpoint until the
password is actually changed, so an installation reachable from an untrusted network should
have its password changed as the first thing you do — or be firewalled until it has been.
ZoneRails Server: Configuration
Configuration is stored in config.json: in the data directory on an installed server (/var/lib/zonerails on Linux and Docker, %ProgramData%\ZoneRails on Windows, /Library/Application Support/ZoneRails on macOS), or in the server directory when running from source. ZR_CONFIG_PATH overrides the location. All settings can be overridden with environment variables.
{
"port": 3000,
"host": "0.0.0.0",
"database": {
"client": "sqlite",
"path": "./data/zonerails.db"
},
"auth": {
"type": "local",
"jwt": {
"expiresIn": "1h",
"refreshExpiresIn": "7d"
}
},
"cache": {
"enabled": true,
"refreshIntervalMinutes": 10
},
"websocket": {
"enabled": true,
"heartbeatIntervalMs": 30000
},
"scheduler": {
"enabled": true
}
}
Environment Variables
| Variable | Default | Description |
|---|---|---|
ZR_PORT | 3000 | Server port |
ZR_HOST | 0.0.0.0 | Bind address |
ZR_HTTPS_ENABLED | false | Set to true to terminate TLS in the server — see HTTPS |
ZR_HTTPS_CERT | — | Absolute path to the PEM certificate, with any intermediates appended |
ZR_HTTPS_KEY | — | Absolute path to the unencrypted PEM private key |
ZR_DB_PATH | ./data/zonerails.db | SQLite database file path — see Database for the other engines and every ZR_DB_* variable |
ZR_AUTH_TYPE | local | local or active-directory |
ZR_JWT_SECRET | (auto-generated) | JWT signing secret |
ZR_ADMIN_PASS | admin | Default admin password |
ZR_CACHE_INTERVAL | 10 | Cache refresh interval (minutes) |
ZR_LDAP_URL | — | LDAP server URL |
ZR_LDAP_BASE_DN | — | LDAP base DN |
Rotating the keys
config.json holds the JWT signing secret and the encryption key for every stored credential. If it, or a backup of it, may have been read by someone who should not have it, rotate both:
- Stop the ZoneRails service.
- On PostgreSQL, MySQL or SQL Server, back up the database yourself; the tool refuses to run until you pass
--backup-taken. On SQLite it takes its own backup. - Run
server.js --rekeywith the server's own Node.js, from itsserverdirectory, as the service account and with the service's environment. The commands for each install are below. A systemnodecannot load the server's code, and without the service's environment the tool looks forconfig.jsonin the wrong place. - Start the service. Everyone has to sign in again.
Linux
cd /opt/zonerails-server/server
sudo -u zonerails env NODE_ENV=production \
ZR_CONFIG_PATH=/var/lib/zonerails/config.json \
ZR_DB_PATH=/var/lib/zonerails/zonerails.db \
ZR_DATA_DIR=/var/lib/zonerails \
ZR_LOG_PATH=/var/log/zonerails/zonerails.log \
/opt/zonerails-server/node/bin/node server.js --rekey
macOS
cd /opt/zonerails-server/server
sudo -u _zonerails env NODE_ENV=production \
ZR_CONFIG_PATH="/Library/Application Support/ZoneRails/config.json" \
ZR_DB_PATH="/Library/Application Support/ZoneRails/zonerails.db" \
ZR_DATA_DIR="/Library/Application Support/ZoneRails" \
ZR_LOG_PATH=/Library/Logs/ZoneRails/zonerails.log \
/opt/zonerails-server/node/bin/node server.js --rekey
Windows, in a Command Prompt run as administrator: the service's LocalService account cannot sign in. If you chose another install directory, use it in the first and last lines.
cd /d "C:\Program Files\ZoneRails Server\server"
set NODE_ENV=production
set ZR_CONFIG_PATH=%ProgramData%\ZoneRails\config.json
set ZR_DB_PATH=%ProgramData%\ZoneRails\zonerails.db
set ZR_DATA_DIR=%ProgramData%\ZoneRails
set ZR_LOG_PATH=%ProgramData%\ZoneRails\logs\zonerails.log
"C:\Program Files\ZoneRails Server\node\node.exe" server.js --rekey
Docker, with the container stopped, using the image and the volumes it runs with. The image supplies the account and the environment.
docker run --rm \
-v zonerails-data:/var/lib/zonerails \
-v zonerails-logs:/var/log/zonerails \
cr.getzonerails.com/zonerails-server:1.8.1 node server.js --rekey
The rekey re-encrypts every stored credential under a new key and rotates the JWT secret in one transaction. --encryption-only and --jwt-only rotate one of the two, and --keep-sessions skips signing everyone out. If either secret is set by environment variable, the tool refuses: rotate it where the variable is defined. Re-encrypting does not un-read anything, so if the database and config.json were exposed together, also reissue the stored credentials at their source: the Cloudflare API tokens, the Windows DNS and directory bind passwords, and the single sign-on client secret.
ZoneRails Server: HTTPS
ZoneRails serves plain HTTP on port 3000 out of the box. You can either put a reverse proxy in front of it — which is what most deployments do — or have the server terminate TLS itself. This page covers doing it in the server.
What the server accepts
Two PEM files, and it is strict about both:
- The certificate, with any intermediates appended to the same file, your own certificate first. There is no separate CA option, and a chain in the wrong order is refused with a message saying so.
-
The private key, unencrypted. The server has no way to supply a passphrase.
If yours is protected, decrypt it first:
openssl pkey -in key.pem -out key-decrypted.pem
A .pfx or .p12 — what Windows AD CS and IIS export by default
— has to be converted first:
openssl pkcs12 -in cert.pfx -clcerts -nokeys -out server.pem
openssl pkcs12 -in cert.pfx -nocerts -nodes -out server.key
Installing it
The simplest route is the setup wizard's Server settings step: choose
Paste the certificate, paste both files, and the server writes them into its own
data directory with the right ownership and permissions (0600 on the key), then
fills in the paths for you. It checks the material before writing — a key that does not
match the certificate, an encrypted key, or a reversed chain is refused with the reason, and
an expired or self-signed certificate is accepted with a warning.
If you would rather place the files yourself, put them where the service account can read them and give the paths instead. Not in the program directory: it is read-only at runtime and replaced wholesale on upgrade.
| Platform | Service account | Where to put them |
|---|---|---|
| DEB / RPM | zonerails | /var/lib/zonerails/tls/ |
| Windows | NT AUTHORITY\LocalService | %ProgramData%\ZoneRails\tls\ |
| macOS | _zonerails | /Library/Application Support/ZoneRails/tls/ |
| Docker | uid 999 | /var/lib/zonerails/tls/ (on the volume) |
Two traps on Linux. The unit sets ProtectHome=yes, so a
certificate left in your own home directory is invisible to the service. And Let's Encrypt
writes /etc/letsencrypt/archive mode 0700 root, so the service
user cannot read privkey.pem there without a group grant or a copy. Both are
caught before you restart — the wizard checks readability as the service
account — but they are why the check exists.
The settings are written to config.json and take effect when the service
restarts. They can also be set by environment, which is how you would do it in a
container: ZR_HTTPS_ENABLED=true, ZR_HTTPS_CERT,
ZR_HTTPS_KEY.
If the server will not start
TLS material that cannot serve stops the server deliberately, with exit code 78 and a message
naming both paths and the problem — a listener that accepts connections and fails every
handshake is far harder to diagnose. On systemd the unit stops rather than retrying, so
systemctl status zonerails-server shows the reason. Fix the paths, or set
https.enabled to false, and start it again.
Port 443
-
Linux: the unit ships with
AmbientCapabilities=CAP_NET_BIND_SERVICE, so the service binds 443 without running as root. On systemd older than 229 (RHEL 7) the directive is ignored and you will need a proxy or a redirect. -
Windows: LocalService binds 443 without extra rights. Check nothing else
owns it first —
netsh http show servicestatefor IIS, andnetsh int ipv4 show excludedportrange protocol=tcpfor Hyper-V/WSL reservations. Remember to update the firewall rule, which the installer creates for 3000. -
macOS: not possible as installed. launchd runs the daemon as
_zonerailsand cannot hand it a privileged socket, so use apfredirect or a reverse proxy. - Docker: publish it —
-p 443:3000.
Renewal
The server serves the certificate it read at startup, for as long as it runs. Renewing the file on disk changes nothing until it restarts, so hook the restart into your renewal:
certbot renew --deploy-hook 'systemctl restart zonerails-server'
GET /api/health reports the certificate the running process is actually serving,
including daysRemaining, so an expiry can be alerted on by whatever already
scrapes that endpoint. It deliberately does not turn the health check red — that signal
is about the database.
ZoneRails Server: Database
ZoneRails Server stores everything it knows — users, roles, connections, deployments, the audit log, the caches — in one database. It ships on SQLite, which needs no server and is the right choice for a single instance, and it runs unchanged on PostgreSQL, MySQL and Microsoft SQL Server when you would rather it lived on the database your organisation already backs up.
| Engine | client | Versions | Notes |
|---|---|---|---|
| SQLite | sqlite | bundled | The default. One file, no server, no configuration. |
| PostgreSQL | postgres | 13+ | Timestamps are stored as timestamptz. |
| MySQL | mysql | 8.0+ | Also MariaDB 10.6+. Timestamps are datetime(3) in UTC — see below. |
| SQL Server | mssql | 2019+ | Also Azure SQL. Reached through the tedious driver. |
The database block
Every engine is configured by the same block in config.json. path applies to SQLite only; the host fields apply to the other three; url is a full connection string that wins over the host fields when it is set. Complete examples ship beside the server as config.example.postgres.json, config.example.mysql.json and config.example.mssql.json.
// SQLite — the default
"database": {
"client": "sqlite",
"path": "./data/zonerails.db"
}
// PostgreSQL
"database": {
"client": "postgres",
"host": "postgres.internal",
"port": 5432,
"database": "zonerails",
"user": "zonerails",
"password": "change-me",
"ssl": false,
"pool": { "min": 0, "max": 10 }
}
// MySQL
"database": {
"client": "mysql",
"host": "mysql.internal",
"port": 3306,
"database": "zonerails",
"user": "zonerails",
"password": "change-me",
"ssl": false,
"pool": { "min": 0, "max": 10 }
}
// SQL Server
"database": {
"client": "mssql",
"host": "sqlserver.internal",
"port": 1433,
"database": "zonerails",
"user": "zonerails",
"password": "change-me",
"ssl": false,
"pool": { "min": 0, "max": 10 }
}
// Any of the three, as a connection string instead
"database": {
"client": "postgres",
"url": "postgres://zonerails:[email protected]:5432/zonerails"
}
| Field | Applies to | Description |
|---|---|---|
client | all | sqlite, postgres, mysql or mssql. Defaults to sqlite. |
path | SQLite | The database file. Created, with its directory, if it is not there. |
host | network engines | Hostname or address. Required unless url is set. |
port | network engines | Defaults to 5432 / 3306 / 1433 when left at 0. |
database | network engines | The database name. It must exist; ZoneRails creates the tables, not the database. |
user, password | network engines | Credentials. The password never appears in a log line. |
ssl | network engines | true for TLS against the system roots, or the driver's own options object. |
pool | network engines | { "min": 0, "max": 10 }. SQLite always uses a single connection. |
url | network engines | A full connection string. Overrides the host fields when set. |
database.type is deprecated. Installations written before multi-engine support named the engine database.type. It is still read as an alias for database.client and logs a deprecation warning once at startup; rename it. If both are present, client wins.
Environment variables
Every field is settable from the environment, so a container needs no config file to reach a managed engine. An environment variable overrides the value in config.json.
| Variable | Sets | Example |
|---|---|---|
ZR_DB_CLIENT | database.client | postgres |
ZR_DB_PATH | database.path (SQLite) | /data/zonerails.db |
ZR_DB_URL | database.url | postgres://zonerails:pw@db:5432/zonerails |
ZR_DB_HOST | database.host | postgres.internal |
ZR_DB_PORT | database.port | 5432 |
ZR_DB_NAME | database.database | zonerails |
ZR_DB_USER | database.user | zonerails |
ZR_DB_PASSWORD | database.password | (the password) |
ZR_DB_SSL | database.ssl | true |
The server refuses to start when client is not one of the four, or when a network client has neither host nor url: it exits with DB_CONFIG_INVALID and says which field is missing. A server that starts on a misread database block is a server pointed at the wrong database.
Migrating an existing installation
An installation already running on SQLite moves to another engine with the built-in copy tool. It creates the schema on the target itself and copies every table into it inside one transaction, so the only two outcomes are a complete database and one holding nothing but the empty schema. The SQLite file is opened read-only and never written — if anything goes wrong you still have it.
The move goes one way only: SQLite to PostgreSQL, MySQL or SQL Server. There is no tool to copy back to SQLite, and none to move between two network engines — plan the destination before you run it, and keep the SQLite file until the new engine has been backed up at least once.
- Stop the ZoneRails service. SQLite must checkpoint its write-ahead log, and nothing may write while the copy reads.
- Create an EMPTY database on the target engine, and a user that owns it. ZoneRails creates the tables; it does not create the database. Do not start ZoneRails against it before the copy: a boot seeds the default roles and the local administrator, and the copy refuses a target that is not empty.
- Point the installation at it, keeping
encryption.keyexactly as it is. Stored credentials are copied still encrypted, and only that key can read them back. An installed server finds its database inconfig.jsonin its data directory; a Docker container takes it from theZR_DB_*environment variables. - Copy with the server's own Node.js, from its
serverdirectory, as the service account and with the service's environment, as for rotating the keys.--fromis the SQLite file in the data directory; the target is the database step 3 configured. Go on only once the copy has printedCopiedwith its row counts and exited 0. - Start the service and check that
/api/healthnames the new engine.
On PostgreSQL, step 2 is two statements run in psql as a superuser such as postgres. Use a password of your own:
CREATE ROLE zonerails LOGIN PASSWORD 'change-me';
CREATE DATABASE zonerails OWNER zonerails;
On an installed server, step 3 replaces the database block of config.json with the target's and leaves every other key alone. The file is /var/lib/zonerails/config.json on Linux, /Library/Application Support/ZoneRails/config.json on macOS and %ProgramData%\ZoneRails\config.json on Windows. Edit it in place, as root (sudoedit) or in an editor run as administrator, so that it keeps its owner and permissions. For PostgreSQL (the examples above show MySQL and SQL Server):
"database": {
"client": "postgres",
"host": "postgres.internal",
"port": 5432,
"database": "zonerails",
"user": "zonerails",
"password": "change-me"
}
In Docker, step 3 writes the same settings to an env file named zonerails-db.env, readable only by you (chmod 600 zonerails-db.env). Docker takes each value literally, so do not quote them. The copy and the container both read this one file, so they cannot disagree about the target:
ZR_DB_CLIENT=postgres
ZR_DB_HOST=postgres.internal
ZR_DB_PORT=5432
ZR_DB_NAME=zonerails
ZR_DB_USER=zonerails
ZR_DB_PASSWORD=change-me
The commands for each install follow. A system node cannot load the server's code, and a copy run as another account or without the service's environment reads the wrong config.json.
Linux
Stop the service:
sudo systemctl stop zonerails-server
Create the database and edit /var/lib/zonerails/config.json (steps 2 and 3), then copy:
cd /opt/zonerails-server/server
sudo -u zonerails env NODE_ENV=production \
ZR_CONFIG_PATH=/var/lib/zonerails/config.json \
ZR_DB_PATH=/var/lib/zonerails/zonerails.db \
ZR_DATA_DIR=/var/lib/zonerails \
ZR_LOG_PATH=/var/log/zonerails/zonerails.log \
/opt/zonerails-server/node/bin/node server.js --copy-database --from /var/lib/zonerails/zonerails.db
Once the copy has exited 0, start the service:
sudo systemctl start zonerails-server
macOS
Stop the daemon. Unloading it, rather than killing it, keeps launchd from starting it again:
sudo launchctl bootout system /Library/LaunchDaemons/com.aholdings.zonerails-server.plist
Create the database and edit /Library/Application Support/ZoneRails/config.json (steps 2 and 3), then copy:
cd /opt/zonerails-server/server
sudo -u _zonerails env NODE_ENV=production \
ZR_CONFIG_PATH="/Library/Application Support/ZoneRails/config.json" \
ZR_DB_PATH="/Library/Application Support/ZoneRails/zonerails.db" \
ZR_DATA_DIR="/Library/Application Support/ZoneRails" \
ZR_LOG_PATH=/Library/Logs/ZoneRails/zonerails.log \
/opt/zonerails-server/node/bin/node server.js --copy-database --from "/Library/Application Support/ZoneRails/zonerails.db"
Once the copy has exited 0, load the daemon again:
sudo launchctl bootstrap system /Library/LaunchDaemons/com.aholdings.zonerails-server.plist
Windows, in a Command Prompt run as administrator: the service's LocalService account cannot sign in. The service's name is ZoneRailsServer, shown as ZoneRails Server. Stop it:
net stop ZoneRailsServer
Create the database and edit %ProgramData%\ZoneRails\config.json (steps 2 and 3), then copy. If you chose another install directory, use it in the first and last lines:
cd /d "C:\Program Files\ZoneRails Server\server"
set NODE_ENV=production
set ZR_CONFIG_PATH=%ProgramData%\ZoneRails\config.json
set ZR_DB_PATH=%ProgramData%\ZoneRails\zonerails.db
set ZR_DATA_DIR=%ProgramData%\ZoneRails
set ZR_LOG_PATH=%ProgramData%\ZoneRails\logs\zonerails.log
"C:\Program Files\ZoneRails Server\node\node.exe" server.js --copy-database --from "%ProgramData%\ZoneRails\zonerails.db"
Once the copy has exited 0, start the service:
net start ZoneRailsServer
Docker, from the directory holding zonerails-db.env, with the image and the volumes the container runs with. Stop the container:
docker stop zonerails-server
Create the database and write zonerails-db.env (steps 2 and 3), then copy. The image supplies the account and the rest of the environment:
docker run --rm --env-file zonerails-db.env \
-v zonerails-data:/var/lib/zonerails \
-v zonerails-logs:/var/log/zonerails \
cr.getzonerails.com/zonerails-server:1.8.1 node server.js --copy-database --from /var/lib/zonerails/zonerails.db
Once the copy has exited 0, replace the container with one that reads the env file. docker start is not enough: a container keeps the environment it was created with, so the old one would come back on SQLite. Only the volumes outlive docker rm. Its published ports, restart policy, networks, mounts and any other -e variables go with it, so recreate it with every option it was created with, adding --env-file zonerails-db.env; docker inspect zonerails-server shows them before you remove it. For the container the install above creates, that is:
docker rm zonerails-server
docker run -d --name zonerails-server -p 3000:3000 --env-file zonerails-db.env \
-v zonerails-data:/var/lib/zonerails \
-v zonerails-logs:/var/log/zonerails \
cr.getzonerails.com/zonerails-server:1.8.1
If Compose runs the container, add env_file: zonerails-db.env to its service and recreate it with docker compose up -d instead. Compose names volumes after its project, so give the copy the names docker volume ls lists.
From a source checkout, stop the server, point zonerails-server/config.json at the target, and run node server.js --copy-database --from ./data/zonerails.db in zonerails-server.
Checking the move
Once the service is up, /api/health answers with the engine it is actually connected to, measured rather than assumed:
curl -s http://localhost:3000/api/health
{
"status": "ok",
"database": { "client": "postgres", "connected": true, "latencyMs": 0.9 }
}
What the copy does and refuses
| Option | Description |
|---|---|
--from <path> | Required. The SQLite database to copy from. Opened read-only. |
--to <path> | The config.json naming the target engine. Defaults to this server's own configuration. |
--replace | Delete the target's existing rows first, in dependency order and inside the same transaction. For a deliberate second run. |
Before it opens the transaction the tool reads the whole source once and prints what it found: the number of rows in every table and the total, any column the source has that the target's schema does not (those are not copied), and every refusal below. Nothing has been written to the target at that point beyond the empty schema, so those counts are the estimate of what the copy is about to do. Afterwards, still inside the transaction, it checks each table's row count and a checksum over its primary keys against the source; either disagreeing rolls the whole copy back.
Transaction log space. Every row crosses inside one transaction, which the engine must be able to undo until the moment it commits. A database with a few million rows in audit_log and dns_cache can therefore hold that transaction open for minutes with its log growing the whole time: on PostgreSQL a long-lived snapshot that blocks vacuuming, on MySQL a growing InnoDB undo log, on SQL Server a transaction log that cannot truncate even in the simple recovery model until the commit. Give the target enough log space for the whole database before you start — the per-table counts the tool prints first are the size to plan against — and prune the audit log and the caches beforehand if they are large.
It refuses, changing nothing beyond the schema it created, when:
- the target already holds application rows —
TARGET_NOT_EMPTY. Starting the server against the empty target before copying is enough to cause this, because a boot seeds the default roles and the local administrator; use--replace, or a fresh database. - the target's
encryption.keycannot decrypt the source's stored credentials —ENCRYPTION_KEY_MISMATCH. Copyencryption.keyfrom the oldconfig.json. - a value is longer than the target's column —
VALUE_TOO_LONG, naming the table, column and row. SQLite enforces no length; the other engines do. - a column that should hold a timestamp holds something that is not one —
VALUE_INVALID, naming the table, column and the value. Only a hand-edited row or a file from a release older than the timestamp migration can produce this; correct it in the SQLite file and run the copy again.
| Exit code | Meaning |
|---|---|
0 | Copied. The summary lists every table. |
1 | Usage: no --from, no such file, or an invalid database block. |
2 | TARGET_NOT_EMPTY |
3 | ENCRYPTION_KEY_MISMATCH |
4 | VALUE_TOO_LONG |
5 | The copy failed and was rolled back. Apart from the schema it created, the target is exactly as it was found; fix the cause and run it again. |
6 | VALUE_INVALID — a column that should hold a timestamp does not. |
Timestamps on MySQL. ZoneRails stores every timestamp as datetime(3) — millisecond precision — and always in UTC: the connection is pinned to UTC and the application supplies the instant, so the same row means the same moment whatever zone the server or the client is in. MySQL's TIMESTAMP type is deliberately not used; its behaviour depends on the server's explicit_defaults_for_timestamp setting, and it cannot represent a date past 2038. Nothing needs configuring, but a report written directly against the tables should read these columns as UTC rather than as local time.
Schema changes
The server migrates its own schema on every start, and an upgrade is therefore just a restart. To do it at a moment of your choosing instead — before a rolling restart, say — run node server.js --migrate-only, which migrates, seeds and exits without opening a port.
There are two other flags, and the difference between them matters on a database that is not yet seeded:
| Command | What it does |
|---|---|
node server.js --migrate-only | Migrates and seeds, then exits. The upgrade flag: use it on a database that is already in service. |
node server.js --migrate | Migrates and seeds nothing, then exits. On a brand-new database that leaves no roles and no local administrator, so nothing can sign in — add --seed unless you are deliberately seeding separately. |
node server.js --migrate --seed | Migrates, then seeds the default roles and the local administrator. This is the first-run command. |
ZoneRails Server: Active Directory
Set auth.type to "active-directory" in config.json to enable AD authentication. Local accounts still work alongside AD (hybrid auth).
{
"auth": {
"type": "active-directory",
"ldap": {
"url": "ldap://dc01.corp.local:389",
"baseDN": "DC=corp,DC=local",
"bindDN": "CN=svc-zonerails,OU=Service,DC=corp,DC=local",
"bindPassword": "ServiceAccountPassword",
"userSearchFilter": "(sAMAccountName={{username}})",
"groupSearchBase": "OU=Groups,DC=corp,DC=local",
"tlsOptions": { "rejectUnauthorized": false }
}
}
}
How AD Login Works
- User enters AD credentials
- Server binds to LDAP with service account
- Searches for user by
sAMAccountName - Re-binds as the user to verify password
- Extracts group memberships from
memberOf - Maps AD groups to ZoneRails roles (configured in admin UI)
- Issues JWT with roles and permissions
If no AD groups are mapped, the user gets the Viewer role by default.
ZoneRails Server: Single Sign-On (OpenID Connect)
Single sign-on lets people authenticate with Microsoft Entra ID or any generic OpenID Connect provider instead of (or alongside) a local password. It is configured from Admin → Authentication (admin:settings) and is a Pro feature — see Licensing & Seats.
Prerequisites
- An HTTPS public URL for this server, set under Admin → Authentication or via
server.publicUrlin config.json / theZR_PUBLIC_URLenvironment variable. The redirect URI you register with the provider is derived from it. - A Pro license (see Licensing & Seats).
- An account holding
admin:settings.
Microsoft Entra ID
- In the Entra admin center, register a new application. Choose the Web platform (not SPA or public client) and add the redirect URI shown on the ZoneRails Admin → Authentication page as its redirect URI.
- Copy the Application (client) ID and the Directory (tenant) ID into the matching fields on the Authentication page.
- Create a client secret under Certificates & secrets and paste its value in — secrets expire, so note the expiry date and rotate before it lapses.
- Optionally, under Token configuration, add a groups claim to emit group memberships. Entra sends group memberships as object IDs, not display names, so group → role mappings must use the object ID.
- No extra API permissions are required — the default
openid profile emailscopes are enough.
Generic OpenID Connect
Any provider that serves a standard /.well-known/openid-configuration discovery document works. ZoneRails uses the authorization code flow with a client secret. Enter the issuer URL, client ID, client secret, and scopes (space separated; openid is required) on the Authentication page and use Test Discovery to confirm the provider's endpoints resolve before saving.
Claim mapping
ZoneRails reads four claims from the identity token. Each is configurable, with sensible defaults:
| Field | Default claim | Used for |
|---|---|---|
| Username | preferred_username | The account's username |
email | Provisioning and email-based account linking | |
| Display name | name | Shown in the UI and audit log |
| Groups | groups | Group → role mapping |
On Entra ID, preferred_username is the user's UPN (usually their email-style sign-in name, not necessarily their mailbox), and groups arrive as object IDs rather than display names.
Provisioning and default role
The first time someone signs in through the provider, ZoneRails creates a local user record with source SSO and it uses one of your licensed named seats. On every sign-in, roles are re-derived from the current group → role mappings and the default role — nothing is "sticky" from a previous login.
If no group mapping applies, the user gets the configured Role for new users (default role), or is refused sign-in entirely when the Authentication page is set to "Deny sign-in unless a group maps to a role."
Linking to an existing local or AD account by email is controlled by the "Link to existing accounts by email" setting: off never links (an email collision is refused as an email_conflict); verified links only when the provider marks the email verified (again refused as email_conflict otherwise); any links on any matching email. Entra ID does not send an email_verified claim, so verified never matches for Entra — use off or any there.
Group → role mappings
Add rows under the SSO group mappings card on the Authentication page, using the provider's group ID (Entra: the group's object ID, not its display name). A user who belongs to several mapped groups gets the union of every matched role. Entra ID stops emitting the groups claim once a user belongs to more than 200 groups (an "overage" condition); those users receive only the default role until you shrink the affected group's membership or restructure the mapping.
Restricting local sign-in and break-glass
Setting local sign-in to Administrators only pushes everyone to single sign-on. On the sign-in screen the username/password form is hidden for everyone, behind an "Administrator sign-in" link that reveals it — the form is a convenience, not the check. The restriction is enforced by the server on every sign-in attempt: it admits any account holding the * or admin:* permission, plus the seeded administrator account, and refuses everyone else. That seeded account can always sign in with a password regardless of this setting, so a misconfigured or unreachable identity provider never locks you out of the server.
Signing out
Signing out ends the ZoneRails session only — it does not sign you out of Entra ID, your generic OIDC provider, or any other application using that same identity session.
Troubleshooting
| Code | Cause | Fix |
|---|---|---|
config_missing | SSO isn't enabled, or the configuration is incomplete | Finish configuring and enable SSO under Admin → Authentication |
unlicensed | The server doesn't have a Pro license | Activate a license in Settings → License |
challenge_missing | The browser didn't send its PKCE challenge when starting the flow | Start sign-in again from the ZoneRails login screen rather than a bookmarked or shared link |
state_mismatch | The callback's state doesn't match a pending flow (expired, reused, or forged) | Start sign-in again; if it persists, check that the server clock is correct and flows aren't expiring early |
token_invalid | Token exchange or ID token validation with the provider failed | Verify the client ID/secret and, for generic OIDC, the issuer URL; check server logs for the underlying detail |
idp_error | The identity provider returned an error (e.g. user cancelled or denied consent) | Retry sign-in; check the identity provider's own logs for details |
no_email | No email claim was present and none could be recovered from the userinfo endpoint | Ensure the email scope is requested and the provider is configured to release it |
email_conflict | The email matches an existing local or AD account and linking is off, or is verified without a verified-email claim | Set "Link to existing accounts by email" to any, or have the user sign in with their existing account first |
seat_limit | Provisioning a new SSO user would exceed the licensed named-seat count | Free a seat (disable an unused account) or increase your seat count |
disabled_user | The matched account is disabled | Re-enable the account in Admin → Users |
mapping_denied | No group mapping matched and the Authentication page is set to deny unless a group maps to a role | Add a group → role mapping for the user's group, or set a default "Role for new users" |
Still stuck? Ask on the ZoneRails community forum, where you can search past answers and post questions.
ZoneRails Server: Roles & Permissions
System Roles
| Role | What it can do | Permissions |
|---|---|---|
| Admin | Everything | * |
| Operator | Direct record writes, full deployment lifecycle including approving others' deployments, audit rollback, connection management | dns:read/create/update/delete, lb:read/create/update/delete, deploy:read/create/execute/schedule/cancel/delete/approve, audit:read/rollback, connections:read/manage |
| Deployer | Plan and execute deployments; records change only through the guardrailed pipeline | dns:read, lb:read, deploy:read/create/execute/schedule/cancel, audit:read, connections:read |
| Planner | Draft, validate and submit deployments for approval — can never execute, schedule, or delete them | dns:read, lb:read, deploy:read/create |
| Approver | Review deployments others submit — approve or reject them; can never plan or execute | dns:read, lb:read, deploy:read/approve, audit:read |
| Viewer | Read-only | dns:read, lb:read, deploy:read, audit:read, connections:read |
Admins can create custom roles from the full permission catalog in the role editor — every permission is presented with a label and description, grouped by namespace (dns, lb, deploy, audit, connections, admin), including the four admin:* permissions for delegating user, role, settings, or maintenance duties without granting full access. Permission format: namespace:action; namespace wildcards (dns:*) and the global * are supported.
The UI hides controls a role cannot use, the API enforces every permission server-side, and scheduled deployments re-check the scheduling user's deploy:execute at fire time. The system roles received lb:read (Operator also the three lb write permissions) when load balancing arrived; custom roles created earlier need lb:read granted by an admin before the Load Balancing tab appears for them.
Deployment Approvals
Any deployment can be submitted for approval. It is frozen while in review; an approver (a role holding deploy:approve) either approves it or rejects it with a comment, which sends it back to draft. An approval covers the changes exactly as they stood — if anything about them differs when the deployment runs, it is refused and needs a new approval. Nobody approves a deployment they created or submitted themselves unless an administrator allows it.
In Admin → Approvals, "Require approval" makes it mandatory: an unapproved deployment can no longer be deployed or scheduled, and one already scheduled without an approval is refused when it comes due. Admins can still deploy an unapproved change immediately in an emergency, with a written reason that is recorded in the activity log. Approval covers deployments; direct record edits by roles holding dns:* permissions are not subject to it.
Ticket Numbers
In Admin → Deployment Policy, administrators can require a ticket number before a deployment is submitted, deployed or scheduled, and give a format pattern — a regular expression the whole ticket must match, such as CHG-\d+ or (CHG|INC)-\d+ — with an example shown to the people entering one. Drafts can be saved without a ticket; the check happens when a deployment leaves draft, and the ticket field shows what is wrong as you type. The pattern applies to any ticket that is entered, even when one is not required, and the emergency approval bypass does not skip it.
ZoneRails Server: Admin Dashboard
Administration lives in the 07 // ADMIN tab of the web app. The tab appears only for users holding an admin permission, and each section is gated individually — a delegated "User Manager" role sees only Users.
Sections
- Users (
admin:users) — Create local users, enable/disable accounts, reset passwords, assign roles, delete users. A live seat banner shows named-seat usage (e.g. Seats: 3 of 5 used); creating a user beyond the licensed seat count is refused with a clear notice. - Roles (
admin:roles) — View system roles, build custom roles from the grouped, labeled permission catalog. - AD Groups (
admin:roles) — Map Active Directory groups to ZoneRails roles for automatic role assignment on login. Degrades gracefully with a notice when LDAP auth isn't configured. - Authentication (
admin:settings) — Choose local sign-in policy and configure Single Sign-On (Microsoft Entra ID or generic OpenID Connect), including group → role mappings. Requires a Pro license. - Deployment Policy (
admin:settings) — Require approval before deployments run and choose whether people may approve their own; require a ticket number and set its format. See Deployment Approvals. - Connections (
connections:manage) — Add, edit, test, and delete shared DNS provider connections (Cloudflare API tokens, AD DNS servers). Secrets are write-only in the UI and encrypted at rest; the Test button saves first, then verifies against the provider. The first connection you add becomes the active one automatically.
The server license is managed in the Settings drawer (gear icon) — see Licensing & Seats.
ZoneRails Server: Licensing & Seats
An unregistered server runs the Standard feature set: one zone, one provider connection, full record CRUD and the deployment pipeline, with RBAC and the Admin tab included. Registering upgrades it to Pro — free, and it unlocks every feature, including Load Balancing, for 5 named user seats.
Registering is how you get the key. There is one registration and one free Pro key per server; you never register and then request a key separately. Register from whichever place you are in:
In the setup wizard
- On the Licence step, choose Register for a free Pro key.
- Enter your organisation and email, and click Continue.
- The wizard shows Registered, and the key is installed. The server is on Pro immediately. The key is also emailed to you so you have a copy; you do not need to paste it. If the wizard instead says Your key will arrive by email, paste the emailed key into Settings → License and click Activate when it arrives.
After setup
- Open the Settings drawer (gear icon) → License and click Request one. This opens
register.getzonerails.com/license/zrsvwith your server's fingerprint already filled in. - Enter your email and accept the Terms of Service, Privacy Policy, and EULA.
- The key is emailed to you straight away. Paste it into Settings → License and click Activate; it validates against the licensing service and unlocks immediately.
Registering the same server again with the same email returns the licence it already has, so it is safe to repeat if the key goes missing. Either way, registering accepts the Terms of Service, Privacy Policy, and EULA, and the acceptance is recorded with the licence. If the email does not arrive within a few minutes, check spam, then email us from the address you registered with.
Named seats
- A seat is an enabled user account on your server — not a device and not a concurrent session. Seated users can sign in from any browser.
- Seat limits are enforced when an account is created or re-enabled; disabling or deleting an account frees its seat instantly.
- The Admin → Users seat banner always shows current usage. The server's licensing heartbeat reports only the count of enabled accounts — never usernames or emails.
- Need more than 5 seats? Contact us about Enterprise.
A server keeps working through temporary licensing-service outages: the last successful validation is cached with a 72-hour grace window.
ZoneRails Server: REST API
The server exposes a full REST API. All endpoints (except /api/auth and /api/health) require a JWT token in the Authorization: Bearer header.
Key Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login | Authenticate and get JWT |
| GET | /api/auth/methods | Public — which sign-in methods this server offers (local, AD, SSO) |
| GET | /api/auth/sso/start | Browser navigation — begins the OIDC flow at the identity provider |
| POST | /api/auth/sso/exchange | Trades the one-time hand-off code for a JWT |
| GET | /api/dns/cached/all | Get all cached zones and records |
| POST | /api/dns/records | Create a DNS record |
| PUT | /api/dns/records/:id | Update a DNS record |
| DELETE | /api/dns/records/:id | Delete a DNS record |
| GET | /api/deployments | List deployments |
| POST | /api/deployments/:id/execute | Execute a deployment |
| POST | /api/deployments/:id/verify | Verify DNS propagation |
| POST | /api/deployments/:id/validate | Pre-flight validation (12 checks per DNS record) |
| GET | /api/deployments/:id/rollback-impact | Rollback impact analysis |
| POST | /api/deployments/:id/rollback | Execute deployment rollback |
| GET | /api/audit | Get audit log entries |
| POST | /api/audit/:id/rollback | Rollback a change |
| GET | /api/admin/users | List users (admin only) |
| GET | /api/admin/permissions | Permission catalog for the role editor |
| POST | /api/deployments/:id/schedule | Schedule a draft deployment |
| GET | /api/health | Server health check |
WebSocket endpoint: ws://server:3000/ws?token=JWT for real-time events.
Record Types Reference
ZoneRails supports every DNS record type that Cloudflare's API exposes — from the nine common types with dedicated form fields down to the full set of RFC-defined advanced types via a structured-data editor.
Common types
| Type | Content | Extra Fields | Proxyable |
|---|---|---|---|
| A | IPv4 address | — | Yes |
| AAAA | IPv6 address | — | Yes |
| CNAME | Target hostname | — | Yes |
| MX | Mail server hostname | Priority (0–65535) | No |
| TXT | Text content | — | No |
| NS | Nameserver hostname | — | No |
| SRV | Target hostname | Priority, Weight, Port | No |
| CAA | (assembled from fields) | Flags, Tag, CA Domain | No |
| PTR | Domain name | — | No |
Advanced types (RFC-defined, structured-data editor)
| Type | Purpose | Data Fields | Proxyable |
|---|---|---|---|
| CERT | Certificate storage (RFC 4398) | type, key_tag, algorithm, certificate | No |
| DNSKEY | DNSSEC public key (RFC 4034) | flags, protocol, algorithm, public_key | No |
| DS | Delegation signer (RFC 4034) | key_tag, algorithm, digest_type, digest | No |
| HTTPS | HTTPS service binding (RFC 9460) | priority, target, value (SvcParams) | No |
| LOC | Geographic location (RFC 1876) | lat/long degrees, minutes, seconds, direction, altitude, size, precision | No |
| NAPTR | Naming authority pointer (RFC 3403) | order, preference, flags, service, regex, replacement | No |
| OPENPGPKEY | OpenPGP public key (RFC 7929) | content (base64) | No |
| SMIMEA | S/MIME cert association (RFC 8162) | usage, selector, matching_type, certificate | No |
| SSHFP | SSH host key fingerprint (RFC 4255) | algorithm, type, fingerprint | No |
| SVCB | Service binding (RFC 9460) | priority, target, value (SvcParams) | No |
| TLSA | TLS cert association / DANE (RFC 6698) | usage, selector, matching_type, certificate | No |
| URI | URI resource identifier (RFC 7553) | priority, weight, target | No |
About advanced types
Advanced record types use Cloudflare's structured data object (one field per column above) rather than a single content string. ZoneRails's advanced editor renders one input per subfield, with a collapsible Edit as JSON escape hatch for power users who'd rather paste a full payload.
Validation is strict on presence and numeric ranges (all required fields must be populated; integer fields are range-checked), but format-level validation of certificate blobs, public keys, fingerprints, SvcParams, and NAPTR regex is delegated to Cloudflare's API — bad inputs surface through the standard error dialog. DNS-over-HTTPS propagation verification is skipped for advanced types, since record equality over DoH text-match isn't reliable.
Advanced types are Cloudflare-only. Active Directory DNS connections will reject any advanced type with a clear error message. CAA is Cloudflare-only as well: AD DNS supports A, AAAA, CNAME, MX, TXT, NS, PTR and SRV.
Badge Reference
| Badge | Where | Meaning |
|---|---|---|
| CF | Search results | Cloudflare provider |
| AD | Search results | Active Directory DNS |
| In Draft | Record name | Record is in a draft deployment |
| Scheduled Apr 15 | Record name | Record scheduled for deployment |
| Tunnel | Record name | Tunnel healthy |
| No route | Record name | Tunnel exists but no ingress rule matches |
| ORPHAN | Record name | Tunnel no longer exists |
| MONITOR POOL LB | Deployment items, Activity Log | Load balancing item kind, shown where DNS items show the record type |
| pending | Load balancing pickers | Refers to a resource this deployment will create; resolved to the real id at execute time |
| ACTIVE | Settings | Currently selected connection |
Deployment Statuses
| Status | Color | Description |
|---|---|---|
| Draft | Gray | Changes planned, not yet executed |
| Scheduled | Blue | Waiting for scheduled execution time |
| Deploying | Amber | Currently executing changes |
| Deployed | Green | All changes applied successfully |
| Partial | Orange | Some succeeded, some failed |
| Failed | Red | All changes failed |
| Cancelled | Gray | Cancelled by user before execution |
| Rolling Back | Red | Rollback in progress |
| Rolled Back | Gray | All successful changes reversed |
| Partial Rollback | Orange | Some items reversed, some rollback failed |
Keyboard Shortcuts
| Key | Action |
|---|---|
Enter in search | Execute search |
Enter in token setup | Save token |
Enter in license field | Activate license |
| Double-click search result | Open record for editing |
| Double-click deploy search result | Add record to deployment |
ZoneRails v1.8.1 · © 2026 A Holdings Company LLC · Back to homepage