Deploying and Scaling Docker Containers on Railway: Builds, Costs and Tradeoffs
- Gate
- AI & ML
- Departs
- Crew
- Joseph Williams
- Passengers
- 519
Railway can build Dockerfiles or run images from your CI pipeline. Here’s how caching, networking, storage, scaling and usage costs shape deployment.

Docker gives teams a portable application image, but it doesn't provide the service that builds, runs, exposes and monitors that image. Railway supplies that operating layer: it can build from a Dockerfile, run an image from a container registry, connect services over a private network, and scale deployments. The practical choice is where you want the image built and how much control you need over that process.
That choice has consequences for build caching, release consistency, persistent data and cost. Railway also offers Railpack, which can build many conventional applications without a Dockerfile. For teams that already treat a container image as their tested release artifact, deploying a pre-built image may fit better. Either way, the application still needs to follow container basics: bind to the right interface and port, keep durable state outside the container filesystem, and scale based on observed resource use.
Choose where the image gets built
Railway supports two Docker deployment paths. Connect a GitHub repository and Railway can build the image from its Dockerfile. Or build and test an image in an existing CI pipeline, push it to a registry, then tell Railway which image and tag to run. The first path keeps build configuration alongside application code; the second makes the image produced by CI the deployment artifact.
Build from a Dockerfile
For a repository deployment, Railway looks for a file named Dockerfile at the root of the selected source directory. If the file has another name or lives elsewhere, set RAILWAY_DOCKERFILE_PATH to its location. Railway builds the image and deploys it as a service. Environment variables and secrets can be configured on that service, and a public domain can be generated when the application needs to accept internet traffic.
Port configuration is a frequent source of trouble. Railway supplies a PORT environment variable, so the application should read that value instead of relying on a local development port. A public web process should bind to 0.0.0.0, not localhost. If the service must also accept connections through Railway's private network, an IPv6-capable bind such as :: may be needed where the framework supports it.
const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0");When the Dockerfile defines an ENTRYPOINT or CMD, Railway uses it as the service's default start command. That makes the container's startup behavior part of the deployment configuration, so check that the command launches a process meant to remain active.

Railway uses BuildKit for Docker builds and supports cache mounts. The way the Dockerfile is arranged can therefore affect how much work a later deployment repeats.
Deploy an image built elsewhere
If CI already builds and tests container images, Railway can pull and run the result without building it again. The service can use a standard Docker registry, including Docker Hub, GitHub Container Registry, GitLab Container Registry, Quay, AWS ECR Public, Google Artifact Registry and Microsoft Container Registry. Private registry credentials are available on the Pro plan.
The flow is straightforward: build and test the image in CI, push it to the registry, then provide Railway with its path and tag. Railway runs the image it pulls. This keeps the artifact tested by CI aligned with the one deployed to production and avoids maintaining separate build paths that might produce different images.
These options place build ownership in different places:
- Dockerfile deployment: Railway builds from source code and the Dockerfile.
- Pre-built image: Your CI pipeline or another build system creates the image, which Railway then runs.
Build time depends on the workload and the cache
There isn't a universal deployment time. A small image with reusable dependency layers behaves differently from a large build that downloads packages and compiles native components from scratch. Build context size, dependency installation, compilation, and whether relevant caches are available all affect the wait.
A Railway test offers a useful example, though not a service-level guarantee. For a small FastAPI application, a Dockerfile build took 11 seconds, followed by four seconds to deploy: 15 seconds in total. Deploying the same application as a pre-built image took six seconds because Railway skipped the build. Those measurements describe that application and test environment.
Railway's build infrastructure has also changed since that comparison. Builder v3 runs builds inside microVMs on a static pool of hosts, with BuildKit caches kept warm between runs. Railway reported reaching 66,000 builds per hour at peak in May 2026, up from roughly 60,000 the previous week. It later reported cutting average build time by about 10 seconds through an OCI metadata optimization. Those platform figures describe Railway's build system; your build time still depends on your own image and cache behavior.

