Galka

Self-hosting

The Galka sync server is open source and published as a Docker image. Run it on your own machine and your tasks never leave it — the app talks to your address instead of api.getgalka.ru, and nothing else changes.

What you need: a host with Docker and Docker Compose, about 512 MB of RAM, and — if the app will reach it over the internet — a domain name pointing at the host so it can hold a TLS certificate. Everything else the stack brings with it: PostgreSQL for storage and Redis for the realtime channel behind /events.

What it is

Run it

1 · A compose file

Save this as docker-compose.yml in an empty directory. It runs the database, Redis, a one-shot schema step and the API itself, and publishes the API on port 8000 of the host:

name: galka

services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: galka
      POSTGRES_USER: galka
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set it in .env}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U galka -d galka"]
      interval: 10s
      retries: 10

  redis:
    image: redis:8-alpine
    restart: unless-stopped
    command: ["redis-server", "--save", "", "--appendonly", "no"]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      retries: 10

  # Creates the schema and exits. Two API workers starting against an empty
  # database would otherwise race to emit the same CREATE TABLE.
  migrate:
    image: skymanrm/galkaapp-api:latest
    command: ["python", "scripts/init_db.py"]
    restart: "no"
    environment:
      TODOAPI_DATABASE_URL: postgresql+asyncpg://galka:${POSTGRES_PASSWORD:?}@db:5432/galka
    depends_on:
      db: { condition: service_healthy }

  api:
    image: skymanrm/galkaapp-api:latest
    restart: unless-stopped
    command: [uvicorn, app.main:app, --host=0.0.0.0, --port=8000,
              --workers=2, --proxy-headers, --forwarded-allow-ips=*]
    environment:
      TODOAPI_DATABASE_URL: postgresql+asyncpg://galka:${POSTGRES_PASSWORD:?}@db:5432/galka
      TODOAPI_REDIS_URL: redis://redis:6379/0
      TODOAPI_ADMIN_USERNAME: ${TODOAPI_ADMIN_USERNAME:-admin}
      TODOAPI_ADMIN_PASSWORD: ${TODOAPI_ADMIN_PASSWORD:?set it in .env}
      TODOAPI_PUBLIC_URL: ${TODOAPI_PUBLIC_URL:-http://localhost:8000}
    ports:
      - "8000:8000"
    depends_on:
      migrate: { condition: service_completed_successfully }
      redis: { condition: service_healthy }

volumes:
  pgdata:

2 · A .env beside it

Compose reads .env from the directory it runs in. Nothing has a default password, so a missing value stops the stack rather than starting it wide open:

POSTGRES_PASSWORD=<a long random string>
TODOAPI_ADMIN_USERNAME=admin
TODOAPI_ADMIN_PASSWORD=<another long random string>
TODOAPI_PUBLIC_URL=https://todo.example.com

chmod 600 .env

3 · Start it

docker compose pull
docker compose up -d
curl http://localhost:8000/health

4 · Put TLS in front of it

The image speaks plain HTTP; terminate TLS with whatever you already run — Traefik, Caddy, nginx. Two things matter for Galka: pass the X-Forwarded-* headers through, and do not time out idle responses. /events is a Server-Sent Events stream held open for as long as a device is connected, and a proxy that cuts it at 60 seconds turns live sync into polling. The production stack uses Traefik with readTimeout=0 and idleTimeout=0 on the HTTPS entrypoint; the full file is in the repository if you want to copy it.

Point the app at it

Settings

All of them are environment variables with the TODOAPI_ prefix.

VariableDefaultWhat it is
TODOAPI_DATABASE_URLlocal postgres Async SQLAlchemy URL — postgresql+asyncpg://….
TODOAPI_REDIS_URLredis://localhost:6379/0 Pub/sub fan-out behind /events. Nothing is stored in it.
TODOAPI_SSE_PING_SECONDS15 Keep-alive interval on the event stream.
TODOAPI_ADMIN_USERNAMEadmin HTTP Basic user for /admin.
TODOAPI_ADMIN_PASSWORDadmin Change it. /admin lists every user's data.
TODOAPI_OWNER_NAMEthe author Name on your copy of the public pages.
TODOAPI_CONTACT_EMAILthe author's Address those pages tell people to write to.
TODOAPI_PUBLIC_URLhttps://api.getgalka.ru Your own address; used to build links on the public pages.
TODOAPI_APP_STORE_URLthe App Store listing Set it empty to drop the download button.

Living with it

The protocol, if you are curious

Sync is offline-first and last-write-wins: every record carries a client-generated UUID and a client-set timestamp, the server keeps a per-user sequence number, and each device pulls everything newer than the cursor it holds. Deletes are two-stage — the Trash is an ordinary synced field, a tombstone is written only on purge. The README documents it endpoint by endpoint, and your own server serves the OpenAPI schema at /openapi.json and the browsable docs at /docs.

Questions, or something that does not work? Write to support@getgalka.ru, or open an issue on GitHub.