Deploy a Node.js API without managing servers
Package a Node.js API as a Docker image that runs without root on port 8080, push it to Docker Hub and host it on blitz.cloud with HTTPS, for free.
To deploy a Node.js API without managing a server on blitz.cloud, you put it in a small Docker image that listens on port 8080 and runs as a normal user, push that image to Docker Hub, and pick it in the blitz.cloud dashboard. blitz.cloud runs it in its own sandbox under an HTTPS address and restarts it if the process dies. No VM, no nginx config, no certificates to renew.
The code in this guide is the node-api example. It uses Node's built-in http module so there is nothing to learn beyond Node itself. An Express or Fastify app works the same way.
The server
Two details make a Node app behave well on blitz.cloud. It reads its port from the environment with 8080 as the default, and it stops cleanly when it receives SIGTERM, which is how a container is asked to shut down during a restart.
import http from "node:http";
const port = Number(process.env.PORT ?? 8080);
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "content-type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ hello: "from blitz.cloud" }));
});
server.listen(port, () => console.log(`listening on :${port}`));
process.on("SIGTERM", () => server.close(() => process.exit(0)));
The full version in the repository also talks to PostgreSQL when DATABASE_URL is set. That part is covered in Deploy an app with a PostgreSQL database.
The Dockerfile
# Install dependencies in a throwaway stage so the final image has no npm cache.
FROM node:22.23.2-alpine3.24 AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22.23.2-alpine3.24
ENV NODE_ENV=production PORT=8080
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
COPY src ./src
# The official Node image ships a "node" user with uid 1000, the same uid
# blitz.cloud runs every app as.
USER node
EXPOSE 8080
CMD ["node", "src/server.js"]
EXPOSE 8080 is not decoration here. blitz.cloud reads the ports an image declares and uses that one, so you don't have to type a port anywhere.
Starting with node directly rather than npm start matters too: npm doesn't pass SIGTERM on reliably, so your shutdown handler would never run.
Test it like blitz.cloud would run it
docker build --platform linux/amd64 -t blitz-example-node-api .
docker run --rm --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges \
-p 8080:8080 blitz-example-node-api
curl http://localhost:8080/
# {"hello":"from blitz.cloud","database":"none"}
When we ran exactly this, / and /healthz answered 200. If your own app fails at this step, it will fail on blitz.cloud too, and the error is easier to read on your laptop.
Push to Docker Hub
docker build --platform linux/amd64 -t yourname/blitz-example-node-api:1.0.0 -t yourname/blitz-example-node-api:latest .
docker login
docker push --all-tags yourname/blitz-example-node-api
The repository on Docker Hub has to be public. Tag every build twice, with a version like 1.0.0 and with latest: the version tells you which build is which, and latest is what the Restart route further down relies on.
If you'd rather not push from your laptop, the examples repository has a GitHub Actions workflow that builds for amd64 and pushes on every commit to main, once you add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN as secrets in your fork.
Deploy on blitz.cloud
- In the dashboard, click Host something new and choose "An app that is already packaged up".
- Search for
yourname/blitz-example-node-api, picklatestand click Continue (the section on shipping new versions below explains why). A repository you just pushed can take a while to show up in Docker Hub's search. - What this app needs should say port 8080, "the one the app itself declares".
- Name it, give it an address such as
notes, and click Put it online.
Your API is then at https://notes.yourname.blitz.cloud.
Configuration and secrets
Settings go on the app's Environment tab. Values are stored encrypted, masked on screen, and reach the app the next time it restarts, so change a value, then restart. Up to 64 settings per app.
Shipping a new version
An existing app can't be switched to a different tag from the dashboard yet, and automatic updates are still being built. Until then, the route that works: deploy the latest tag, push a new latest when you release, and press Restart on the app. A tag named latest is fetched again every time the app starts, so the restart picks up the new build. You lose the ability to tell at a glance which version is running, which is the price.
If the repository is public you can skip all of this: paste its link into "Host something new" and blitz.cloud builds it for you, see deploy a GitHub project. This section is the route for a private project, until connecting a GitHub account ships (What's live).
Is this a good fit?
For a small JSON API, a webhook receiver or a Discord bot with an HTTP endpoint, yes. Each app gets 128 MB of memory reserved and up to 512 MB, and a share of a CPU. For a busy API that needs several cores, or for serverless functions that scale to zero and bill per request, it isn't what blitz.cloud does.
Put your first app online today.
Free plan, no credit card, no waiting list.
Create a free account