The real constraints of Embarko today, each with a recommendation for building around it.
| Limit | Recommendation |
|---|---|
| Memory is fixed at 256MB; CPU is shared | Keep the footprint small; add timeouts around CPU work |
| Disk has no quota | Cap logs, uploads and caches yourself |
| Network isolation is unverified | Authenticate and encrypt internal traffic |
Only DATA_DIR survives a redeploy | Store everything durable under DATA_DIR |
| No backups | Let the user export their data |
| Only the last few images are kept | Keep your own source history |
| No background workers | Run heavy jobs elsewhere |
| No custom Dockerfile | Use standard project layouts |
| The platform sets CSP | Don't rely on non-default CSP |
| No region choice | Never promise a location |
| Logs are a snapshot | Poll; ship important logs elsewhere |
| Public endpoints are rate-limited | Back off, don't loop |
| Apex domains skip your CDN | Prefer a subdomain |
Other pages: capabilities.md lists what exists, api.md is the call-by-call contract, and troubleshoot.md is the error reference. GET /capabilities is machine-readable (a contents page; open the topic you need). If any of these disagree, it wins.
Memory is reserved per app and enforced per container. An app that goes over is OOM-killed, not slowed down. A real incident confirmed this: the kernel killed one container and its neighbors were fine. CPU is a relative share, not a ceiling. A CPU-heavy neighbor can slow your app when the host is busy.
Recommendation:
Nothing stops an app from writing far more to disk than its fair share. A runaway app (endless logs, an uncapped cache, an upload endpoint with no size limit) can affect the shared host, not just itself.
Recommendation: cap anything that could grow without limit: rotate logs, limit upload size, evict cache entries. No quota does not mean free headroom. You are expected to police this yourself.
Nobody has tested whether one app's container can reach another's directly, skipping the platform's routing layer.
Recommendation: don't treat network isolation as a security boundary for anything sensitive. Authenticate and encrypt traffic you would otherwise trust "because it's internal". Treat every request as if it could come from outside the platform.
Every redeploy builds a fresh image and starts a fresh container. The only folder kept across redeploys is the one DATA_DIR points at. Everything else resets to what the image contains. Code that expects a folder outside DATA_DIR and outside your uploaded source can crash at once with ENOENT, not just lose data later.
Recommendation:
path.join(process.env.DATA_DIR, ...): a SQLite file, analytics counters, uploaded files, cached state.{ recursive: true }. Don't assume they exist.Surviving a redeploy is the only durability promise for DATA_DIR. There is no snapshot, no backup and no restore. It is safe from your own deploys, but not from hardware loss, and not from your app corrupting or deleting its own data. Deleting the app destroys the data for good. Automatic backups are in progress but not available.
Recommendation:
Every deploy builds a real image, kept on the host and never pushed to a registry. Only the most recent few are kept. Older ones are removed automatically to limit disk use. Rollback, by API or from the dashboard, can only reach images still on disk. Going further back returns a 422 that says exactly how far back you can go (see troubleshoot.md). That is a normal response, not a platform failure.
Recommendation: treat rollback as "undo my last few deploys", not a long-term archive. To rebuild a much older version, keep your own source history (git tags, releases) and redeploy from it.
Every app runs as one long-lived process listening on PORT. There is:
The default memory also assumes a web app, not a RAM-heavy processing job.
Recommendation: if the workload is really a background worker, don't fit it into an Embarko app today. That includes video or image processing, jobs that take minutes rather than seconds, and anything needing system binaries Railpack doesn't auto-install (ffmpeg, headless Chromium and so on). Run that part on infrastructure built for it, and have the Embarko app call it.
Apps are built with Railpack, which detects the language and runtime from the source. You can't supply your own Dockerfile or a fully custom build pipeline. Most apps (Node, Python, static sites) need no setup. A build that doesn't fit a standard framework layout has no override for the build step.
Recommendation: use the project layout your language already expects (a package.json start script, a standard static site layout). Auto-detection is built around these. Railpack does allow some runtime serving overrides in your own repo, for example a custom Caddyfile for static sites (see railpack.com). These never replace the build step itself.
The platform sets a default Content-Security-Policy and a few other security headers at the edge for every app. These override anything your app sets. You can't set a different CSP per app today.
Recommendation: WebAssembly's 'wasm-unsafe-eval' is already in the platform default. If you need a CSP directive the default lacks, there is no override today. Don't rely on non-default CSP behavior until there is a way to configure it.
No API call has a region parameter and the dashboard has no region setting. An app's compute and its DATA_DIR volume always sit together. The platform chooses the region and doesn't commit to one. Choosing a region is a planned feature.
Recommendation:
Checking logs returns the tail of the latest run's output at that moment. It doesn't stream, and how far back it goes is not guaranteed.
Recommendation: poll instead of expecting a live tail. If you need durable, long-lived logs (compliance, past debugging, anything you can't lose), ship them from the app to your own logging service. Don't use the platform as your log's system of record.
Two public endpoints are capped per email, per IP and globally:
| Endpoint | Per email | Per IP | Global |
|---|---|---|---|
Request a deploy token by email: POST /api/public/deploy-tokens/request | 5/hour | 10/hour | capped, number not stated |
Submit an anonymous customer query: POST /api/public/customer-query | 10 | 20 | 500/hour |
Both always return the same generic success response, whether or not the limit was hit and whether or not the email exists.
Recommendation: don't loop on a missing or unconfirmed email expecting a different answer. If you may have hit the limit, back off and retry later, not right away.
A subdomain (app.yourdomain.com) gets a CNAME to a dedicated platform hostname. An apex domain (yourdomain.com) can't use CNAME under standard DNS rules. It gets an A record pointing straight at the platform's origin, which skips any CDN or DDoS protection you would normally have in front.
Recommendation: prefer a subdomain over an apex domain if keeping your own CDN or proxy in front matters. Full details: troubleshoot.md.