Languages
What the platform detects in a .NET, Go, Ruby, PHP, Node.js, Python or Rust project, which version it builds on, and what ends up in the image.
A project with no Dockerfile is built from a Dockerfile the platform writes for its language, on that language's official image. This page says, language by language, what is detected, which version is used and how to pin another, which commands run and how to replace them, and what the running image contains. It is about projects that run as a server; static sites (Vite, Astro, Hugo…) are onBuilds.
How the language is decided
A Dockerfile in the build root wins over everything below: it is built exactly as written. Without one, the first line of this table that matches your build root decides.
| Language | Recognised by |
|---|---|
| .NET | a .csproj, .fsproj or .vbproj file |
| Go | go.mod |
| Ruby | Gemfile |
| PHP | composer.json or index.php |
| Node.js | package.json, with no other language's manifest beside it |
| Python | requirements.txt, pyproject.toml, Pipfile, setup.py or main.py |
| Rust | Cargo.toml |
The order is what settles a mixed tree. A Go service with a package.json for its CSS is a Go project, a Laravel app with Vite is a PHP project, a Rails app with jsbundling is a Ruby project, and a Flask or Django app with a Vite front end is a Python project whose front end is built in the same image.
Languages with no row here (Elixir, Java, Deno, Dart and others) are built by a general-purpose fallback builder, which handles the common layouts. A Dockerfile is the way to full control for any of them. A tree that nothing here can build stops with "add a Dockerfile; supported stacks:" and the list above.
Versions at a glance
| Language | When nothing is pinned | Pinned by | Built on | Runs on |
|---|---|---|---|---|
| .NET | none: the project must name it | <TargetFramework> | Microsoft's sdk:<version> | Microsoft's aspnet:<version> |
| Go | the current release | cannot be pinned lower | golang:1-alpine | alpine:3 |
| Ruby | 3.3 | Gemfile ruby line, .ruby-version | ruby:<version> | the same image |
| PHP | 8.3 (8.2–8.4 available) | composer.json require.php | the platform's PHP image | the same image |
| Node.js | 24 | engines.node, .nvmrc, .node-version | node:<major> | node:<major>-slim |
| Python | 3.13 (3.9 or later) | .python-version, requires-python, runtime.txt, Pipfile | python:<version> | python:<version>-slim |
| Rust | the newest stable | rust-toolchain.toml | rust:<version>-bookworm | debian:bookworm-slim |
What every language has in common
- Replacing a command.
install,commandandstartunder[build]in nesly.toml replace the matching step in every language; the console andnesly build setwrite the same fields (seeBuilds). AProcfilewith aweb:line is the start command when you set none. - Other builders' settings.
NIXPACKS_*variables andnixpacks.tomlare not read for any language on this page; write the same thing in nesly.toml[build]. When your project has either, the build log says so in one line. - The port. Every image sets
PORT=8080. Listen on$PORTand on all interfaces (0.0.0.0), not on localhost. - Your environment variables reach every build step as a secret. They are not written into the image; the running container gets them from the platform.
- No system packages. The build installs nothing with apt: what the base image carries is what you have, and each section below says what that is. A system library beyond it needs a Dockerfile.
- Images come from the official ones (for PHP, the platform's own, built on the official one) through the platform's registry cache. The images a project that pins nothing builds on are kept warm on the build machines, so a first build does not wait for them to download.
- Faster rebuilds. For Node.js, Python and PHP only the dependency files are copied before the install, so a deploy that changes only source code reuses the installed layer. Node.js, Python, PHP and Rust keep downloaded packages in a cache between builds.
- The container starts as root. A build has 15 minutes.
.NET
Recognised by a .csproj, .fsproj or .vbproj anywhere in the first few directory levels (bin, obj and node_modules are skipped). With a single .sln at the root that lists projects, only those count. With several projects, the one that uses Microsoft.NET.Sdk.Web is published and its project references are restored with it; several web projects, or several projects and no web one, are refused with a sentence naming them, and a Dockerfile picks one.
Version. The <TargetFramework> of the published project, or of a Directory.Build.props or Directory.Build.targets above it: net10.0 builds on Microsoft's sdk:10.0 image and runs on aspnet:10.0. There is no default version; a project whose framework cannot be read is refused before the build. Changing the version means changing TargetFramework.
Build. dotnet publish "<project>" -c Release -o /app -r linux-x64 --self-contained false -p:PublishReadyToRun=true: ReadyToRun, so the app's code is precompiled and the first requests after a cold start spend less time compiling, and framework-dependent, so it runs on the aspnet image. The runtime identifier follows the build machine's architecture (linux-arm64 on arm64) and replaces a RuntimeIdentifier the project sets itself. Native AOT is not used; it needs a Dockerfile.
Turning ReadyToRun off. Set <PublishReadyToRun>false</PublishReadyToRun>in the project file or a Directory.Build.props: a project that sets the property decides alone. An environment variable PublishReadyToRun=false on the project does the same without a code change. The restore downloads the ReadyToRun compiler and runtime packs from nuget.org, so anuget.config that clears nuget.org needs a feed that carries them, or ReadyToRun off.
Migrations. When the tree has an EF Core Migrations folder and either references Entity Framework Core or asks for dotnet ef database update as its release command, a migration bundle is built to /app/efbundle as well (seerelease commands).
Start. dotnet <name>.dll, where the name is the project's<AssemblyName>, or the project file's name without one.
Overrides. install runs before the publish;command replaces the publish and must write its output to/app; start replaces the entrypoint.
In the image. aspnet:<version> with the publish output in/app: no SDK and no source code. ASPNETCORE_URLS is set to port 8080, so the app needs no listen address of its own.
Go
Recognised by a go.mod in the build root.
Version. Always the current Go release (golang:1-alpine). Thego line in go.mod is a minimum, not a pin: the newest release builds it, and a module that asks for a toolchain newer than the image gets that toolchain downloaded. An older Go cannot be chosen here; a Dockerfile can.
Build. Without a go.sum, go mod tidy writes one first. Thengo build -trimpath of the package in the build root, withCGO_ENABLED=0.
Start. The binary that build produced.
Overrides. install runs after the tidy and before the build.command replaces go build; the platform then cannot know which binary you built, so a build command without a start command is refused. startreplaces the binary as the container's command.
In the image. alpine:3 with your whole source tree in/app, so templates and static files read by relative path are found, and the binary outside it, where no file of yours can collide with it. With your own build command, the image holds what that command left in the tree, and your start command runs it from /app.
Ruby
Recognised by a Gemfile. Unless you set a start command, a deploy with no Gemfile.lock (bundle lock writes it) or no config.ru is refused before the build, naming the file. A Gemfile beside a Jekyll _config.yml is a static site, not this.
Version. The ruby line of the Gemfile, then.ruby-version. An exact version (ruby "3.2.4") builds on exactly that image; a range (ruby "~> 3.3") on the newest patch of the minor it names. With neither, Ruby 3.3. CRuby only: JRuby, TruffleRuby and mruby need a Dockerfile.
Install. The Bundler version the lock's BUNDLED WITH names; a Bundler 1 lock is raised to 2.5.23, because Bundler 1 does not run on Ruby 3.2 or later. Thenbundle install without the development and test groups. A package.json beside the Gemfile brings Node 24 and npm from the official Node image and installs the JavaScript packages with the lockfile's manager (yarn install, pnpm install --frozen-lockfile,npm ci, or npm install without a lock).
Build. A Rails app with an asset pipeline (sprockets-rails, propshaft, jsbundling-rails or cssbundling-rails in the lock) runs bundle exec rails assets:precompile, with a placeholder SECRET_KEY_BASE for that step when you have not set one. Anything else has no build step. RACK_ENV and RAILS_ENV areproduction unless you set them.
Start. Rails: bundle exec rails server -b 0.0.0.0 -p ${PORT}. Any other Rack app: bundle exec rackup config.ru -o 0.0.0.0 -p ${PORT:-3000}.
Overrides. install replaces bundle install and the JavaScript install; command replaces the asset precompile;start replaces the server command.
In the image. The same ruby:<version> image the build ran on, with your source in /app and the installed gems. It is the full image, so native gems find their compiler and the libpq, libsqlite3, libyaml and libxml2 libraries.
PHP
Recognised by a composer.json or an index.php in the build root.
Version. The lowest version composer.json's require.php admits:"^8.2" builds on 8.2. The platform has 8.2, 8.3 and 8.4; a constraint below 8.2 builds on 8.2, one above 8.4 on 8.4, and the build log says so. Without a phprequirement, 8.3.
The image. The platform's PHP image, built from the officialphp:<version>-apache: the pdo_pgsql, pdo_mysql, zip, intl, bcmath, gd and opcache extensions, Composer 2, Node 24 with npm and corepack, git and unzip, and the production php.ini. An extension not on that list needs a Dockerfile.
Install. composer.json and composer.lock first, thencomposer install --no-dev --no-scripts --no-autoloader --prefer-dist, then your code, then composer dump-autoload -o --no-dev, which is where your Composer scripts run. Apath repository in composer.json moves the install after the code is copied.
Build. A package.json with a build script (Laravel's Vite, Mix) installs the JavaScript packages with the lockfile's manager and runs <manager> run build. No build script, no Node step.
Laravel (artisan beside composer.json): LOG_CHANNEL=stderr unless you set it, so the application log reaches nesly logs; storage/ and bootstrap/cache are writable by the web server.
Start. Apache with mod_php as the only process, on $PORT, logging to the container's output. The document root is public/ whenpublic/index.php exists, the project root otherwise. mod_rewrite is on.
Overrides. install replaces the Composer install and the JavaScript install, and runs after your code is copied; command replaces the JavaScript build;start replaces Apache.
In the image. The same PHP image, with your project in /var/www/html.
Node.js
Recognised by a package.json with no manifest of another language beside it (composer.json, requirements.txt, pyproject.toml, Pipfile, setup.py, setup.cfg, Cargo.toml, pom.xml, build.gradle, mix.exs, gleam.toml, deno.json, pubspec.yaml, shard.yml, Package.swift, stack.yaml, build.zig). A frontend framework that builds to static files is served as a static site (seeBuilds); this section is for a Node server.
Version. engines.node in package.json, then.nvmrc, then .node-version. A version or a range with an upper end ("20", "^20", ">=18 <21") builds on the line it starts from. An open range (">=18") builds on Node 24 when it allows 24, otherwise on the highest of 20, 22, 24 and 26 it allows. With nothing pinned, Node 24.
Package manager. The packageManager field of package.json, else the lockfile: pnpm-lock.yaml is pnpm, yarn.lock is Yarn, bun.lock or bun.lockb is Bun; npm otherwise. pnpm, and Yarn named in packageManager, run through corepack at the version you name; Bun comes from its official image.
| Manager | Install |
|---|---|
| npm | npm ci with package-lock.json, npm install without |
| pnpm | pnpm install --frozen-lockfile with pnpm-lock.yaml, pnpm install without |
| Yarn 1 | yarn install --frozen-lockfile with yarn.lock, yarn install without |
| Yarn 2 and later | yarn install --immutable with yarn.lock, yarn install --no-immutable without |
| Bun | bun install |
devDependencies are installed, even when your project sets NODE_ENV=production, and they stay in the image: a start script that runs tsx or ts-node needs them.
Build. <manager> run build when package.json has abuild script, with NODE_ENV=production unless you setNODE_ENV yourself.
Start. For a recognised server framework, its own start (thedeploy guides name them). Otherwise<manager> start when package.json has a start script (Bun: bun run start), then node <main> for package.json'smain, then node index.js. With none of them the build stops and asks for a start script.
Overrides. install, command andstart replace the install, the build script and the start.
In the image. node:<major>-slim with /appas the build left it, node_modules included, and the package manager the build used, so the running container downloads nothing. NODE_ENV=production.
Python
Recognised by requirements.txt, pyproject.toml, Pipfile, setup.py or main.py, when no language above has claimed the tree.
Version. The first of .python-version,requires-python in pyproject.toml (Poetry's python), runtime.txt and the Pipfile's python_version. A constraint builds on its lower bound (">=3.11" is 3.11). The image is tagged by minor, so the newest patch of it builds. Below 3.9 builds on 3.9. With nothing pinned, Python 3.13. CPython 3 only: PyPy needs a Dockerfile.
Install. Into a virtual environment at /opt/venv, by the first file found:
| File | Install |
|---|---|
| poetry.lock | poetry export of the main group, installed with pip |
| uv.lock | uv sync --frozen --no-dev --no-install-project |
| Pipfile.lock | pipenv requirements, installed with pip |
| requirements.txt | pip install -r requirements.txt |
| pyproject.toml alone | uv pip install . |
| setup.py | pip install . |
| Pipfile without a lock | refused: run pipenv lock and commit Pipfile.lock |
poetry, uv and pipenv are installed for the build only and are not in the running image. A lockfile installs your dependencies, not your project itself: the app runs from its source in/app.
Build. A package.json with a build script beside the Python manifest (Vite or Webpack next to Flask or Django) brings Node from the official image (the project's own Node version, Node 24 without one), installs the JavaScript packages and runs the build after the Python install. A Django app (manage.py and a django dependency) with no build command of its own runsmanage.py collectstatic --noinput when django.contrib.staticfiles is installed.
Start. When you set none and there is no Procfile, the first that fits:
- FastAPI with uvicorn declared:
uvicorn <module>:<app> --host 0.0.0.0 --port $PORT. - Flask with gunicorn declared:
gunicorn <module>:<app> --bind 0.0.0.0:$PORT. - A module that starts its own server when it is run (
app.run(…)):python <module>.py. - Streamlit:
streamlit run <script> --server.port=$PORT --server.address=0.0.0.0 --server.headless=true --server.enableCORS=false. - Django:
gunicorn <WSGI module>, from the directory that holds manage.py, withpython manage.py migrateas the release command, run once per deploy before the new version serves. A project on its own SQLite file migrates in each container before gunicorn instead. python main.py, when main.py exists. Beside a requirements.txt or pyproject.toml only when main.py starts a server itself (a.run(call): a script that runs and exits is not a web app.
With none of these the deploy is refused with a start line for your framework. In a chained start (cd backend && gunicorn app.wsgi), the last command becomes the container's main process.
Overrides. install replaces the Python install (the JavaScript install still runs); command replaces the front-end build and Django's collectstatic;start replaces the start.
In the image. python:<version>-slim with/opt/venv and your project in /app; no Node. A dependency that exists only as source on Linux and links a system library at run time (psycopg2, mysqlclient, pycurl) makes the image the full python:<version>, because the slim one lacks that library; psycopg2-binary and psycopg[binary] do not.PYTHONUNBUFFERED=1, so print output reaches the logs at once.
Rust
Recognised by a Cargo.toml, when no language above has claimed the tree.
Version. A release pinned in rust-toolchain.toml (or the olderrust-toolchain file):
[toolchain]
channel = "1.85"stable, or no file, builds on the newest stable. rust-version in Cargo.toml is a lower bound and never selects an older compiler. nightly, beta and dated channels are refused; pin a release or use a Dockerfile.
Build. cargo build --release, with --locked when Cargo.lock is committed. Every executable at the top of target/release is kept, so a workspace and a renamed [[bin]] need nothing extra.
Start. ./bin/<name> for the single binary, or thedefault-run one. A project that builds several binaries and names none must set a start command; without one the container stops and says so.
Overrides. install runs before the build. commandreplaces cargo build and must leave its executables in target/release.start replaces the binary.
In the image. debian:bookworm-slim with the CA certificates, your source tree in /app without target/ (templates and config files read at run time are there), the binaries in /app/bin, and the shared libraries they load beyond libc (libssl, libpq) in /app/lib.