Files
keepalive/README.md
T
tiennm99 0af652953f fix: bound connects, write on start, and tighten config checks
- Each connect attempt, including initialization, times out after 1 minute,
  and PostgreSQL DSNs without connect_timeout get connect_timeout=30, since
  lib/pq honours the context only while dialing.
- A service increments right after connecting, so a restart writes even
  when the interval outlasts the process.
- A config path that is a directory (Docker's stand-in for a missing
  bind-mount source) fails with a clear message.
- Generated service names take the first free suffix, and suffixed names
  are reserved so an explicit name cannot silently collide with them.
- counter_key inside a service's config map is rejected instead of being
  overwritten.
- Unknown keys in the config file log a warning without blocking start.
- A .dockerignore keeps local configs and .env out of the build context.
2026-10-09 11:15:59 +07:00

7.4 KiB

keepalive

Pluggable Go daemon that periodically touches external services to prevent idle shutdowns, pauses, or cold starts.

The current adapters perform cheap datastore writes for Redis Cloud, Valkey, Aiven, Neon, Supabase, MongoDB Atlas, Couchbase Capella, and similar hosted services.

Successor to the *-keepalive family: one binary, one image, five datastore adapters. Valkey and other Redis-compatible stores use the redis adapter.

Configuration

By default, keepalive reads the first config file it finds: config.yml, config.yaml, /config.yml, then /config.yaml. One deployment can keep any number of services alive.

# Default interval for every service.
interval: 1m
counter_key: counter

services:
  - adapter: redis
    config:
      url: redis://default@redis-a.example.com:6379
      namespace: keepalive

  # Valkey, Dragonfly, KeyDB and other Redis-compatible stores use the redis
  # adapter with a redis:// or rediss:// (TLS) URL.
  - name: valkey-a
    adapter: redis
    # One service can override the global interval and counter key.
    interval: 30s
    counter_key: valkey-counter
    config:
      url: rediss://default@valkey-a.example.com:6379
      namespace: keepalive

  - adapter: postgresql
    config:
      url: postgresql://user:pass@postgres-a.example.com:5432/keepalive?sslmode=require

  - adapter: mysql
    config:
      dsn: user:pass@tcp(mysql-a.example.com:3306)/keepalive

  - adapter: mongodb
    config:
      uri: mongodb+srv://user:pass@mongo-a.example.com
      database: keepalive
      collection: counter

  - adapter: couchbase
    config:
      connection_string: couchbases://couchbase-a.example.com
      username: user
      password: pass
      bucket_name: keepalive
      scope_name: _default
      collection_name: _default

name is optional. When omitted, keepalive generates a name from adapter and the connection host, such as redis-redis-a-example-com. Duplicate generated names get the first free suffix, like redis-redis-a-example-com-2.

interval at the root sets the default schedule for every service and defaults to 1m. interval inside a service overrides that default only for that service. In the example above, every service runs every 1m except valkey-a, which runs every 30s. Interval values use Go duration syntax, for example 30s, 5m, 1h, 1h30m, or 1.5h. Plain integers are treated as seconds, so 90 means 90s.

counter_key at the root sets the default counter key for every service and defaults to counter. counter_key inside a service overrides that default only for that service. In the example above, every service writes counter except valkey-a, which writes keepalive:valkey-counter.

Supported adapters

adapter Driver config keys
redis github.com/redis/go-redis/v9 url (redis:// or rediss://), optional namespace
postgresql github.com/lib/pq url
mysql github.com/go-sql-driver/mysql dsn
mongodb go.mongodb.org/mongo-driver/v2 uri, database, collection
couchbase github.com/couchbase/gocb/v2 connection_string, username, password, bucket_name, scope_name, collection_name, optional ready_timeout, optional bucket_ram_quota_mb

Quick start (Compose)

cp config.example.yml config.yml
docker compose up -d --build

compose.yml also deploys on Coolify with the Docker Compose build pack (compose file /compose.yml). Coolify turns the ./config.yml bind mount into an editable file storage; paste your config there. keepalive is a background worker with no port, so leave the service without a domain. Keep the real config.yml out of git, since it holds datastore credentials. Create config.yml before the first start: if the file is missing, Docker mounts an empty directory in its place and keepalive exits with an error. The container runs as user 65532, so the file must be readable by that user (for example mode 0644).

Quick start (Docker)

docker build -t keepalive:local .
docker run -d --name keepalive --restart unless-stopped \
  -v "$PWD/config.yml:/config.yml:ro" \
  keepalive:local

If you prefer to mount the config into a working directory instead of the container root, set the container working directory and mount the file there:

docker run -d --name keepalive --restart unless-stopped \
  --workdir /workspace \
  -v "$PWD/config.yml:/workspace/config.yml:ro" \
  keepalive:local

Quick start (local)

git clone https://github.com/tiennm99/keepalive
cd keepalive
cp config.example.yml config.yml
go run .

How it works

On startup each adapter initializes the minimum resource it owns, then every tick performs the cheapest write that proves the cluster is alive. counter_key selects the key/doc ID and defaults to counter.

  • Redis (also Valkey, Dragonfly, KeyDB, Garnet, Upstash) — initialize with SETNX key 0, then INCR key. When namespace is empty, the key is counter; when namespace: keepalive, the key is keepalive:counter.
  • PostgreSQL — CREATE TABLE IF NOT EXISTS keepalive, seed key, then UPDATE ... RETURNING
  • MySQL — CREATE TABLE IF NOT EXISTS keepalive, seed key, then UPDATE + SELECT
  • MongoDB — upsert {_id: key, count: 0} on connect, then FindOneAndUpdate({_id: key}, {$inc: {count: 1}}, upsert)
  • Couchbase — optionally create the bucket when bucket_ram_quota_mb is set, create configured scope/collection when missing, insert key = 0 if missing, then an atomic binary INCREMENT key

Each configured service starts independently and writes once right after connecting, then once per interval. A connect attempt, including initialization, gives up after 1 minute; PostgreSQL URLs without connect_timeout get connect_timeout=30. When a connect or a tick fails, the service logs the error, closes its connection, and reconnects after 1 minute, which also re-runs initialization (for example, recreating a dropped table). Other services in the same deployment keep running. An unknown adapter, a missing required config key, or counter_key placed inside config stops keepalive at startup. Unknown keys elsewhere in the file, such as a misspelled interval, only log a warning.

For hosted Couchbase/Capella clusters, ready_timeout defaults to 30s. If Couchbase reports CONNECTION_ERROR, check the connection string, bucket name, database user permissions, and Capella allowed IP/network access.

Adding a new adapter

  1. Create adapter/<name>.go.
  2. Implement the Adapter interface in adapter/adapter.go (Connect, Increment, Close).
  3. Register the factory in init():
    func init() { Registry["<name>"] = func(cfg Config) (Adapter, error) { return &myAdapter{}, nil } }
    
  4. Add an import _ "your driver" if needed, and the adapter config keys to config.example.yml and the table above.

Migrated from

This repo replaces six single-datastore repos (redis-keepalive, valkey-keepalive, postgresql-keepalive, mysql-keepalive, mongodb-keepalive, couchbase-keepalive). Their full histories were absorbed into this repository — browse earlier commits to find each implementation under its own subfolder (redis/, valkey/, postgresql/, mysql/, mongodb/, couchbase/).

License

Apache-2.0 — see LICENSE.