Skip to content
AppCrane
FOR ENTERPRISE IT MANAGERS

The apps arrived without you

Someone in finance built a reconciliation tool with Claude Code. Someone in support built a triage dashboard. They work, they are in use, and nobody asked you first — but you are the one who has to answer where they run, who can open them, what is inside them and what happens when one breaks. AppCrane is a self-hosted platform that puts those apps on one Ubuntu server you own, behind one sign-on, with a role model and an audit trail that were written for exactly this position.

Sign-on is enforced at the proxy, not in the app

The apps your business builds with an agent were not written with your identity provider in mind, and asking each one to integrate is how shadow accounts happen. AppCrane settles identity before the request reaches the container. Caddy runs a forward_auth check against AppCrane on every proxied request; AppCrane resolves the caller's role on that specific app and refuses with 403 if they hold none, so an unauthorised visitor never touches the app at all.

Once the answer is yes, the authenticated identity is handed to the container as request headers. Caddy strips any client-supplied copy of those headers before the auth step runs and then writes the verified values itself, so a visitor cannot forge them:

X-AppCrane-User-Email: someone@example.com
X-AppCrane-User-Role:  admin
X-AppCrane-App-Role:   owner
X-AppCrane-Is-Admin:   1

The practical effect is that an internal app your team never reviewed still cannot be opened by someone who has no business opening it — and it did not have to be modified to get there.

SAML, OIDC and SCIM are in the open-source build

SAML 2.0, OIDC and SCIM 2.0 provisioning all ship in the AGPL-3.0 build with nothing to activate, configured by a platform admin. SCIM covers users and groups, which is the half that matters on the day someone leaves: deprovisioning happens in your directory and arrives here, rather than being a task somebody remembers. AppCrane is also offered under a separate commercial licence for organisations that need to run private modifications as a service — that changes the licence terms, not which features are present.

Identity does not require your IdP, though. Local password accounts and API keys create sessions too, which is what keeps a break-glass path open when the directory is the thing that is down.

Roles, and where each one actually applies

There are two layers, and it is worth keeping them apart. Platform-wide, a user is platform_admin, admin or user. On a specific app, a user holds owner, admin, user or none. The per-app role is the one the proxy checks on every request.

A third layer exists and is deliberately inert: an application can declare its own role vocabulary — approver, auditor, whatever its domain needs — and AppCrane stores who holds them and delivers them in a header. It never interprets them. An app-defined role can never confer an AppCrane privilege, which means letting a team manage its own authorisation vocabulary costs you nothing on the platform.

The operator is locked out of app secrets. Environment variables are encrypted with AES-256-GCM, and access follows app assignment — including for platform_admin, the role that installs and updates the platform. Reveals are throttled and audited across both doors, the HTTP API and the MCP endpoint, so switching transport does not buy a fresh allowance. If your reason for hosting internally is that a model API key must not be readable by whoever administers the box, that is the specific property to check.

An audit log that tells an agent from a person

Every mutating action is written to the audit log against the user who took it, and every row carries an actor_kind of human or agent. That second column is the one that matters now: most deploys on a box like this come from Claude Code or Cursor rather than from a browser, and "what did the agents change here last week" should be one query rather than a reconstruction from timestamps.

MCP tool calls are audited the same way, with their arguments redacted before the row is written. There is a stricter mode for the MCP door — set APPCRANE_AUDIT_REQUIRED=1 and a tool call is refused outright if its audit row cannot be written, because the check runs before the action. The REST middleware cannot make the same promise and says so rather than pretending: it runs after the handler, so under the same flag it reports the failure loudly instead of claiming nothing happened.

Vulnerability scanning that reports and never blocks

Every deploy scans what it just shipped and records the result against that deployment. Apps built from source are read from their lockfiles and queried against OSV; apps deployed from an image are scanned with a pinned Trivy against the resolved digest — the bytes actually running, not whatever the tag points at today.

