Introduction
Fly.io is a serverless deployment platform that excels in global, low-latency deployments thanks to its extensive edge network across 35+ regions. Unlike Vercel or Heroku, Fly.io uses Firecracker containers for native horizontal scaling and pay-per-use CPU/memory pricing. In 2026, with the rise of full-stack apps and microservices, mastering Fly.io can cut costs by 40-60% compared to traditional cloud alternatives while delivering worldwide performance.
This intermediate tutorial walks you through deploying a Node.js Express API: from installing flyctl to scalable production rollout. You'll learn to configure an optimized Dockerfile, a custom fly.toml, manage secrets, and enable auto-scaling. By the end, your app will be live on a .fly.dev domain with free HTTPS, production-ready. Ideal for devs looking for an actionable, no-fluff guide to bookmark.
Prerequisites
- Node.js 20+ installed
- Docker Desktop (for local builds)
- Free GitHub account (optional for CI/CD)
- Basic Docker and CLI knowledge
- Unix-like terminal (WSL on Windows)
Install flyctl
curl -L https://fly.io/install.sh | sh
# Vérifier l'installation
fly version
fly auth signupThis one-liner script installs flyctl, Fly.io's official CLI. The fly auth signup command creates a free account (with $3 initial credit). Avoid corporate proxies that block curl; use brew install flyctl on macOS as a fallback.
Create the Base Node.js Application
Before deploying, let's initialize a minimal Express API. Think of it like a house: the server is the foundation, routes are the rooms. We'll create a complete project with a health-check endpoint for Fly.io's Kubernetes-like probes.
Initialize the Express Project
mkdir flyio-node-app && cd flyio-node-app
npm init -y
npm install express
npm install -D nodemon typescript @types/express @types/node tsxThis sets up a Node project with Express for the API and TypeScript for robustness. nodemon and tsx make local development easy. Test locally with npx tsx server.ts before moving to Docker.
Write the Express Server
import express from 'express';
const app = express();
const PORT = process.env.PORT || 8080;
app.use(express.json());
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }]);
});
app.post('/api/users', (req, res) => {
const user = req.body;
res.status(201).json({ id: 3, ...user });
});
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
});
});This complete TypeScript server exposes /health for Fly.io checks, handles basic user CRUD, and listens on 0.0.0.0 (required for containers). The SIGTERM handler ensures graceful shutdowns, preventing connection loss during scaling.
Containerize with Dockerfile
Fly.io builds and deploys via Docker. Our multi-stage Dockerfile creates a lightweight image (<100MB), like a concentrated espresso: fast builds, minimal runtime.
Create the Optimized Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build || true # Si build step existe
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server.ts ./server.ts
EXPOSE 8080
CMD ["npx", "tsx", "server.ts"]Multi-stage builds shrink the image size: builder installs deps, runner copies only production files. Use Alpine for lightness. Add a basic tsconfig.json if missing: {"compilerOptions": {"target": "ES2022", "module": "ESNext"}}.
Configure fly.toml
fly.toml is the deployment manifest: ports, regions, scaling. It's like a factory blueprint: defines processes, volumes, and HTTP.
Generate and Customize fly.toml
app = "flyio-node-demo-2026"
primary_region = "cdg" # Paris pour latence EU
[build]
builder = "paketobuildpacks/builder:base"
[env]
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
[[http_service.checks]]
path = "/health"
port = 8080
type = "http"
interval = "10s"
grace_period = "5s"
method = "GET"
[[services]]
internal_port = 8080
protocol = "tcp"
[[services.ports]]
port = 80
handlers = ["http"]
[[services.ports]]
port = 443
handlers = ["tls", "http"]This fly.toml sets up a named app, EU region (cdg=Paris), health checks for auto-scaling, and forced HTTPS. auto_stop_machines saves ~90% on idle costs. Run fly launch --no-deploy to auto-generate and then edit.
Set Environment Secrets
flyctl apps create flyio-node-demo-2026 --region cdg
flyctl secrets set DATABASE_URL=postgres://user:pass@db.flycast:5432/db
flyctl secrets set API_KEY=sk-1234567890abcdef
flyctl secrets listSecrets are encrypted and injected at runtime, invisible in logs. Use them for DB creds or API keys. Run fly apps create if not using launch; list checks without exposing values.
Deploy the Application
flyctl deploy --ha=false
# Vérifier status
flyctl status
flyctl logs
curl https://flyio-node-demo-2026.fly.dev/healthdeploy builds/pushes the image and rolls out with zero downtime. --ha=false for single-region start; remove for global. Real-time logs aid debugging; health endpoint confirms readiness.
Scaling and Monitoring
Once live, scale horizontally. Fly.io's autoscaler adjusts VMs (machines) based on CPU/load, like a smart thermostat.
Scale and Configure Autoscaler
flyctl scale count 2 --max-per-region 3
flyctl scale vm shared-cpu-1x --memory 512
# Autoscaler
flyctl scale set-autoscaler --min 1 --max 10
# Metrics
flyctl metrics
flyctl dashboardScale to 2 min instances, max 3/region for HA. Autoscaler handles bursts (min 1 idle). metrics shows live CPU/RAM; dashboard provides advanced web graphs.
Best Practices
- Smart regions: Pick primary_region near your users (
fly regions list), add [[services]] for multi-region. - Lightweight images: <200MB max; use .dockerignore for node_modules/src.
- Strict health checks: Always include /healthz with <5s timeout for fast restarts.
- GitHub CI/CD: Add flyctl/deploy to Actions for zero-config.
- Persistent volumes:
fly volumes create data --region cdg --size 10for local DB.
Common Errors to Avoid
- Port binding: Listen on 0.0.0.0:8080, not localhost; otherwise OOMKilled.
- No Dockerfile: Fly falls back to buildpacks, but it's slow; always use a custom Dockerfile.
- Exposed secrets: Never in fly.toml or logs; use
fly secrets setonly. - Scaling without checks: Machines get stuck crashed without /health; always add it.
Next Steps
Master managed Postgres (fly postgres create), Workers KV, or Tailscale VPN integration. Check the official Fly.io docs and our Learni Dev cloud-native deployment courses for advanced hands-on workshops.