Buildpacks or Dockerfile on Scalingo? How to Choose (and When to Switch)
Scalingo auto-detects your language from marker files, but when should you override with .scalingo-buildpacks or a Dockerfile? A practical decision guide with examples and verification commands.
16 Jul 2025, 05:17 UTC

You push a repo to Scalingo, it detects package.json, picks the Node.js buildpack, and deploys. That works great — until the day you need a pinned runtime version, a system library the buildpack doesn't ship, or a language setup the auto-detector doesn't recognize. At that point you face a real decision: stay on buildpacks, add a .scalingo-buildpacks override, or take full control with a Dockerfile. This post walks through how detection actually works, what each override buys you, and the maintenance cost that comes with it.
What auto-detection actually does
When you run git push scalingo main, Scalingo scans your application root for language marker files: package.json for Node.js, requirements.txt for Python, Gemfile for Ruby, and so on. Based on what it finds, it selects a buildpack from its supported set (Node.js, Python, Ruby, Java, Go, PHP), caches your dependencies, compiles the runtime, and produces a deployable container.
The important detail: the default buildpacks don't necessarily pin your exact runtime version. The buildpack resolves a version from your project files where it can (for example, an engines field in package.json), but if you leave things implicit, you may get upgraded automatically on a later deploy. That's convenient until it isn't.
To see what was actually selected after a push, run this from your machine with the Scalingo CLI installed and authenticated:
scalingo --app my-app info
scalingo --app my-app buildpacks:infoCheck the Stack/runtime fields against what you expect. If the version drifted from your local development setup, that's your cue to pin it explicitly in your project files rather than relying on detection defaults.
The middle option: .scalingo-buildpacks
Before reaching for Docker, know that there's a lighter override. A .scalingo-buildpacks file in your repo root lets you declare which buildpack(s) to use instead of the auto-detected one. This is useful for multi-buildpack setups — say, a Node.js frontend build that produces assets consumed by a Python app — or for pinning to a specific buildpack source.
You keep the managed-runtime benefits (Scalingo maintains the buildpack, dependency caching still works, base image patching is handled for you) while controlling the composition. For most "the default didn't quite fit" cases, this is enough.
When a Dockerfile is the right call
A Dockerfile in the repo root takes precedence over buildpack detection entirely. You should reach for it when:
- You need a runtime or version outside the supported buildpack set.
- You need system packages installed via
apt-getthat no buildpack provides. - You want a byte-identical environment between local development, CI, and production.
- Your build process is unusual enough that fighting a buildpack's assumptions costs more than maintaining a Dockerfile.
A minimal example for a Go service with a pinned toolchain:
FROM golang:1.22-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/server ./cmd/server
FROM debian:bookworm-slim
COPY --from=build /bin/server /bin/server
EXPOSE 3000
CMD ["/bin/server"]Key platform expectations to respect: your container must listen on the port provided via the PORT environment variable, and it should read configuration (database URLs, etc.) from the environment rather than baked-in files. Addon provisioning still works the same way — Scalingo injects variables like DATABASE_URL and REDIS_URL into the container environment and handles credential rotation transparently, so never hardcode those values into the image.
After deploying, verify the process is actually up and env injection worked:
scalingo --app my-app ps
scalingo --app my-app addons:list
scalingo --app my-app env:get DATABASE_URLps should show your web process running; the env:get output should be a correctly formatted connection URL, not empty. If the deploy fails, scalingo --app my-app logs is your first stop — Dockerfile builds surface their errors there.
The trade-off nobody mentions upfront
Switching to a Dockerfile transfers a maintenance burden to you that buildpack users never think about: base image security patches. Scalingo keeps the buildpack stack's underlying images updated; it does not rebuild or patch your custom image. A debian:bookworm-slim base you pinned eighteen months ago is exactly as patched as it was eighteen months ago, and stale images can eventually cause deployment failures as platform expectations evolve.
The practical mitigation is boring but effective: rebuild and redeploy on a schedule (monthly is a reasonable default), and use tag-based base images you consciously bump rather than latest, so upgrades are deliberate. If your team won't actually do that, that's a strong argument for staying on buildpacks even when the fit is imperfect.
There's also a rollback asymmetry worth knowing. With buildpacks, rolling back to a previous deployment restores a known-good build artifact. With Docker, the image is whatever your Dockerfile produced at push time — reverting means reverting the Dockerfile and rebuilding, so keep your base image pins in version control where they can be reverted cleanly.
A decision rule you can apply today
Default to auto-detected buildpacks. Pin your runtime version in your project files so detection stays deterministic. If you need buildpack composition or a non-default buildpack, use .scalingo-buildpacks. Only move to a Dockerfile when you have a requirement the buildpack system genuinely cannot meet — and when you do, put a recurring reminder in your team's calendar to rebuild the image. The flexibility is real, but so is the patching responsibility that comes with it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.