# 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 in the EU with HTTPS.

Web page: https://blitz.cloud/guides/deploy-nodejs-api/
Updated: 2026-09-26

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](https://github.com/blitzworks-io/blitz-cloud-examples/tree/main/node-api). 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.

```javascript
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](https://blitz.cloud/guides/deploy-app-with-postgres/).

## The Dockerfile

```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

```bash
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
```

```bash
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

```bash
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 has to be public. Docker Hub is what the wizard searches; a public image on ghcr.io or Quay works too if you type its full name. 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](https://github.com/blitzworks-io/blitz-cloud-examples/blob/main/.github/workflows/docker-publish.yml) 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

1. In the [dashboard](https://dashboard.blitz.cloud/), click Host something new and choose "An app from Docker Hub".
2. Search for `yourname/blitz-example-node-api`, pick `latest` and 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.
3. What this app needs should say port 8080, "the one the app itself declares".
4. 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

Deploy the `latest` tag and push a new `latest` when you release. Automatic updates are on by default for a tag that moves: every night around 4:00 UTC blitz.cloud checks where `latest` points and puts a newer build online. If the new version doesn't start within ten minutes, the one before comes back. To ship right away, 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. The tag won't tell you which build is running; the app's Activity tab lists each version that went live.

If your code is on GitHub, you can skip Docker Hub altogether. Connect your GitHub account in blitz.cloud, pick the repository, private ones included, and the branch. Every push then builds the Dockerfile above and puts the new version online. See [deploy a GitHub project](https://blitz.cloud/docs/deploy-from-github/).

## 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.
