HotupdaterHot Updater
Self Hosting (Custom)Hosting

Docker

Since v0.22.0+

Deploy Hot Updater server using Docker containers.

Prerequisites

  • Docker Or OrbStack: Install it via Docker or OrbStack.
  • Docker Compose (included with Docker Desktop)
  • PostgreSQL database or use Docker Compose setup below

Installation

Install required dependencies.

npm install @hot-updater/server @hot-updater/aws hono @hono/node-server drizzle-orm postgres
npm install @hot-updater/bare @hot-updater/standalone hot-updater drizzle-kit tsx typescript @types/node --save-dev

Database Setup

Create the database connection file.

src/drizzle.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "../hot-updater-schema";

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema, casing: "snake_case" });
drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./hot-updater-schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: { url: process.env.DATABASE_URL! },
});

Hot Updater Configuration

Create the Hot Updater instance.

src/hotUpdater.ts
import { createHotUpdater } from "@hot-updater/server";
import { drizzleAdapter } from "@hot-updater/server/adapters/drizzle";
import { s3Storage } from "@hot-updater/aws";
import { db } from "./drizzle";

export const hotUpdater = createHotUpdater({
  database: drizzleAdapter({ db, provider: "postgresql" }),
  storages: [
    s3Storage({
      region: "auto",
      endpoint: process.env.R2_ENDPOINT!,
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY_ID!,
        secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
      },
      bucketName: process.env.R2_BUCKET_NAME!,
    }),
  ],
  basePath: "/hot-updater",
  routes: { updateCheck: true, bundles: true },
});

Server Setup

Create the server entry point.

src/index.ts
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { bearerAuth } from "hono/bearer-auth";
import { hotUpdater } from "./hotUpdater";

const managementToken = process.env.HOT_UPDATER_AUTH_TOKEN;
if (!managementToken) {
  throw new Error("HOT_UPDATER_AUTH_TOKEN is required");
}

const app = new Hono();

app.use("/hot-updater/api/*", bearerAuth({ token: managementToken }));
app.mount("/hot-updater", hotUpdater.handler);

const port = Number(process.env.PORT) || 3000;

serve({ fetch: app.fetch, port }, (info) => {
  console.log(`Server running at http://localhost:${info.port}`);
});

When using Hono mount(), update-check routes such as /hot-updater/app-version/* and /hot-updater/fingerprint/* are usually exposed for React Native clients, but that is a deployment choice. /hot-updater/version is always mounted for diagnostics. Always protect /hot-updater/api/* when routes.bundles: true.

Schema Generation

Generate the Drizzle schema for Hot Updater tables.

npx hot-updater db generate src/hotUpdater.ts --yes

db generate writes hot-updater-schema.ts; it does not change the database. Apply the generated schema separately:

npx drizzle-kit push

Build Configuration

The image uses CommonJS and compiles the root schema together with src:

package.json (excerpt)
{
  "private": true,
  "type": "commonjs",
  "scripts": {
    "build": "tsc"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "include": ["src", "hot-updater-schema.ts"]
}

This emits the server entry point as dist/src/index.js.

Dockerfile

Create Dockerfile for schema generation, migration, and production runtime.

FROM node:22-slim AS builder

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci

COPY . .
RUN DATABASE_URL=postgresql://schema:placeholder@localhost:5432/schema \
    npx hot-updater db generate src/hotUpdater.ts --yes
RUN npm run build

FROM builder AS migrator
CMD ["npx", "drizzle-kit", "push"]

FROM node:22-slim AS runtime

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

EXPOSE 3000

CMD ["node", "dist/src/index.js"]

Create .dockerignore:

node_modules
dist
.git
.env*
*.log

Build and Run

Build the migration and runtime images, then apply the schema before starting the server.

docker build --target migrator -t hot-updater-migrator .
docker run --rm \
  -e DATABASE_URL="postgresql://user:password@host:5432/dbname" \
  hot-updater-migrator
docker build --target runtime -t hot-updater-server .

Run the container with environment variables.

docker run -p 3000:3000 \
  -e DATABASE_URL="postgresql://user:password@host:5432/dbname" \
  -e HOT_UPDATER_AUTH_TOKEN="replace-with-a-secret" \
  -e R2_ENDPOINT="https://your-account-id.r2.cloudflarestorage.com" \
  -e R2_ACCESS_KEY_ID="..." \
  -e R2_SECRET_ACCESS_KEY="..." \
  -e R2_BUCKET_NAME="..." \
  hot-updater-server

Docker Compose

Create docker-compose.yml for complete setup with PostgreSQL.

version: "3.8"

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: hot_updater
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  migrate:
    build:
      context: .
      target: migrator
    environment:
      DATABASE_URL: postgresql://postgres:postgres@postgres:5432/hot_updater
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"

  hot-updater:
    build:
      context: .
      target: runtime
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://postgres:postgres@postgres:5432/hot_updater
      HOT_UPDATER_AUTH_TOKEN: replace-with-a-secret
      PORT: 3000
      R2_ENDPOINT: https://your-account-id.r2.cloudflarestorage.com
      R2_ACCESS_KEY_ID: "..."
      R2_SECRET_ACCESS_KEY: "..."
      R2_BUCKET_NAME: "..."
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:

Run with Docker Compose

Build the targets, apply the schema once, then start the server.

docker compose build
docker compose run --rm migrate
docker compose up -d hot-updater

View logs.

docker compose logs -f hot-updater

Stop all services.

docker compose down

CLI Configuration

Configure your CLI to use this server.

hot-updater.config.ts
import { bare } from "@hot-updater/bare";
import { s3Storage } from "@hot-updater/aws";
import { standaloneRepository } from "@hot-updater/standalone";
import { defineConfig } from "hot-updater";

const managementToken = process.env.HOT_UPDATER_AUTH_TOKEN;
if (!managementToken) {
  throw new Error("HOT_UPDATER_AUTH_TOKEN is required");
}

export default defineConfig({
  build: bare({ enableHermes: true }),
  storage: s3Storage({
    region: "auto",
    endpoint: process.env.R2_ENDPOINT!,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY_ID!,
      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
    },
    bucketName: process.env.R2_BUCKET_NAME!,
  }),
  database: standaloneRepository({
    baseUrl: "http://localhost:3000/hot-updater",
    commonHeaders: { Authorization: `Bearer ${managementToken}` },
  }),
  updateStrategy: "appVersion",
});

The storage plugin must match the storages in your server's createHotUpdater.

On this page