# Deploy an app without a Dockerfile

URL: https://getflowing.ai/docs/build/without-a-dockerfile

Which stacks GetFlowing builds with no Dockerfile, the versions and start commands it picks, the variables it asks for, and what to do when it cannot.

Most apps need no Dockerfile. If yours has none, GetFlowing reads its files, works out the stack, and writes a Dockerfile for it when it builds. It never adds the file to your repository.

The Dockerfile it wrote is printed at the start of the build log. When the deploy goes live, GetFlowing says what it built the app as, for example:

> GetFlowing built it as a Next.js app (pnpm, Node 24), with a Dockerfile it wrote as the repository has none of its own; add one to the repository to change how it is built.

A connected AI assistant can find out before anything is built: its `check_app` says what GetFlowing would build the app as, or why it cannot. To change how your app is built, add your own Dockerfile. See [Take over with your own Dockerfile](#take-over-with-your-own-dockerfile).

## The stacks it recognises

The first stack that matches your app is used, in the order below. A PHP, Ruby, Go, Rust, Java, .NET, Elixir, Gleam or Deno app can also have a `package.json`, for its front end or its npm packages, and it is still built as its main stack. A Python app that lists Django, FastAPI, Flask, Starlette, gunicorn or uvicorn is built as Python even if it also has a `package.json`.

| Stack | Recognised by | How it is built and started |
| --- | --- | --- |
| PHP | `composer.json`, or an `index.php` at the root or in `public/` | `composer install` without development packages, served by Apache with the PHP extensions Laravel and Symfony need. Where `public/` has the `index.php`, it is the site's root, and a path with no file goes to that `index.php`. A front end with a `build` script in `package.json` is built with Node. |
| Ruby | `Gemfile` | `bundle install` without the development and test gems. A Rails app's front end is built with Node where it has a `build` script, its assets are precompiled where it uses Propshaft or Sprockets, and it runs `rails server`. Another app runs Puma, or rackup on `config.ru`. |
| Go | `go.mod` | The main package at the root, or the one folder in `cmd/`, built as one program. The module's files are kept beside it, for the templates and static files it reads. |
| Rust | `Cargo.toml` | `cargo build --release`, running the binary its one `[[bin]]` or its package names. |
| Java | `pom.xml`, `build.gradle.kts` or `build.gradle` | Packaged with Maven or the Gradle wrapper, without tests, then the largest jar it made is run, or Quarkus's `quarkus-app`. Java may use three quarters of the app's memory. |
| .NET | A `.csproj` or `.fsproj` at the root, or one web project in `src/` beside a solution file | `dotnet publish`, run on ASP.NET Core. |
| Elixir | `mix.exs` | `mix release` for production. A Phoenix app's assets are built with `mix assets.deploy` where `mix.exs` defines it, and its server is started. |
| Gleam | `gleam.toml` | `gleam export erlang-shipment`, run on Erlang. |
| Deno | `deno.json` or `deno.jsonc`, or a `deno.lock` with no `package.json` | Installed, built with its `build` task, and started with its `start` task or its main module. |
| Python | `requirements.txt`, `pyproject.toml`, `Pipfile`, `setup.py`, `manage.py`, or the lockfile of uv, Poetry or Pipenv | Installed with uv, Poetry or Pipenv from their lockfiles, otherwise pip. Django and Flask run on gunicorn, FastAPI on uvicorn, where the app lists that server. |
| Node | `package.json` | Installed with npm, pnpm, Yarn or Bun, built with your `build` script, started with your `start` script or the framework's own server. |
| Static site built with Node | Vite, Create React App, Angular without server rendering, Gatsby, Vue CLI, Astro without a server adapter, SvelteKit with `adapter-static`, Next.js with `output: 'export'`, Docusaurus or Eleventy | Built with Node, then served as files. |
| Static site | An `index.html` with no server code or `.env` file beside it | Served as it is. |

Every app it builds runs as a user other than root.

### Static sites

A static site is served as files. A path is served as its file, its `.html` page or its folder, and anything else gets `index.html`, so a single-page app's routes work. Files whose names start with `.env`, `.git` or `.ht`, and `.npmrc`, are never served.

## Choosing versions

GetFlowing reads the version your app asks for, and otherwise uses its default.

| Stack | Where it reads the version | Versions | Default |
| --- | --- | --- | --- |
| Node | `.nvmrc`, `.node-version`, `.tool-versions`, Volta, or `engines` in `package.json` | 20, 22, 24 and 26. An older one runs on the nearest of these above it. | 24 |
| Python | `.python-version`, `runtime.txt`, the `Pipfile`, or `requires-python` | 3.10, 3.11, 3.12, 3.13 and 3.14 | 3.13 |
| Go | The `toolchain` or `go` line in `go.mod` | 1.21 to 1.27. Others build on the nearest of these. | 1.27 |
| Rust | `rust-toolchain` or `rust-toolchain.toml` | Any release | 1.98 |
| Java | `pom.xml` or the Gradle build | 8, 11, 17, 21 and 25. Another runs on the nearest of these above it, or on the newest. | 21 |
| .NET | `TargetFramework` in the project | 8.0, 9.0 and 10.0. An older one runs on 10.0. | 10.0 |
| Ruby | `.ruby-version`, or the `ruby` line in the `Gemfile` or `Gemfile.lock` | 3.1, 3.2, 3.3 and 3.4 | 3.4 |
| PHP | The `php` version `composer.json` requires | 8.1, 8.2, 8.3 and 8.4 | 8.4 |
| Elixir | Not read | Elixir 1.20 on Erlang/OTP 28 | The same |
| Gleam | Not read | Gleam 1.19.2 on Erlang/OTP 28 | The same |
| Deno | Not read | Deno 2.9.8 | The same |

The package manager comes from `packageManager` in `package.json`, or from the lockfile you commit. Commit your lockfile: the install is held to it, so the versions you tested are the versions that are built.

## Choosing the start command

- **Node:** a `Procfile`'s `web:` line, else the `start:prod`, `start` or `serve` script in `package.json`. A script that only runs a development server is skipped. With none, GetFlowing tries the framework's own server (Next.js, Nuxt, SvelteKit with `adapter-node`, Astro with `@astrojs/node`, NestJS), then the `main` file in `package.json`, then `server.js`, `index.js`, `app.js` or `main.js`.
- **Python:** a `Procfile`'s `web:` line, else the framework's production server: gunicorn for Django and Flask (uvicorn for a Django app served over ASGI, waitress for Flask), uvicorn for FastAPI. With no framework, `python main.py`, `app.py` or `server.py`, which must then read `PORT` itself.
- **Ruby:** a `Procfile`'s `web:` line, else `rails server` for Rails, else Puma or rackup on `config.ru`.
- **Deno:** a `Procfile`'s `web:` line, else the `start` task in `deno.json` (its `preview` task where `start` only watches for changes), else its main module, such as `main.ts`, `server.ts` or `app.ts`, with every permission. An app that serves with `Deno.serve` or `deno serve` answers on the port in `PORT` whatever address it names, and a module that exports a fetch handler is served.
- **PHP:** Apache, on the port in `PORT`.
- **Go, Rust, Java, .NET, Elixir and Gleam:** the program the build made.

Whatever starts your app must listen on `0.0.0.0` on the port in `PORT`. See [What every app needs](https://getflowing.ai/docs/build/app-requirements).

## Variables the build reads

A front end reads some variables when it is built, not when it starts, and puts their values into the pages every visitor downloads. When GetFlowing writes the Dockerfile, its build is given your app's variables with the prefix the app's framework reads:

| Prefix | For |
| --- | --- |
| `NEXT_PUBLIC_` | Next.js |
| `VITE_` | Vite, and a Laravel, Rails or Deno front end built with it |
| `REACT_APP_` | Create React App |
| `PUBLIC_` | Astro and SvelteKit |
| `GATSBY_` | Gatsby |
| `VUE_APP_` | Vue CLI |
| `EXPO_PUBLIC_` | Expo |
| `FRESH_PUBLIC_` | Fresh, when its `build` task runs |

- Only variables not marked secret are given. When you add one, untick **Store as a secret**.
- A variable marked secret is never given to the build, whatever its prefix. The deploy names any it left out:

> NEXT_PUBLIC_API_URL is marked secret, so the build was not given it: a variable with a public prefix is put into the pages every visitor downloads. If it is meant for the browser, unmark it as secret; if not, rename it without the prefix.

- The values are never written into the image's history or the build log. The log names the variables only.
- A changed value reaches the app at its next deploy, which builds it again.
- If these variables come to more than 16 KB compressed, the deploy is refused before anything is built.

With a Dockerfile of your own, the build is given none of your variables. See [Variables and secrets](https://getflowing.ai/docs/configure/variables#values-used-while-building).

## What an app cannot start without

Some frameworks cannot start without a secret that only you can give. GetFlowing finds them from the app's code and asks for them before the deploy:

| App | Variable |
| --- | --- |
| Laravel | `APP_KEY` |
| Rails | `RAILS_MASTER_KEY` or `SECRET_KEY_BASE` |
| Phoenix | `SECRET_KEY_BASE`, where `config/runtime.exs` stops without it |
| Django | The variables its settings read `SECRET_KEY` and `ALLOWED_HOSTS` from, where they have no default it can start with |

If the app's variables do not have one, the deploy is refused before anything is built. The message names each variable and what it is for, for example:

> GetFlowing builds this as a Laravel app (PHP 8.4), which cannot start without APP_KEY, the key Laravel encrypts sessions and cookies with: run php artisan key:generate --show on your computer and set what it prints, marked secret. Set it in the app's Variables, then deploy again.

Set each one in the app's Settings, under Variables, a key marked secret, then deploy again. GetFlowing compares only the names: it never reads, makes or logs a value. The app's chat says the same before it asks for any approval, and a connected assistant's `check_app` lists them.

## Framework notes

### Phoenix

- Where `config/runtime.exs` reads `PHX_HOST`, every deploy sets it to the app's address, unless the app's variables set it.
- An app that uses PostgreSQL through Ecto (`postgrex` in `mix.lock`) is given a managed database, under the variable `config/runtime.exs` reads. See [Databases](https://getflowing.ai/docs/configure/databases).
- Its server is started, and it runs with no Erlang distribution.

### Django

- **Static files** are collected when the app is built, where WhiteNoise serves them and `STATIC_ROOT` is set.
- **`ALLOWED_HOSTS`**: the health check asks the app by its own address, which Django refuses unless `ALLOWED_HOSTS` names it. Where the settings read `ALLOWED_HOSTS` from a variable as a list, such as `os.environ.get("ALLOWED_HOSTS", "").split(",")`, GetFlowing adds that address when it starts the app (a `Procfile` that starts it is left as it is). Where the hosts are written into the settings, or read as one host, and do not include `"*"`, the health check fails, and a connected assistant's `check_app` says so. Read them from a variable as a list instead.

### Laravel and Rails

Laravel's `APP_KEY`, and Rails's `RAILS_MASTER_KEY` or `SECRET_KEY_BASE`, are asked for before the deploy. A front end with a `build` script in `package.json` is built with Node, and given the variables its build reads.

## Apps in a monorepo

<Callout type="info">
Importing several apps from one repository is not on for every workspace yet. Until it is on for yours, an import builds from the root of the repository.
</Callout>

A Node app in a folder of its repository, where a workspace at the repository's root names that folder (in the `workspaces` list of the root's `package.json`, or the `packages` of `pnpm-workspace.yaml`), is built from the repository's root, with the lockfile committed there. The packages it depends on are built first.

| Package manager | What is installed |
| --- | --- |
| pnpm | The app, the packages it depends on, and the root |
| npm | The app and the root |
| Yarn 4 and later | The app and the packages it depends on. The build stops if `yarn.lock` does not match the workspace's `package.json` files. |
| Yarn 1 to 3, and Bun | The whole workspace |
| Turborepo | The app and what it depends on, pruned by Turborepo, where the root lists `turbo`, names its `packageManager`, and its lockfile holds Turborepo 2 or later |

A Node app with a lockfile of its own, which takes nothing from a workspace, is built from its folder alone. So is an app of any other stack in a folder.

A workspace's app is refused when:

- There is no lockfile committed at the repository's root.
- It takes a package from a `file:` or `link:` path outside the repository.
- It takes packages from a workspace (`workspace:` or `catalog:` versions, or a path outside its folder), and no workspace at the root names its folder.
- It takes a package with a `workspace:` version, and no package of that name is in the workspace's folders.
- Its `package.json` has no `name`, and it is built with pnpm, Yarn 2 or later, or Turborepo, which pick it by its name.

## Apps it cannot build without a Dockerfile

For these apps GetFlowing does not guess. It says why before it builds, and you add a Dockerfile or change the app.

| Your app | Why | What to do |
| --- | --- | --- |
| SvelteKit on `adapter-auto` | `adapter-auto` builds only for Vercel, Netlify and Cloudflare | Use `@sveltejs/adapter-node`, or `adapter-static` for a static site |
| Astro with another host's adapter | The adapter builds for that host | Use `@astrojs/node`, or no adapter for a static site |
| Django or Flask with no production server | Nothing listed can serve it in production | Add `gunicorn` to your dependencies |
| FastAPI with no uvicorn | Nothing listed can serve it | Add `uvicorn`, or `fastapi[standard]` |
| FastAPI or Flask whose app is not found | No `app = FastAPI(...)`, `app = Flask(...)` or `create_app()` in `main.py`, `app.py` or the other files such an app starts from | Add a `Procfile` with a `web:` line |
| Django whose `manage.py` names no settings module | The project to serve is not known | Add a `Procfile` with a `web:` line |
| Python with nothing to start | No `Procfile`, web framework or `main.py` | Add a `Procfile` with a `web:` line |
| Node with nothing to start | No start script, and no `server.js`, `index.js`, `app.js` or `main.js` | Add a `start` script |
| Node with no lockfile, taking some packages at any version (`"*"`) | A build of its folder alone would install whatever the public registry has under those names | Commit the lockfile, or build it as part of its workspace |
| A static site framework with no build script | Its files cannot be made | Add a `build` script |
| A workspace's app that cannot be built as one | See [Apps in a monorepo](#apps-in-a-monorepo) | Change the workspace, or add a Dockerfile |
| Gradle without its wrapper | Which Gradle builds it is not known | Commit `gradlew` and its `gradle` folder |
| PHP asking for a version not listed above | There is no image for it | Ask for one of 8.1, 8.2, 8.3 and 8.4 |
| Go with several commands in `cmd/`, none named after the module or `server`, `api`, `web` or `app` | Which one serves the app is not known | Add a Dockerfile that builds the right one |
| Go with no main package at its root or in `cmd/` | There is nothing to run | Add a Dockerfile |
| A Cargo workspace | Which crate serves the app is not known | Add a Dockerfile |
| .NET with several web projects, or several project files at the root | Which one is the app is not known | Add a Dockerfile |
| An Elixir umbrella project whose `mix.exs` names no release | Which of its apps serves the app is not known | Name the release in `mix.exs` |
| Gleam on the JavaScript target | Only Gleam on Erlang is built without a Dockerfile | Add a Dockerfile |
| Gleam taking a package from a path outside its folder | The build does not have that path | Add a Dockerfile |
| Deno with nothing to start | No `Procfile`, start task in `deno.json` or main module | Add a `start` task |
| An `index.html` beside server code or an `.env` file | Serving the folder as a static site would publish them | Move the site into its own folder, or add a Dockerfile |
| None of the files above | No stack is recognised | Add a Dockerfile |

## Not covered yet

- **Stacks not in the table above.** Add a Dockerfile for them.
- **Other Elixir, Gleam and Deno releases.** Each is built on the one release in [Choosing versions](#choosing-versions). An app that needs a newer one fails its build, and the build log says why.
- **Installing one app of a Yarn 1 to 3 or Bun workspace.** The whole workspace is installed.
- **A Django app whose hosts nothing can be added to.** It fails its health check. See [Django](#django).

## Take over with your own Dockerfile

To change how your app is built, add a Dockerfile where GetFlowing looks for one. GetFlowing then builds from yours and writes none. The one it wrote is at the start of the build log, so you can start from that. See [Deploy an app with your own Dockerfile](https://getflowing.ai/docs/build/dockerfile).

A Dockerfile of your own is given none of your app's variables while it builds, so a front end that reads them at build time needs another way to get them. See [What it does not support](https://getflowing.ai/docs/build/dockerfile#what-it-does-not-support).