It reports and never blocks. A scanner that is missing, slow or wrong cannot fail your deploy, which is a deliberate trade and worth knowing before you plan a control around it. The separate release check is what can stop a production deploy: by default it blocks any app when a dependency has a known vulnerability with a published fix, unless an owner records a time-limited exception. A platform admin can turn that blocking off.

The fleet report leads with arithmetic rather than with a word. Every report opens with how many app-and-stage rows have a usable scan result, out of how many, as a percentage — then names how many were skipped, errored or never scanned, and lists the most common reasons, counted. There is still a one-word assurance level, but it never travels without those counts beside it, because "partial" spans one per cent to ninety-nine.

A daily digest goes out per recipient and is scoped to what that person is entitled to see: fleet-wide to a platform admin, own-apps-only to an app owner, so the digest cannot quietly tell someone which other apps are exposed.

Per-app ceilings, checked in force

Each app carries a memory ceiling in megabytes and a CPU share — 512 MB and half a core by default — applied as Docker's --memory and --cpus when the container is created. Changing them is platform-admin only. Memory and swap are pinned equal, so a container cannot quietly spill into swap instead of hitting its limit.

Those are creation-time flags: a limit changed after a container was created is recorded in the database and not in force until that container is recreated. appcrane_check_resource_limits exists to find exactly those containers, and reports "unknown" rather than guessing when Docker cannot be read. appcrane_memory_budget answers the adjacent question — whether the sum of every configured ceiling would fit in host RAM if all the containers asked at once, from configured ceilings rather than measured usage.

Containers also run with no-new-privileges, raw-socket capabilities dropped, a process-count limit, and inter-container connectivity disabled on the shared network, so one internal app cannot reach another's port by default.

What you get without configuring anything

A nightly local backup

On by default: the database and .env to DATA_DIR/backups/local at 04:00, newest seven kept. It covers a corrupted or deleted database and nothing else — not icons, per-app data, volumes, repositories or images.

Off-site backup status

The off-site S3 copy is optional and does nothing until a bucket and credentials are entered. Until an upload has actually completed, every surface — settings, MCP, dashboard, boot log — says "No off-site copy — everything AppCrane knows lives on this host."

Automatic HTTPS

Caddy provisions certificates on first request. An organisation with its own CA can supply a certificate and key instead and skip ACME entirely.

Two environments per app

Production and sandbox, always on, separate ports. Promotion refuses unless sandbox is live and healthy, and the promoted production release is health-checked with automatic revert.

Managed databases

PostgreSQL, MariaDB, MongoDB and Redis provisioned per app, with credentials injected under the variable names the app actually reads. No shared database where one team's mistake is another team's outage.

Health checks and restart

Each app declares a health endpoint; AppCrane probes it, restarts on failure and notifies by email.

Missing containers come back

At boot and every 10 minutes, an app whose live release has no running container is redeployed from that release, and its owners get one e-mail. A reboot or a full disk does not leave apps down until someone notices.

A disk floor and host limits

Deploys and updates are refused below 5 GB free. The installer caps system and Docker logs, turns on unattended security updates and keeps a 1 GB reserve file; Settings → Job Health → Host limits shows each one and applies them after a two-factor code. A daily check e-mails platform admins about pending host security updates.

Requests, rather than a ticket queue

Someone who lands on an app they cannot open gets a Request access button rather than a dead end. The request queues against that app, and an owner, app admin or global admin approves it at a chosen role or denies it — from the dashboard or from an agent session. The grant is an ordinary audited action, so the answer to "why does this person have access" is a row rather than a memory.

Code that never leaves the box

A new managed app's code lives in a bare git repository under DATA_DIR/repos on the AppCrane host itself — no GitHub organisation, no service account, no third party holding the source. Deploys clone from the local repository, a push can trigger a deploy on its own, and the repositories travel in the backup. For teams whose reason to self-host is that the code is the sensitive part, that removes the last hop off the server.