Structure Dockerfiles to reuse work
BuildKit follows Docker's layer-caching rules: changing an earlier instruction can invalidate later layers. Put files that change less often near the start of the Dockerfile and frequently edited application code later. For a Node application, copy the dependency manifests and install packages before copying the rest of the source:
FROM node:22
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
CMD ["npm", "start"]With this ordering, a source edit doesn't automatically require reinstalling dependencies. Copying the whole repository before running npm ci can make every source change invalidate that install layer. The same principle applies to other languages: separate dependency inputs from files that change frequently.
A .dockerignore file keeps irrelevant files out of the build context. For package downloads, BuildKit cache mounts can retain package-manager caches between builds; Railway's cache mount guidance describes the configuration. Multi-stage builds can also keep compilers and other build-only dependencies out of the runtime image.
For example, a Node application can build in one stage and run from a slimmer stage:
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]The exact Dockerfile depends on the application. The aim is to preserve reusable dependency layers and leave build-time tools out of the final image when the running service doesn't need them. Cache hits aren't guaranteed: changing dependencies, an earlier layer or the base image can trigger more work, as can losing the relevant cache. If a build remains expensive and CI already produces a tested image, deploying that image is another path.
Containers need the right network and storage model
Railway runs most applications that behave like ordinary containers, but a container isn't a virtual machine with unrestricted control of its host. A conventional web process and environment variables fit the model. Durable files need a Volume or external storage; related containers should be separate Railway services. Docker Compose projects can be adapted, with some differences, and workloads that depend on host-level or privileged behavior may need changes.
Public and private connections
For public web traffic, make the application listen on 0.0.0.0 and use the supplied PORT. A process bound only to localhost may look healthy from inside its container but remain unreachable through Railway's network. If private-network traffic is required, check that the server supports IPv6 binding as well.
Railway handles the public networking layer after a domain is generated for the service. Services in a project can communicate through Railway's private network, which is useful for connections to a database or another internal service. Inside a container, however, localhost refers to that container itself. A web service connecting to a separate database service needs the database's private address, not its own loopback address.
Keep durable data out of the container filesystem
Files written into a container should not be treated as persistent application data across deployments. Choose a Railway Volume when the application needs a persistent filesystem, or use a database or object storage when those better fit the data.
Volumes have a direct effect on scaling: Railway services with an attached Volume cannot use replicas. A service that needs horizontal scaling should put shared state outside its individual containers. That usually means a database for application state, an object store for uploads, and a shared store for sessions.
Scale from measurements, and account for state
Railway supports vertical scaling through resource limits and vertical autoscaling up to the limits available to a service. It also supports horizontal replicas, including replicas in multiple regions. The right move depends on which resource constrains the application and whether its state can be shared between instances.
Vertical scaling
Start with the service's metrics before raising limits simply because traffic has grown. Railway's Metrics tab shows CPU, memory, disk and inbound and outbound network traffic. The shape of that usage matters: an application might need substantial memory while using little CPU, whereas a worker could spend most of its time on CPU-heavy tasks.
Adjust resource limits when the metrics show the service needs more capacity. Railway also offers replica limits, which can cap the resources available to an individual replica and constrain unexpected usage increases.

Horizontal and multi-region scaling
When a single instance can't handle the workload, increase the number of replicas. Railway creates multiple instances of the service deployment, and those instances can be spread across regions. This requires a stateless service: attached Volumes prevent replica use, so sessions, uploads and other shared data must live in a database, object store or another external system.

Running replicas in several regions can bring compute closer to users in different parts of the world. But distributing the service doesn't automatically distribute its dependencies. If the database remains in one region, requests may still cross regions to reach it. Consider where stateful services run before expecting multi-region compute to reduce latency.

