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
- The whole backend, nothing else. Accounts, the sync protocol, the Trash, the activity log, the signup page and an admin page. No telemetry and no call home — the server only ever talks to the devices that sign in to it.
- One image,
skymanrm/galkaapp-api, built by CI from the same commit that runs in production and tested inside the image before it is published.:latestfollows the main branch; every build also gets an immutable:sha-<12>tag to pin or roll back to. - MIT licensed. The name Galka and the app icon are not covered by the licence; the code 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
- Open
https://your-host/signupin a browser and create the account there. The app signs in, it does not register. - In Galka, open Settings ▸ Account ▸ Sign in, replace the server address with your own, and enter that email and password. Every device you sign in the same way joins the same account.
- Signing out leaves the tasks on the device — the local database is the source of truth and sync is an addition to it, so you can move between servers without losing anything.
Settings
All of them are environment variables with the TODOAPI_ prefix.
| Variable | Default | What it is |
|---|---|---|
TODOAPI_DATABASE_URL | local postgres | Async SQLAlchemy URL — postgresql+asyncpg://…. |
TODOAPI_REDIS_URL | redis://localhost:6379/0 |
Pub/sub fan-out behind /events. Nothing is stored in it. |
TODOAPI_SSE_PING_SECONDS | 15 |
Keep-alive interval on the event stream. |
TODOAPI_ADMIN_USERNAME | admin |
HTTP Basic user for /admin. |
TODOAPI_ADMIN_PASSWORD | admin |
Change it. /admin lists every user's data. |
TODOAPI_OWNER_NAME | the author | Name on your copy of the public pages. |
TODOAPI_CONTACT_EMAIL | the author's | Address those pages tell people to write to. |
TODOAPI_PUBLIC_URL | https://api.getgalka.ru |
Your own address; used to build links on the public pages. |
TODOAPI_APP_STORE_URL | the App Store listing | Set it empty to drop the download button. |
Living with it
- Updating:
docker compose pull && docker compose up -d. Pinimage:at a:sha-tag first if you would rather choose your moment. - Backups: everything is in Postgres —
docker compose exec -T db pg_dump -U galka galka | gzip > galka.sql.gzon a timer is enough. Redis holds no state. - Schema changes are applied by the
migrateservice on each start; new columns are added in place, so an update needs no manual step. - Admin:
/adminshows users, their projects and tasks and their device tokens. It is reachable from wherever the API is, so give it a real password or keep it off the public internet.
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.