Apps that are connected to GitHub can authenticate through this instance's own GitHub App instead of a long-lived personal access token: one-hour installation tokens narrowed to a single repository, cached in memory, handed to git through its environment rather than a URL, a config file or a command line.

Aligned with SOC 2, with the evidence to show it

AppCrane is built to support SOC 2 controls. Password sign-in takes an authenticator-app code, required for platform admins by default, with single-use recovery codes; single sign-on users get MFA from their identity provider instead. Sessions end on the server at the earlier of a fixed lifetime and an idle timeout: 8 hours and 30 minutes idle for platform admins, 24 hours and 8 hours for everyone else, by default. Access reviews run on a cadence, with reminders, escalation of overdue campaigns and CSV/JSON reports. The audit log is sealed into a SHA-256 hash chain with signed checkpoints, and those checkpoints can stream to your SIEM so someone other than the server operator holds them. Weekly restore drills restore the newest backup into a scratch area and record RPO and RTO. Every new app answers an intake with its data classification, and every production deploy runs a release check.

Settings → Compliance produces a signed evidence pack for a chosen period, mapped to SOC 2 CC6, CC7, CC8, A1 and C1. AppCrane is self-hosted software, so the SOC 2 audit is of your environment: AppCrane provides the controls and the evidence pack, and the pack supports an audit rather than replacing one.

For environments that need FIPS 140-3, set APPCRANE_FIPS=require and AppCrane refuses to start unless Node runs a FIPS provider and a startup self-check passes. Passwords are PBKDF2-HMAC-SHA256, secrets AES-256-GCM, tokens and signatures HMAC-SHA256 and SHA-256, and MD5 is refused. AppCrane has no cryptographic module of its own, so the validated modules are yours to supply: a Node build linked to a CMVP-validated OpenSSL FIPS provider, Caddy built with Go's FIPS 140-3 module, FIPS base images and a host in FIPS mode. With those in place, the deployment runs in FIPS 140-3 mode using validated cryptographic modules. AppCrane is aligned with SOC 2 but not SOC 2 audited, and it runs in FIPS 140-3 mode on validated modules you supply but holds no CMVP certificate of its own.

Frequently asked

How is single sign-on enforced for an app my team did not write?

At the reverse proxy. Caddy runs forward_auth against AppCrane on every request, AppCrane resolves the caller's role on that app, and a caller with no role gets 403 without ever reaching the container. The app is then handed the verified identity as headers it can read without a callback.

Is enterprise SSO behind a paid tier?

No. SAML 2.0, OIDC and SCIM provisioning are in the AGPL-3.0 build with nothing to activate. The separate commercial licence is about running private modifications as a service, not about unlocking features.

Can a platform administrator read an app's secrets?

Not for an app they are not assigned to. Env vars are AES-256-GCM encrypted and access follows app assignment, including for platform_admin. Reveals are throttled and audited across both the HTTP API and MCP.

Will a vulnerable dependency stop a deploy?

The scan itself never blocks, by design. The separate release check is what can stop a production deploy: by default it blocks any app when a dependency has a known vulnerability with a published fix, unless an owner records a time-limited exception. A platform admin can turn that blocking off. What you get instead is a result recorded against every deployment and a fleet report that opens with how many rows actually have a usable scan result, as a fraction and a percentage.

What is backed up if I configure nothing?

The database and .env, nightly at 04:00, newest seven kept, on the same host. Not icons, per-app data, volumes, repositories or images — and no off-site copy until you enter a bucket and credentials, which every backup surface states plainly until an upload has completed.

How do I stop one app from taking the host down?

Per-app memory and CPU ceilings, applied as Docker flags at container creation. Because they are creation-time flags, use appcrane_check_resource_limits to find containers still running without a limit that was set after they started.

Keep reading

AppCrane — self-hosted · open source · agent-native github.com/gitayg/appCrane ↗