Understand usage based costs
Railway doesn't charge a separate rate for Docker hosting. Billing reflects resource consumption, including CPU, RAM, network egress and Volume storage. The rates in the source are:
| Resource | Price |
|---|---|
| RAM | $10 per GB per month |
| CPU | $20 per vCPU per month |
| Network egress | $0.05 per GB |
| Volume storage | $0.15 per GB per month |
There is also a base subscription fee that goes toward resource usage. The Hobby plan is $5 per month and the Pro plan is $20 per month, according to the plan details.
For a continuously running small API averaging 512 MB of RAM, 0.05 vCPU and 10 GB of monthly egress, the example calculation is $5.00 for RAM, $1.00 for CPU and $0.50 for egress. That's $6.50 in total. On the Hobby plan, the $5 subscription covers part of that usage, leaving an estimated additional resource charge of about $1.50 for this example.
Usage based billing means the service isn't tied to a fixed CPU allocation. An application averaging 0.05 vCPU is charged for that use instead of reserving a larger allocation that sits idle. The tradeoff is that a high baseline memory footprint continues to cost money even when CPU use is low; replicas add to the application's resource consumption.
For services with long idle periods, Railway's Serverless mode can stop an inactive service and reduce compute use while it sleeps. Usage limits and resource limits can also put a ceiling on consumption.
Compose projects, web services and workers
A multi-container application can be represented as several Railway services. Railway's Docker Compose support can help translate an existing Compose setup into a project, but Railway doesn't use the Compose file as its production orchestrator. In that translation, a Compose service with build: maps to a Railway service built from a Dockerfile; one with image: can run the existing image. Compose volumes: map to Railway Volumes, and Railway supplies private networking between services. See the Compose guide.
A background worker is simply another service running a container. Deploy it from a Dockerfile or a pre-built image, and don't generate a public domain if it doesn't receive public HTTP requests. Its start command should launch the worker process rather than a web server.
The web service and worker can share one image while using different start commands, or use separate Dockerfiles if their runtime needs differ. They can connect to the same database, queue or internal service through Railway's private network. Since they do different work, scale each service according to its own CPU, memory and queue-processing needs.
When Railpack, a Dockerfile or an image fits
Docker isn't required for every application on Railway. Railpack detects a supported stack, works out how to install dependencies and build the application, then packages the result as a container image. Supported stacks include Node, Python, Go, PHP, HTML/Staticfile, Java, Ruby, Deno, Rust, Elixir and Shell scripts. Railway also has deployment guides for frameworks such as Next.js, Express, FastAPI, Django, Rails and Spring Boot.
Railpack's settings can be customized through environment variables or a configuration file, including language versions, build and start commands, packages and cache directories. It suits applications that match the detected build and runtime setup and don't need a custom image. For example, Railpack can detect Node from package.json and Python from requirements.txt or pyproject.toml. Its Java provider recognizes Gradle projects through a gradlew wrapper and Maven projects through a pom.* file. This route means fewer build files to maintain.
Use a Dockerfile when the image itself needs explicit control: perhaps the app needs system packages, a particular base image, native dependency compilation, a carefully selected runtime file set or a multi-stage build. The Dockerfile also keeps those choices visible in the repository.
Choose a pre-built image when CI already produces the artifact you want to release. Railway pulls and runs it without rebuilding, which lets the image tested in CI remain the image used in production. The distinction is mostly about control and ownership: Railpack suits conventional stacks, Dockerfiles suit custom image requirements, and pre-built images suit teams with an established image pipeline.
Troubleshoot from the logs
When a deployment fails, inspect the build and deployment logs before changing the Dockerfile. Railway exposes both in the dashboard and through the CLI. Use railway logs for deployment logs, railway logs --build for build output, or railway logs --lines 100 to fetch a fixed number of recent lines.

The container builds, then exits
An exit code of 0 means the process finished successfully, but it may have finished instead of staying alive. Check the startup command first. For Docker deployments, the Dockerfile's ENTRYPOINT and CMD provide the default command. If it starts the wrong process, the application crashes, or the command completes immediately, the container exits. Deployment logs show which process ran and its result.
Also check the configured environment variables. A variable missing from the production service can cause a locally working application to fail during startup.
The service runs, but can't be reached
Check the bind address and port: public services should listen on 0.0.0.0 and use Railway's PORT. For private-network traffic, verify IPv6 binding support. If those settings are correct, confirm that the service has a generated public domain when you are trying to reach it from the internet.

Builds take too long
Use the build logs to identify the slow step. A large build context, a missing .dockerignore, lengthy dependency installation, native compilation or an invalidated cache can all extend a build. If package installation repeats after source edits, reorder the COPY instructions so dependency manifests are copied and installed before application source. If the final image is large, review the base image and whether a multi-stage build can remove tools used only during compilation.
Files disappear after a redeploy
Files written to the container filesystem aren't durable application storage. Move them to a Railway Volume, database or object store. If the service needs replicas, account for the Volume restriction: a service with an attached Volume can't run replicas. Put shared state in external storage to support horizontal scaling.
The image works locally but fails on Railway
Compare the production environment with the local one. Check environment variables, file permissions, CPU architecture, dependencies on localhost, and files that exist on your machine but never made it into the image.
Architecture mismatches can arise when images are built on Apple Silicon, where Docker defaults to arm64. Railway's private registry guide calls out building with --platform linux/amd64 when needed. For a multi-container app, check that each service uses the correct private address for its dependencies: within a container, localhost points back to that container. If the application starts but requests fail, HTTP logs can help distinguish an application error from a network issue or delay from an upstream service.
Make the deployment choice fit the operating model
Railway can build a container from source, run an image created by CI, or package a supported application through Railpack. The useful choice depends on how much control the image needs and where the team already builds and tests it. Once the service runs, the operating decisions are familiar: use the supplied port, bind to an address the network can reach, store durable data outside the container, and scale from resource metrics. For horizontally scaled services, make state available outside individual replicas; for multi-region deployments, check that their dependencies are placed where the application needs them.