Fly.io runs your application as one or more Fly Machines, which are lightweight VMs started from a container image. A public fly.dev hostname is routed through Fly's proxy to an internal port on the Machine.
For an MCP server, that means building a Docker image that listens on 0.0.0.0:8080, setting internal_port = 8080 in fly.toml, and adding upstream credentials with fly secrets.
Prerequisites
- A generated bundle for your chosen runtime.
- The flyctl CLI and a Fly.io account.
- Docker for local image testing, if you want to verify before deploying.
Fly.io has no free tier. New accounts get a trial of up to two hours of Machine runtime or seven days, whichever comes first; after that, a credit card is required. Billing is per second while a Machine is running, plus a small charge for the root filesystem of stopped Machines.
fly auth login
fly version
1. Bind the server to port 8080
Fly routes to the internal port configured in fly.toml; it does not use a dynamically injected port like some PaaS platforms. Standardize on 8080 so the image, config, and health checks agree.
For TypeScript, update the generated entry point to listen on all interfaces and default to port 8080.
// Before
port: { type: 'string', default: '3000' },
serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => {
// After
port: { type: 'string', default: process.env.PORT ?? '8080' },
serve({ fetch: app.fetch, port, hostname: '0.0.0.0' }, () => {
Make npm start use the HTTP transport.
"scripts": {
"build": "tsc",
"start": "node dist/server.js --http",
"dev": "tsx watch src/server.ts"
}
Python needs no source edits; the Dockerfile below passes --host 0.0.0.0 --port 8080. PHP and Java reuse the Dockerfiles from the Cloud Run guide, which default to 8080.
2. Add a Dockerfile
Fly builds a container image from a Dockerfile in the bundle root. The TypeScript image compiles the project, installs production dependencies, and starts the HTTP transport.
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm install --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 8080
CMD ["node", "dist/server.js", "--http"]
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "server.py", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8080"]
Add a .dockerignore so local build artifacts are not copied into the image.
.git
node_modules
dist
venv
__pycache__
3. Add fly.toml
This configuration publishes the app over HTTPS, routes to port 8080, and lets idle Machines stop. The extended idle timeout is useful for streamed MCP responses.
app = "my-mcp-server"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[http_service.concurrency]
type = "connections"
soft_limit = 20
hard_limit = 25
[http_service.http_options]
idle_timeout = 600
Change primary_region to the Fly region closest to your upstream API. Use fly platform regions to list the available codes.
Two Machines by default: the first deployment creates two Machines for redundancy. With min_machines_running = 0, both can stop when idle. Run fly scale count 1 if you want a single Machine, or set min_machines_running = 1 to keep one warm.
4. Launch and deploy
Run the launch flow without deploying so Fly creates the app and confirms the configuration. Decline the offer to add a Postgres database and keep the existing Dockerfile and fly.toml.
fly launch --no-deploy
fly deploy
fly launch asks for an app name and region. If it generates a different fly.toml, keep the http_service block above, especially internal_port = 8080. The deploy builds the image on Fly's remote builder, creates the Machines, and attaches a public hostname.
Check the release status and follow the logs:
fly status
fly logs
5. Add upstream credentials
The bundle README lists the variable names produced from your API security schemes. Store them as Fly secrets so they are not committed to the repository or written into fly.toml.
fly secrets set MCP_AUTH_API_TOKEN=your-token
Setting a secret triggers a new release. Fly secrets are available at runtime but not during the image build, so keep any build-time configuration in the Dockerfile or in [build.args] instead of secrets.
6. Verify the endpoint
The app is reachable at https://<app-name>.fly.dev. The MCP endpoint is that hostname plus /mcp:
https://my-mcp-server.fly.dev/mcp
Connect an MCP client that supports Streamable HTTP and call a read-only tool. fly logs shows startup errors and missing credentials; fly status shows whether the Machines are started, stopped, or suspended.
PHP and Java with Docker
Copy the PHP or Java Dockerfile from the Cloud Run guide into the bundle root and reference it in fly.toml. Those images already bind to 0.0.0.0:${PORT:-8080}, and Fly does not set PORT, so they listen on 8080 as required.
Start commands: Fly uses the Dockerfile CMD by default. If you override it, keep the process bound to 0.0.0.0:8080 and make sure the override uses shell form if you need environment expansion.
Optional: health checks
Fly does not require an HTTP health check to deploy. If you add one, it must return a 2xx status; do not point it at /mcp. Add a small route to the server first:
app.get('/health', (c) => c.text('ok'));
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/health"
Cost and scale notes
- No free tier. New organizations get a trial of up to two hours of Machine runtime or seven days. After that, billing is pay-as-you-go with no monthly platform fee.
- Per-second billing. A running shared-cpu-1x Machine with 256 MB costs about $2.19 per 30 days; 512 MB is about $3.69. Prices vary by region and include egress charges.
- Auto-stop is not free. Stopped Machines are billed only for root filesystem storage, at about $0.15/GB per month. Auto-stop reduces compute cost but does not eliminate it.
- Cold starts. The first request after a Machine stops wakes it, which adds latency. Use min_machines_running = 1 when MCP clients need consistent response times.
- Multi-region state. Multiple Machines or regions do not share local files or memory. The PHP bundle's file session store is per Machine; use an external store if you scale beyond one instance.
- Streaming. The idle_timeout setting keeps long-lived MCP responses from being closed too early. Clients should still reconnect if a Machine is replaced or stopped.
Operational notes
- Secure the endpoint. A fly.dev hostname is publicly reachable. Upstream credentials are protected, but the MCP endpoint is not authenticated; add an authorizer or a gateway before sharing it.
- Redeploy on spec changes. Regenerate the bundle, reapply the port and start-command changes, and run fly deploy.
- Keep one region for stateful tools. If your tools rely on local session state, pin the app to one region and one Machine, or move state out of the process.
- Watch the release logs. Port mismatches and missing environment variables show up in fly logs immediately after a deploy.
Next steps
Fly.io is a good fit when you want container-level control, per-second billing, and Machines that can stop when idle without giving up a public HTTPS endpoint. Compare it with Render, Railway, Google Cloud Run, and AWS App Runner. A side-by-side comparison is available in Where to Deploy Your MCP Server.
Need a bundle to deploy? Start with From OpenAPI to a Running MCP Server.