A container app runs your server process (PHP, Ruby or Node) in a container on Dply Edge, behind the same hostname, firewall and routing as any other app. Use Container delivery when your app needs a long-running server: a Laravel or Symfony app, a Rails app, or a Node HTTP server such as Express, Nest, Fastify or Koa. Containers sleep when idle and you pay only for the seconds they run.
Note
Container apps are available on every plan. Starter has a $25 spending cap by default; past it, container traffic stops until the next billing period or until you raise the cap on the Billing page. See Spending caps & alerts.
Create a container app
When you create an app, dply detects the repository's runtime. PHP and Ruby repositories get Container delivery preselected; a Node repository can also choose it.
During create you pick a starting plan for the container. Each plan fills in the settings below, and you can change any of them later.
| Plan | Size | Instances | Sleeps after | Scheduler | Migrations on boot |
|---|---|---|---|---|---|
| Flex (default) | 0.25 vCPU, 1 GiB | 1 | 5 minutes | Off | Off |
| Small | 0.5 vCPU, 4 GiB | 1 | 30 minutes | On | Off |
| Medium | 1 vCPU, 3 GiB | 2 | 1 hour | On | On |
How a deploy builds your image
On every deploy dply clones the repository and uses your Dockerfile if there is one. Without one, it generates Dockerfile.dply for the detected stack:
| Stack | What the generated image does |
|---|---|
| PHP (Laravel, Symfony) | Picks the newest PHP (8.2–8.5) your composer.json allows, installs the extensions your dependencies require, runs composer install, and builds frontend assets when package.json has a build script. Serves with php-fpm behind nginx. If laravel/octane is installed it runs Octane instead (RoadRunner when spiral/roadrunner is present, otherwise Swoole). |
| Rails | Ruby slim image, bundle install, assets:precompile, then Puma. |
| Node | Installs with your lockfile's package manager, runs npm run build when present, then npm start (or the pnpm/yarn equivalent). |
Generated images listen on port 8080. A Node server must listen on process.env.PORT. With your own Dockerfile, dply sends traffic to the first EXPOSE port.
For a Laravel app, dply adds the dply/laravel package to the image when the app uses the scheduler, a database, queue workers or an attached resource that needs it. If you ship your own Dockerfile, add it yourself:
composer require dply/laravel
OS packages
Generated images carry only what the stack needs. To add other software, such as ffmpeg, ghostscript or poppler, list it in dply.yaml:
container:
packages: [ffmpeg]
| Stack | Installed with | Look up names at |
|---|---|---|
| PHP | apk add (Alpine) |
pkgs.alpinelinux.org |
| Rails, Node | apt-get install (Debian) |
packages.debian.org |
Use plain package names: versions and flags are not accepted. A name the package manager doesn't know fails the deploy. Queue workers and the scheduler run the same image, so they get the packages too.
On PHP the packages are part of the runtime image that Faster starts reuses. The deploy after you change the list rebuilds it once, and code-only deploys skip the rebuild again.
PHP extensions don't go here. Require them in composer.json, for example "ext-gd": "*", and dply installs any the image lacks before composer install. A package that only suggests an extension doesn't count: require it yourself.
container.packages only applies to images dply generates. With your own Dockerfile, add the install step there. Build shows what the live image has under What the app runs on.
Environment and first-deploy defaults
Every production variable under Environment is passed into the container's process environment before the app starts. Read them with your framework's normal environment API.
On the first deploy dply fills in what the framework needs to boot, only where you have not set a value, and saves them under Environment so you can change them:
| Stack | Defaults |
|---|---|
| Laravel | A generated APP_KEY, APP_ENV=production, APP_URL and ASSET_URL set to the app's URL, LOG_CHANNEL=stderr, SESSION_DRIVER=cookie |
| Rails | A generated SECRET_KEY_BASE, RAILS_ENV=production, RAILS_LOG_TO_STDOUT=1, RAILS_SERVE_STATIC_FILES=1 |
| Node | NODE_ENV=production |
Warning
A Laravel app with no database of its own boots on SQLite at /tmp/database.sqlite. Container disk is wiped whenever an instance starts, and instances do not share a disk, so anything written there can disappear. Attach a database from Add resource before you store real data. See Postgres, MySQL & MongoDB.
Use a database or Valkey store for cache and sessions, and object storage for uploads.
Choose a size
Open your app's Overview, select the App card, then choose a Size. The change applies on the next deploy.
| Size | Memory | Per second awake | Per hour awake | Always on, 25% CPU | Most per month |
|---|---|---|---|---|---|
| 0.25 vCPU | 1 GB | $0.0000101 | $0.0364 | $13.58 | $21.85 |
| 0.5 vCPU | 4 GB | $0.0000267 | $0.0962 | $44.01 | $57.73 |
| 1 vCPU | 3 GB | $0.0000363 | $0.131 | $43.54 | $78.40 |
| 2 vCPU | 6 GB | $0.0000726 | $0.261 | $87.07 | $156.80 |
| 4 vCPU | 12 GB | $0.000145 | $0.521 | $173.20 | $312.81 |
Below the ladder, 1/16 vCPU (256 MB, non-PHP apps only) is $0.00000262 per second awake ($0.0094 an hour).
The per-second and per-hour prices are per instance with every vCPU busy. CPU is billed only while it works, and a sleeping instance costs nothing. Always on, 25% CPU is one instance awake all month (720 hours) at a typical web app's load. Most per month is the cap: however busy the CPU, one instance never bills more than that in a month (two always-on instances, twice that). Internally the sizes are keyed basic (0.25 vCPU), standard-1 (0.5), custom-1 (1), custom-2 (2) and standard-4 (4). 1/16 vCPU is available only to non-PHP apps.
An app created before 2026-09-27 on 1 vCPU / 6 GB (standard-2) or 2 vCPU / 8 GB (standard-3) keeps that size, shown as retired, and is billed for the memory it runs, until you pick a size. The 1 and 2 vCPU sizes now carry 3 and 6 GB, which costs less; the App card suggests the move when a week of memory peaks says the app fits.
Important
PHP apps need at least 0.25 vCPU (1 GiB). On 1/16 vCPU a PHP process runs out of memory and every request fails. Octane apps (Swoole, RoadRunner) need at least 0.5 vCPU. dply does not stop you from picking a smaller size.
Custom size
Choose Custom on the App card to set vCPU, memory and disk yourself, then Save custom size:
- vCPU: 1 to 4.
- Memory: at least 3 GiB per vCPU, at most 12 GiB.
- Disk: at most 2 GB per GiB of memory, and at most 20 GB.
For example, 1 vCPU with 3 GiB and 6 GB of disk costs up to $0.121 an hour ($88.05 a month always on).
Right-sizing suggestions and memory crashes
dply samples each awake app's peak memory every hour. After six samples in a week on the same size, the App card suggests the smallest size that leaves 30% headroom over the highest peak, and shows how much it saves per hour. It never suggests a larger size.
When a deploy's health check or logs show the process was killed for memory, dply raises the app one size and redeploys. It does this at most once per size, and never for a custom size.
Warning
An automatic size increase raises the app's hourly cost. You are notified that the app was unhealthy after deploy; check the size on the App card afterwards.
Instances
Max instances on the App card is the most instances that run at once (1 to 20; the card offers 1, 2, 3, 4, 5, 8, 10 and 20). The first start runs only this many. A gradual deploy can briefly run one extra, so the new version starts before the old one stops.
Min instances (always awake), right under it, is how many never sleep (0 to the maximum). 0 lets every instance sleep when idle.
Requests are spread across instances as they fill up, and a cookie keeps a visitor on the same instance. See Scaling & sleep for how many requests an instance takes, minimum instances, scaling windows and sleep.
To restart the app without a deploy, open Deploys and choose Restart the app, or run dply-edge container restart. Running instances restart one at a time, so the others keep serving; sleeping ones stay asleep. They come back on the live build with the env vars it was deployed with, so an env var change still needs a deploy.
Regions and jurisdictions
Open Overview, select the App card, then Sleep, scaling, region:
- Run only in: Anywhere (fastest), EU only or US FedRAMP only.
- Regions: tick one or more placement regions (ENAM, WNAM, EEUR, WEUR, APAC, SAM, ME, OC, AFR). EU only allows EEUR and WEUR; US FedRAMP only allows ENAM and WNAM. Leave all unticked to use every region inside the choice above.
If your app uses a dply database or dply Valkey and you leave regions unticked, it runs in the region next to that data. See Data regions.
Rollouts
A deploy replaces running instances according to the rollout settings in Sleep, scaling, region:
| Mode | What happens |
|---|---|
| Gradual (default) | Instances move to the new image in steps. One extra instance is reserved so the new image starts before an old one stops. |
| Immediate | Every instance moves to the new image in one step. |
| None | The deploy updates the routing Worker only. Running instances keep the current image until you pick Gradual or Immediate. |
- Steps: comma-separated percentages of instances on the new image, increasing, ending at
100, at most 10 steps. Leave blank for the default:100with one instance, otherwise10, 100. - Wait before replacing (seconds): 0 to 3600.
0replaces an instance as soon as the rollout reaches it; a higher number leaves it alone until it has been running that long.
A replaced instance is asked to stop and has 15 minutes to exit. You cannot start a new deploy while a rollout is still in progress.
After each deploy dply requests the app's URL. If the app does not answer, or answers with an error that looks like a platform failure, the deploy is marked failed and you are notified. An HTTP 500 from your own code is treated as your app's response.
Faster starts (beta)
Where your dply offers it, Faster starts in Sleep, scaling, region moves the app to Cloudflare's newer container scheduling:
- Faster deploys. A code change ships your app's files (a compressed copy of
/app, typically 10 to 30 MB) instead of building and pushing a new image. A deploy takes about a minute. The image itself is rebuilt only when its base changes (PHP version, extensions, server). - Faster wakes. The first start of each release saves a snapshot of the container; later wakes start from it with your code already in place.
- No rollout. Each instance moves to the new version on its first request after a deploy, so the Rollouts settings are hidden. Queue workers finish their current job first.
Turning it on or off takes the app down for a minute or two during that deploy. It is not available with regions or a jurisdiction set, and it applies to generated Dockerfiles; an app with its own Dockerfile gets the faster scheduling but still builds an image on each deploy.
Migrations on boot
Turn on Run migrations when a container starts in Sleep, scaling, region to run migrations as each container starts:
- Laravel:
php artisan migrate --force --isolated - Rails:
bundle exec rails db:prepare
It is off by default. Migrations run on every start, including every wake from sleep, so they add a second framework boot to each cold start. It applies to generated Dockerfiles only. The equivalent environment variable is DPLY_MIGRATE_ON_BOOT=1.
PHP performance defaults
Generated PHP images come tuned:
- OPcache is on with 128 MB, 20,000 files and no timestamp checks (the code never changes inside a container). JIT (
tracing, 64 MB) is on for php-fpm, Swoole and RoadRunner, and off for FrankenPHP. - Laravel caches. Routes and events are cached when the image is built. The config cache is built as each container starts, since your environment variables only exist at runtime (Octane apps skip it: they boot the app once per worker), and views compile the first time they render. If a cache step fails the app starts without it and the log says so. If your app can't boot without its environment, routes and events are cached at start instead.
- Kept connections. With dply/laravel, Redis (phpredis) connections stay open between requests, and on php-fpm so do Postgres and MySQL connections, so a request skips the connect and TLS handshake. (FrankenPHP threads never exit, so there each would hold a database connection for as long as the container runs.) A connection you configure yourself (
PDO::ATTR_PERSISTENTin a connection'soptions, orREDIS_PERSISTENT) is left as you set it. Session settings your code changes (SET …, advisory locks) carry over to the next request on the same worker.
Worker mode
Worker mode in Sleep, scaling, region runs a Laravel app with php artisan octane:frankenphp: the app boots once per worker and serves every request from memory, instead of booting the framework for each request. It is off by default.
It needs laravel/octane in require and the FrankenPHP server ("extra": {"dply": {"php-server": "frankenphp"}} in composer.json). Until a deploy finds both, the switch stays off. Apps with Octane and no pin already run under Swoole or RoadRunner.
State leaks between requests in worker mode. Singletons, static properties and anything your code keeps in memory survive from one request to the next, including the previous user's data if you store it there. Only turn it on for an app that is written for Octane. Each worker restarts after 500 requests.
Cold starts
When a request reaches a sleeping app, dply starts an instance and waits up to 45 seconds for its port to open. If the container is not running or crashes before answering, the request is retried up to two more times. Requests with a body over 1 MB are streamed and not retried. Expect a cold start to take a few seconds; keep instances awake with Min instances if that matters (see Scaling & sleep).
A visitor opening a page in a browser waits up to 3 seconds. If the app hasn't answered by then, they see a Starting up… page that reloads by itself until the app answers. Crawlers, API calls and form posts don't get this page: they wait for the app's own answer.
Some apps open their port before they can serve a page. Set DPLY_READY_PATH to a path that answers below 400 only once the app is ready, such as /healthz/readiness for n8n, and dply waits for that path instead of an open port. The n8n and Ghost templates set it for you. If the path still isn't ready 30 seconds after the port opens, the app gets traffic anyway.
WebSockets
An app that serves its own WebSockets (Reverb, Action Cable, socket.io) works on the same hostname. The upgrade headers reach your app unchanged, and the 101 response passes through without header rules or caching.
- A socket goes to the instance the visitor's page load was sent to.
- Each open socket counts as one in-flight request against the instance's capacity.
- An instance with any open socket never sleeps, so one idle browser tab keeps it (and its billing) running. Close idle sockets if scale-to-zero matters.
- A rollout replaces instances, so clients should reconnect after a deploy.
For a managed relay instead of your own socket server, see Realtime (WebSockets).
Logs
What your app, its queue workers and the routing Worker printed in the last 15 minutes is on Build & deploy logs, under What your app is printing. Filter by source or worker in the dialog. Log lines appear after the deploy that enables them. See Logs.
How compute is billed
Container compute is metered per second: vCPU while it works, memory and disk while an instance runs. Traffic your app sends to visitors bills once, as bandwidth, like any site. Traffic your app sends out on its own (calls to outside APIs, S3, webhooks) bills as App outbound traffic at the rate above. Log lines your app writes, plus one per request, bill as Workers Log events. Usage is collected hourly and appears as Apps and workers (compute) on the billing page, for web instances, queue workers and preview containers alike. It bills at the rates above. See Plans & pricing.
The App card's Cost estimate shows the running rate per second, minute, hour and day (every vCPU busy) for your size and instance count, the always-on month at typical CPU, the monthly cap, and what a given number of Hours awake each day costs.
Each app's compute for a billing period is capped at the size's Most per month for each instance-month it ran: its memory-seconds divided by one instance's memory for 720 hours, at least one. The cap never takes a bill below what dply pays Cloudflare.
Previews of a container app run their own container and are billed the same way. They never run the app's crons or consume its queues.