The Best Declarative Deploy Configs in 2026
What Are the Best Declarative Deploy Configs in 2026?
Railway is the best fit for teams that want to define multi-service application infrastructure in code and review changes before applying them. Render is a strong choice for a complete stack described in static YAML. Fly.io gives you more direct control over compute and networking, while Vercel and Netlify suit applications built around frontend deployments and functions.
The right choice for your team depends on what you need the configuration to describe. All platforms compared here let you define simpler deployment tasks, such as configuring a build command, but only some support more complex requirements like reproducing an application’s services, databases, networking, and environment-specific settings.
How do Dockerfiles relate to infrastructure as code?
Many infrastructure quickstarts will guide you through deployment in their UI. Deploying an application through a dashboard is convenient when you’re getting started, but remembering which settings changed, who changed them, and whether staging matches production gets effectively impossible as the application scales.
Enshrining deployment configuration in code (and governing that code in Git) gives those changes the same review process and observability as application code. A pull request can show that an API is getting another replica, a worker’s start command is changing, or a new database is being added. Critically, your team can review the change before the deployment actually happens.
Deployment configuration and infrastructure as code options exist on a spectrum, from the lowest application-level details to the highest infrastructure platform specifications.
A Dockerfile describes how to build a container image and provides defaults for running it. General-purpose infrastructure tools such as Terraform and CloudFormation can describe broad swaths of physical and virtual infrastructure, including networks, compute resources, databases, and access policies.
Between them are the configurations most application teams use directly: instructions that tell a platform how to build and deploy their services, connect them to databases and other adjacent infrastructure, and manage their environments.
This article focuses on that middle layer: the PaaS deployment configuration layer.
What can Dockerfiles and Docker Compose describe?
When should you use a Dockerfile?
A Dockerfile defines the application image: its base image, dependencies, build steps, and default runtime command.
That image can generally run on different platforms without rebuilding the application’s runtime setup from scratch, but you still need to configure the infrastructure around it, including database access, public routing, persistent storage, and deployment behavior.
A Dockerfile is useful when your application needs custom system packages, a custom build process, to run on a specific base OS, or you need portability across hosting providers. For platforms that include automatic builders, maintaining your own Dockerfile may be unnecessary.
When should you use docker-compose.yml?
Docker Compose describes a multi-container application, including services, networks, volumes, and configuration passed into containers. It’s useful for running an API alongside Postgres and a worker in local development, or deploying a small application on infrastructure you operate yourself. See the Compose application model.
Compose can describe the containers your application needs, but you still need to own the host and the surrounding production setup.
What does a deployment platform add?
A platform as a service handles much of the surrounding production setup for you. Depending on the provider, its configuration can cover:
- Application services, workers, and scheduled jobs.
- Databases and persistent storage.
- Build and deployment commands.
- Variables and service connections.
- Health checks and scaling.
- Domains and networking.
- Staging, production, and preview environments.
With consistency and infrastructure reproducibility as the main goal, the key question when evaluating PaaS deployment effectiveness becomes how much of your entire setup you can reproduce from the configuration, and how much still depends on dashboard settings or separate provisioning steps.
Which declarative deploy configs should you compare?
What are the main declarative deployment options?
| Platform | Config | Main abstraction | Best fit |
|---|---|---|---|
| Railway | .railway/railway.ts; Python and Go authoring in beta | Project/environment resources | Multi-service applications with reusable configuration |
| Render | render.yaml | Blueprint of interconnected resources | A stack described in static YAML |
| Fly.io | fly.toml | App runtime and Machine configuration | Applications needing explicit runtime and network control |
| Vercel | vercel.json, vercel.toml, or vercel.ts | Build, routing, and function configuration | Framework-driven web applications |
| Netlify | netlify.toml | Build, routing, functions, and deploy contexts | Sites and web applications with preview workflows |
What actually differs between them?
The main options differ in what they treat as the primary unit of configuration and how much of the application lifecycle they manage.
Railway and Render let you describe connected application infrastructure, including services, databases, and variables. Railway uses programmatic definitions evaluated against a selected environment, with a plan/apply workflow for reviewing changes. Render uses a YAML Blueprint that can declare resources across projects and environments. Both are useful when your goal is to reproduce a full application stack from version-controlled configuration.
Fly.io centers its configuration on an individual app and the Machines running its processes. Its configuration exposes runtime details such as ports, CPU, memory, health checks, and volume mounts. For a stack with separately deployed frontend and API apps, you generally maintain separate app configurations, with database provisioning handled through an additional workflow.
Vercel and Netlify focus their deployment files on builds, routing, functions, and related application settings. Their configurations are useful when your application fits those deployment models. If it also depends on independently provisioned databases or other services, check how those resources are created and connected outside the deployment file.
How does Railway define project infrastructure in TypeScript, Python, and Go?
Railway Infrastructure as Code replaces the older service-level railway.json and railway.toml model. TypeScript authoring is generally available; Python and Go are in beta.
The file describes resources within a selected project environment. Using a programming language lets teams share definitions and express environment-specific differences without maintaining separate copies of the whole configuration.
The workflow includes a review step:
railway config plan
railway config apply
The plan command shows and validates the proposed changes. apply previews changes and asks for confirmation before applying them. Existing projects can be imported with railway config pull. This makes Railway a strong fit when application infrastructure needs to grow alongside the code. Render or Fly remain good alternatives if your team prefers a static configuration file that can be read without evaluating a program.
How does Railpack fit into deployment configuration?
Railpack is Railway’s automatic application builder. It detects supported languages and frameworks, installs dependencies, and produces a container image using detected or configured build and start commands.
Railpack builds the application, while Railway IaC defines the project resources and their settings. You can use both without writing a Dockerfile, but you might want to use a Dockerfile when you need more control over the image or a build process that Railpack doesn’t support.
What can Render describe in a YAML Blueprint?
Render’s render.yaml can describe web services, workers, cron jobs, Postgres databases, and environment groups.
For example, an API can reference a database connection string directly:
services:
- type: web
name: api
runtime: python
buildCommand: pip install -r requirements.txt
startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: postgres
property: connectionString
databases:
- name: postgres
Render is a strong choice when you want explicit resource definitions in YAML. Its Blueprints also support projects and environments, so the model extends beyond a flat list of services.
Static configuration becomes less convenient when many environments need similar definitions with small differences; keeping repeated sections aligned takes care.
How much runtime control does Fly.io give you?
Fly’s fly.toml exposes app runtime settings such as processes, ports, health checks, regions, and Machine resources.
app = "example-api"
primary_region = "ord"
[http_service]
internal_port = 8080
force_https = true
[[http_service.checks]]
path = "/health"
interval = "30s"
timeout = "5s"
grace_period = "10s"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
This is useful when you want to make explicit decisions about how the application runs. A fly.toml is centered on an app, however, so it doesn’t serve the same purpose as a Blueprint describing an API and its separately provisioned database or other adjacent infrastructure. Fly is a good fit when Machine placement and network behavior are part of your application design, and you don’t mind wiring them together manually.
When do Vercel and Netlify fit your application?
What does Vercel’s project configuration cover?
Vercel’s project configuration defines builds, routing, headers, functions, and scheduled tasks. In addition to vercel.json, it supports TOML and programmatic TypeScript configuration.
This flexibility is useful for framework-based applications, but lacks the capability to describe broader infrastructure requirements.
What does Netlify’s deployment configuration cover?
Netlify’s netlify.toml covers builds, functions, redirects, headers, and deploy contexts. Context overrides let you change settings for production, branch deployments, and deploy previews.
Both platforms are good choices when their web deployment model fits your application. An application that also needs independently operated services or databases should account for how those resources are provisioned and connected.
How do declarative deploy configs compare in practice?
How do you configure a simple full-stack app for staging and production?
Consider an application with a Node frontend, Python API, and Postgres database, deployed to staging and production.
To keep the comparison consistent, assume:
frontend/contains a Node application withbuildandstartscripts.- The frontend listens on
0.0.0.0:$PORTand proxies browser requests to the API usingAPI_HOSTandAPI_PORT. backend/containsrequirements.txtand a FastAPI application atmain:app, with a/healthendpoint.- Each environment has its own database.
- Both environments deploy from
main. This example doesn't demonstrate release promotion.
The frontend proxy lets the browser use one public origin while the API stays on the private network. The application must implement that proxy while the deployment configuration supplies its destination.
How does Railway configure this full-stack app?
This file describes the services and database for the environment selected in the CLI. The same definition is applied separately to staging and production, with a different frontend domain for each.
.railway/railway.ts
import {
defineRailway,
github,
postgres,
project,
service,
} from "railway/iac";
export default defineRailway((ctx) => {
if (!["production", "staging"].includes(ctx.environment)) {
throw new Error("Select the production or staging environment.");
}
const production = ctx.environment === "production";
const db = postgres("postgres");
const api = service("api", {
source: github("example/full-stack-app", {
branch: "main",
rootDirectory: "backend",
}),
start: "uvicorn main:app --host 0.0.0.0 --port 8080",
healthcheck: "/health",
replicas: 1,
env: {
PORT: "8080",
DATABASE_URL: db.env.DATABASE_URL,
},
});
const frontend = service("frontend", {
source: github("example/full-stack-app", {
branch: "main",
rootDirectory: "frontend",
}),
build: "npm run build",
start: "npm start",
replicas: 1,
domains: [
{
domain: production
? "app.example.com"
: "staging.example.com",
port: 8080,
},
],
env: {
PORT: "8080",
API_HOST: api.env.RAILWAY_PRIVATE_DOMAIN,
API_PORT: "8080",
},
});
return project("full-stack-app", {
resources: [db, api, frontend],
});
});
The Railway authoring API connects the API to Postgres and the frontend to the API. The target environments must already exist, and custom domains still require DNS setup. Repository access, application code, and database schema initialization remain prerequisites.
How does Render configure this full-stack app?
Render can also describe both environments and their connected resources in one file. Here, the service definitions are repeated explicitly for staging and production.
render.yaml
# Replace the repository URL with your own.
#
# Outside this file:
# - Authorize Render to access the repository and create the Blueprint.
# - Configure DNS for the custom frontend domains.
# - Supply any third-party secrets your application needs.
# - Run migrations or restore data to initialize each database.
#
# This Blueprint DOES declare both environments, their services,
# their databases, and the connections between those resources.
projects:
- name: full-stack-app
environments:
- name: production
services:
- type: pserv
name: api-production
runtime: python
repo: <https://github.com/example/full-stack-app>
branch: main
rootDir: backend
region: oregon
plan: 0.5c-512mb
buildCommand: pip install -r requirements.txt
startCommand: uvicorn main:app --host 0.0.0.0 --port 8080
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: postgres-production
property: connectionString
- type: web
name: frontend-production
runtime: node
repo: <https://github.com/example/full-stack-app>
branch: main
rootDir: frontend
region: oregon
plan: 0.5c-512mb
buildCommand: npm ci && npm run build
startCommand: npm start
domains:
- app.example.com
envVars:
- key: API_HOST
fromService:
name: api-production
type: pserv
property: host
- key: API_PORT
value: "8080"
databases:
- name: postgres-production
region: oregon
plan: 0.1c-256mb
ipAllowList: []
- name: staging
services:
- type: pserv
name: api-staging
runtime: python
repo: <https://github.com/example/full-stack-app>
branch: main
rootDir: backend
region: oregon
plan: 0.5c-512mb
buildCommand: pip install -r requirements.txt
startCommand: uvicorn main:app --host 0.0.0.0 --port 8080
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: postgres-staging
property: connectionString
- type: web
name: frontend-staging
runtime: node
repo: <https://github.com/example/full-stack-app>
branch: main
rootDir: frontend
region: oregon
plan: 0.5c-512mb
buildCommand: npm ci && npm run build
startCommand: npm start
domains:
- staging.example.com
envVars:
- key: API_HOST
fromService:
name: api-staging
type: pserv
property: host
- key: API_PORT
value: "8080"
databases:
- name: postgres-staging
region: oregon
plan: 0.1c-256mb
ipAllowList: []
Render documents resource definitions and compute plans in its Blueprint reference, and connection references in its cross-service wiring guide. Custom domains still require DNS configuration.
Both Railway and Render capture the application’s resource relationships. The difference here is how they express reuse: Railway evaluates one definition against the selected environment, while this Blueprint lists each environment’s resources explicitly.
How does Fly.io configure this full-stack app?
For this architecture, Fly uses separate frontend and API apps in each environment. The following four files describe their runtime configuration. Postgres provisioning and attachment remain separate.
The app names are placeholders and must be available in your Fly organization. Deploy each file from its service directory so the Dockerfile and build context resolve correctly. Fly’s private networking uses IPv6, so the API listens on :: for connections through its .internal hostname.
backend/fly.production.toml
# Outside this file:
# - Create the api-production app in the intended Fly organization.
# - Provide backend/Dockerfile with Python dependencies installed.
# - Provision production Managed Postgres and attach it to this app,
# supplying DATABASE_URL as a secret.
# - Initialize the database schema and data.
# - Deploy this app separately from the frontend.
#
# No public HTTP service is declared. The frontend connects privately.
# The frontend and API must share the appropriate private network.
app = "api-production"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[processes]
api = "uvicorn main:app --host :: --port 8080"
[checks.health]
type = "http"
port = 8080
method = "GET"
path = "/health"
interval = "30s"
timeout = "5s"
grace_period = "10s"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
frontend/fly.production.toml
# Outside this file:
# - Create the frontend-production app.
# - Provide frontend/Dockerfile that installs dependencies,
# builds the frontend, and makes npm start available.
# - Configure custom-domain certificates and DNS if desired.
# - Deploy and scale this app separately from the API.
#
# API_HOST points at an existing app; it does not create that app.
app = "frontend-production"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "8080"
API_HOST = "api-production.internal"
API_PORT = "8080"
[processes]
web = "npm start"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "off"
auto_start_machines = true
[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
backend/fly.staging.toml
# Outside this file:
# - Create the api-staging app.
# - Reuse backend/Dockerfile.
# - Provision a separate staging Managed Postgres database and
# attach it to this app, supplying DATABASE_URL as a secret.
# - Initialize staging schema and test data.
# - Deploy this app independently.
app = "api-staging"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[processes]
api = "uvicorn main:app --host :: --port 8080"
[checks.health]
type = "http"
port = 8080
method = "GET"
path = "/health"
interval = "30s"
timeout = "5s"
grace_period = "10s"
[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
frontend/fly.staging.toml
# Outside this file:
# - Create the frontend-staging app.
# - Reuse frontend/Dockerfile.
# - Configure any custom staging domain separately.
# - Deploy and scale this app independently.
#
# Separate names distinguish the environments, but do not by
# themselves establish private-network isolation between them.
app = "frontend-staging"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "8080"
API_HOST = "api-staging.internal"
API_PORT = "8080"
[processes]
web = "npm start"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "off"
auto_start_machines = true
[[vm]]
cpu_kind = "shared"
cpus = 1
memory = "512mb"
Fly’s app configuration reference covers runtime settings; Managed Postgres has its own provisioning workflow. These files do not declare database resources or a coordinated release across apps.
How do you migrate from render.yaml to Railway?
Begin with the application’s resource relationships, then translate individual settings.
| Render definition | Railway equivalent | What to review |
|---|---|---|
| Web service | service() | Source, build, start, and public access |
| Background worker | service() with a worker command | Runtime behavior and restart needs |
| Postgres resource | postgres() | Database configuration and data transfer |
envVars | Service env | Literal values, secrets, and references |
fromDatabase | Database variable reference | Connection format and application driver |
healthCheckPath | healthcheck | Endpoint behavior and deployment checks |
| Preview environment | Railway PR Environment | Provisioning rules and environment values |
Relevant Railway fields are documented in the IaC reference. Render’s source fields are documented in the Blueprint reference.
For the example application, declare Postgres, connect the API through DATABASE_URL, and add the frontend. Review the proposed infrastructure before applying it. Choose resources based on the application’s needs, then check behavior under representative traffic.
How do you keep infrastructure reproducible across environments?
Staging and production should share the same application structure as much as possible. Keep deliberate differences explicit, such as replica counts, domains, external API credentials, and deployment branches.
Railway environments provide isolated service configurations, and PR Environments add temporary infrastructure for reviewing changes.
Secrets need their own setup. A configuration that references an existing secret still depends on that secret being present in the target environment. See Railway variables and Render environment variables and secrets for the respective setup workflows.
Reproducing infrastructure also doesn’t reproduce its data. Decide whether a new environment needs an empty database, migrations, and fixtures, or a controlled copy of existing data.
When do you need tools beyond your deployment platform’s config?
Some applications need infrastructure that extends beyond what their deployment platform manages. That might include cloud networking, specialized compute, or resources spread across several providers.
These requirements may justify considering a broader toolset, such as Terraform for infrastructure and objects across providers, or Kubernetes for granular control of container orchestration.
When should you use Terraform?
Terraform manages infrastructure through provider-defined resources, with reusable modules, state, and a plan/apply workflow. It’s useful when your infrastructure spans several systems. For example, an application might run on a PaaS while depending on cloud storage, DNS, and resources in another provider. That wider scope is powerful but adds significant work, such as provider configuration, state management, and ownership of the resources you provision.
When should you use Kubernetes?
Kubernetes manages containerized workloads through resources such as Deployments, Services, Jobs, and storage definitions. Helm is used to package and template Kubernetes resources. This approach fits teams that need to control container orchestration directly or already operate a shared cluster. It also requires decisions about cluster operations, networking, upgrades, and observability that a PaaS would otherwise handle.
When should you use AWS CloudFormation or SAM?
CloudFormation describes AWS infrastructure directly, including compute, networking, databases, and IAM resources. AWS SAM builds on CloudFormation with a model for serverless applications. These tools suit applications whose architecture depends on AWS services and permissions. They represent a broader commitment than choosing where to run an API and its database.
Which level should you use?
| Need | Tool / layer |
|---|---|
| Define how one container image is built | Dockerfile |
| Run a small multi-container application | Docker Compose |
| Manage application infrastructure through a platform | Railway / Render / Fly.io |
| Deploy a framework-driven web application | Vercel / Netlify |
| Manage infrastructure across providers | Terraform |
| Control container orchestration | Kubernetes |
| Manage AWS resources directly | CloudFormation / SAM |
Your team should choose the layer that covers the infrastructure you need and not more. Additionally, you can and should combine layers: a Dockerfile can build an image deployed by a PaaS, while Terraform manages other infrastructure the application depends on.
Frequently asked questions
What’s the difference between a Dockerfile and a declarative deploy config?
A Dockerfile describes how to build an application image and provides its default runtime command. A declarative deploy config describes how a platform should deploy that image or application code, with coverage that varies by provider. For a full-stack application, check whether it also describes the database, service connections, networking, and environment-specific settings you need to recreate the deployment.
Can you use the same Railway configuration for staging and production?
Yes. The example above uses one definition and evaluates it against the environment selected in the CLI, changing the frontend domain for staging and production. The target environments must already exist, and each needs its own secrets and database initialization. Sharing the definition keeps the application structure consistent while leaving deliberate differences explicit.
Do you need a Dockerfile to deploy on Railway?
You don’t need one when Railpack supports your application’s build process. Railpack detects supported languages and frameworks and builds the container image. A Dockerfile is useful when you need custom system packages, a specific base image, or more control over the build. In either case, Railway’s infrastructure configuration describes the resources around the application.
Does deployment configuration reproduce database data and secrets?
No. Declaring a database reproduces the resource, but you still need to initialize its schema and decide what data belongs in the new environment. A reference to an existing secret also depends on that secret being available in the target environment. Review Railway’s variable setup alongside your migrations and fixtures before treating a configuration as a complete way to recreate the application.
When should you choose Render or Fly.io instead of Railway?
Render is a strong fit when your team wants connected services and databases described in static YAML. Fly.io suits applications where Machine placement, runtime settings, and networking need explicit control, and your team is comfortable provisioning databases and coordinating separate apps. Railway fits teams that want reusable application infrastructure definitions and a plan/apply review workflow.
Which declarative deploy config should you choose?
For a full-stack application, Railway and Render provide useful ways to describe connected resources in one configuration. Railway suits teams that want reusable code, a broad array of infrastructure control, and a plan/apply workflow. Render suits teams that prefer a simple YAML file. Fly.io is worth choosing when runtime, region, and compute control matter. Vercel and Netlify are strong fits when the application is centered on their web deployment workflows.
Before choosing, consider recreating a small version of your application from its configuration artifact alone. The volume and complexity of any remaining configuration steps will tell you a lot about how consistent, reproducible, and dependable your declarative deployment method really is.