Express apps deploy with npm start. Listen on PORT, turn on trust proxy for HTTPS cookies, and keep sessions out of memory so logins survive a deploy.
Last updated
These are the defaults. Override any of them in Settings → Build.
// package.json
"scripts": {
"start": "node index.js"
}
// index.js
const express = require("express");
const app = express();
app.set("trust proxy", 1);
app.get("/healthz", (req, res) => res.send("ok"));
app.listen(process.env.PORT || 3000);We build on Node 22 unless engines.node in package.json asks for another version. For TypeScript, add a build script (tsc) and start the compiled output (node dist/index.js). The Node.js guide covers TypeScript and monorepos in more detail.
HTTPS ends at the platform's proxy, so your app sees plain HTTP. With app.set("trust proxy", 1), Express reads X-Forwarded-Proto: req.secure is true and cookies marked secure are actually sent.
Without it, express-session silently drops a secure session cookie and users can never stay logged in, and express-rate-limit logs an ERR_ERL_UNEXPECTED_X_FORWARDED_FOR validation error.
Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving.
express-session's default MemoryStore lives in the process, so every deploy logs everyone out. Store sessions in a Redis or Postgres addon:
const session = require("express-session");
const { RedisStore } = require("connect-redis");
const { createClient } = require("redis");
const redis = createClient({ url: process.env.REDIS_URL });
redis.connect();
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true },
}));Add a Postgres addon and DATABASE_URL is injected automatically. The database is on a private network, so no SSL options are needed:
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });Run migrations (Prisma, Drizzle, Knex) from the Build command or in front of the start command, for example npx prisma migrate deploy && node index.js.
On each deploy the new container starts first, and the old one is stopped once the new one is healthy. Close the server on SIGTERM so requests in flight can finish:
const server = app.listen(process.env.PORT || 3000);
process.on("SIGTERM", () => server.close(() => process.exit(0)));<project-slug>.ethiodeploy.com. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.TypeError: Missing parameter name at index 1: *
Cause: Express 5 changed route path syntax, so a bare * wildcard is no longer valid.
Fix: Name the wildcard: app.get("/*splat", …), or "/{*splat}" to also match the root.
Users are logged out on every request or every deploy
Cause: trust proxy is off (secure cookie dropped) or sessions live in memory.
Fix: Set trust proxy and use a Redis or Postgres session store.
Deploy fails: GET / returned 404 (or 401)
Cause: / doesn't return a 2xx or 3xx status, so the health check never passes.
Fix: Add a /healthz route and set it as the Health Check Path.
Health check times out
Cause: The server listens on localhost or a different port.
Fix: Call app.listen(process.env.PORT) without a host.
Cannot find module 'dist/index.js'
Cause: The TypeScript build didn't run or outputs somewhere else.
Fix: Add a build script and match the path in start.
Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.
Can I host an Express app for free?
Yes. The Free plan runs one project with a 512 MB Postgres or Redis at no cost. It sleeps after 30 minutes without traffic and wakes on the next request. Pro ($10/month) is always on with 1 GB of memory.
Do WebSockets and Socket.IO work?
Yes. The app runs as one long-lived process behind the proxy, so WebSocket upgrades work without extra setup.
Can I run a Discord or Telegram bot with Express?
If the bot only uses webhooks, run it as a normal web service. For a long-polling bot with no HTTP server, set the service type to Background Worker, which skips the health check and never sleeps. Workers on the Free plan count against its usage quota.
Ready to deploy? Sign in with GitHub and pick your repo. No Dockerfile, no YAML.
See the full docs for custom domains, env vars, addons, and billing.