Operating › Production deployment

Production deployment

Run Uptimer as separate services against PostgreSQL, with migrations gated by a job.

Production differs from the dev image in four ways: PostgreSQL, real auth, one process per service, and migrations run by a dedicated job.

Services

CommandRole
server --services api,uiDashboard + REST API (2517).
server --services grpcWorker gRPC channel (50051).
server --services availabilitiesRuns the scheduled checks.
workerProbers — add as many as you need.

api,ui, grpc and availabilities are one logical server: they share a single identity (server.pem/server.uuid) via a shared volume.

Database & migrations

Use PostgreSQL with two databases — uptimer_server (control plane) and uptimer_worker. Bring the schema to head with a one-shot job before the app starts, and stop replicas migrating on boot:

uptimer migrate                          # run once per deploy — gates the rollout
UPTIMER__SERVER__DB__BOOT_MIGRATE=false  # set on the app containers

Why: the schema is applied once, atomically, instead of racing across booting replicas.

docker-compose

A minimal stack (a runnable split-services variant is in examples/1.3.0/remote-workers):

x-server-env: &server-env
  UPTIMER__SERVER__DB__DSN: "postgres://uptimer:secret@db:5432/uptimer_server?sslmode=disable"
  UPTIMER__SERVER__DB__BOOT_MIGRATE: "false"
  UPTIMER__SERVER__AUTH__DEV: "false"
  UPTIMER__SERVER__SQIDS_SALT: "change-me"
  UPTIMER__GRPC__PORT: "50051"
  UPTIMER__GENERAL__LOGGING__LEVEL: "PROD"

services:
  db:
    image: postgres:17
    environment: { POSTGRES_USER: uptimer, POSTGRES_PASSWORD: secret, POSTGRES_DB: uptimer_server }
    volumes: ["pg:/var/lib/postgresql/data"]
  migrator:
    image: ghcr.io/myuptime-info/uptimer:1.3.0
    command: ["migrate"]
    environment: *server-env
    depends_on: [db]
    restart: "no"
  web:
    image: ghcr.io/myuptime-info/uptimer:1.3.0
    command: ["server", "--services", "api,ui"]
    environment:
      <<: *server-env
      UPTIMER__SERVER__UI__PORT: "2517"
      UPTIMER__GENERAL__SITE_URL: "https://uptimer.example.com"
    ports: ["127.0.0.1:2517:2517"]      # publish on loopback; put TLS + real auth in front
    volumes: ["server:/data"]
    depends_on: [migrator]
  grpc:
    image: ghcr.io/myuptime-info/uptimer:1.3.0
    command: ["server", "--services", "grpc"]
    environment: *server-env
    volumes: ["server:/data"]
    depends_on: [migrator]
  availabilities:
    image: ghcr.io/myuptime-info/uptimer:1.3.0
    command: ["server", "--services", "availabilities"]
    environment: *server-env
    volumes: ["server:/data"]
    depends_on: [migrator]
  worker:
    image: ghcr.io/myuptime-info/uptimer:1.3.0
    command: ["worker"]
    environment:
      UPTIMER__WORKER__DB__DSN: "postgres://uptimer:secret@db:5432/uptimer_worker?sslmode=disable"
      UPTIMER__WORKER__GRPC_SERVER: "grpc:50051"
    volumes: ["worker:/data"]
    depends_on: [grpc]

volumes: { pg: {}, server: {}, worker: {} }

The uptimer_worker database must exist too — create it next to uptimer_server (a Postgres init script is the easy way). web publishes only on loopback; terminate TLS and add real auth at your reverse proxy.

Bootstrap

docker compose run --rm web server init      # once — server identity
docker compose run --rm worker worker init   # once per worker

Then register each worker (UUID + public key) in the dashboard — see Remote workers.

Image

Pin the version explicitly:

docker pull ghcr.io/myuptime-info/uptimer:1.3.0