Home-Ops

Home-Ops

apps.detail.types.app from gobrowns' Repository

apps.detail.sections.overview

Home Ops is a self-hosted household management system: chores, a shared calendar, recipes and meal planning, groceries, contacts, notes and health records — one private application on hardware you own. No cloud account, no API key, no telemetry. It is built for a household rather than a company, which shows up in one place above all: members have different levels of trust. Every record carries its own visibility — private, shared with named people, or the whole household — and there is no administrator override on any of it. Every member also owns a colour that follows them into every list. Modules: Tasks (assignments, categories, priorities, subtasks, recurring chores) · Notes (a shared noticeboard with tags and pinning) · Calendar (month and agenda views, RFC 5545 recurrence, drag to move) · Kitchen (recipes with structured ingredients, import from a web page or a Mealie backup, meal planning) · Shopping (many lists, per-list visibility, generated from the meal plan) · Contacts (vCard import and export) · Health (vitals, medications, lab results, activity). Everything runs in this single container — FastAPI, nginx and PostgreSQL 16 together under s6-overlay — with one appdata path and one port. The first page you open is a setup wizard; whoever completes it becomes the administrator, and that page stops existing once a user exists. Before you start: set POSTGRES_PASSWORD, and leave RUN_MIGRATIONS set to true so the database schema is created on first boot. Add your server's LAN address to APP_HOSTS and set WEB_PORT to the host port you mapped, or the browser will be refused when it tries to log in. Appdata: the container path /data holds the PostgreSQL cluster and the uploaded recipe images together. Point it at a share that lives on the cache pool (not a fuse/shfs path that can move to the array) — PostgreSQL is not happy on /mnt/user. Back up that one directory and you have backed up everything.

Home Ops

A self-hosted household management system: chores, a shared calendar, recipes and meal planning, groceries, contacts, notes and health records — one private application on hardware you own.

It is built for a household rather than a company, which mostly shows up in one place: members have different levels of trust. A teenager, a housemate and a grandparent should not see the same things, and "the admin can see everything" is the wrong answer for a family's medical notes. So every record carries its own visibility, and there is no administrator override on any of it.

Runs as one Docker container — application, web server and database together — with a single volume and a single port.

What it does

Module What it covers
Tasks Assignments, categories, priorities, subtasks, recurring chores
Notes A shared noticeboard with tags, pinning and a shared order
Calendar Month and agenda views, RFC 5545 recurrence, three edit scopes, drag to move
Kitchen Recipes with structured ingredients, import from a web page or a Mealie backup, meal planning
Shopping Many lists, per-list visibility, transfers between them, generated from the meal plan
Contacts A household directory with vCard import and export
Health Vitals, medications, lab results and activity, shared per person

Two ideas run through all of it. Every member owns a colour that follows them into every list, so you find your own row before you read a word. And visibility is per record — private, shared with named people, or the whole household — enforced on every read path.

Requirements

  • Docker, with Compose v2 (docker compose, not docker-compose)
  • About 1 GB of disk for the image, plus whatever your data comes to
  • No cloud account, no API key, no external service

Installation

1. Set a database password. Do this before the first start — Postgres bakes it into the data directory on first boot, and changing it afterwards does not change the user.

cp .env.example .env

Then edit .env and set:

POSTGRES_PASSWORD=<something long and random>
RUN_MIGRATIONS=true
APP_HOSTS=localhost,192.168.1.50        # your server's LAN address

RUN_MIGRATIONS=true matters: it creates the database schema. Left off, the app starts but has no tables, and the first page fails.

2. Start it.

docker compose up -d --build

The first build takes a few minutes. If your Docker's Compose has trouble building, build the image directly and then start it:

docker build -t home-ops:single .
docker compose up -d

3. Open it at http://localhost:8080 — or the address in APP_HOSTS, on the port in WEB_PORT.

The first visit shows a setup page, because no account exists yet. Whoever completes it becomes the administrator. That page stops existing once there is a user.

Configuration

.env is written so each value appears once. Four lines cover a normal install:

POSTGRES_PASSWORD Set before the first start. Any characters; it is escaped wherever it is used.
WEB_PORT The port the app is served on. Written here and nowhere else.
APP_HOSTS Every hostname and LAN address you will reach it on — names only, comma separated. Each becomes an allowed origin.
PUBLIC_BASE_URL Your HTTPS address, if you put it behind a tunnel or a reverse proxy.

DATABASE_URL and TRUSTED_ORIGINS are assembled from those and should stay blank unless you need something they cannot express.

Reaching it over HTTPS

The app is designed to answer on several addresses at once — a public HTTPS hostname, a VPN name, and a LAN address — because a household reaches it from different places. Two consequences:

  • SESSION_COOKIE_SECURE defaults to false, because a Secure cookie is silently dropped over plain HTTP and would lock out the LAN. Set it true only if every path is HTTPS.
  • TLS is not handled here. Put a tunnel or a reverse proxy in front and add the hostname to PUBLIC_BASE_URL.

Everyday operation

Update to a newer version. Your data lives in a Docker volume, not the image, so replacing the image keeps it:

docker compose down
docker compose up -d --build
docker compose exec app home-ops-migrate      # if the schema changed

Apply migrations by hand (what RUN_MIGRATIONS=true does for you on start):

docker compose exec app home-ops-migrate
docker compose exec app home-ops-migrate current

Back it up. Everything is under /data in the one volume — the database and the uploaded recipe images together:

docker run --rm -v home-ops_home_ops_data:/data -v "$PWD:/out" \
  alpine tar czf /out/home-ops-backup.tar.gz -C /data .

A database dump on its own is not a complete backup: recipe images live beside it. Keep the archive somewhere other than the machine it came from.

Logs.

docker compose logs -f app

Troubleshooting

The first page errors, or nothing loads. The schema was never created. Set RUN_MIGRATIONS=true in .env and restart, or run docker compose exec app home-ops-migrate.

"Password authentication failed" in the logs. POSTGRES_PASSWORD was changed after the first start. Postgres fixed it at initialisation. Either put the original back, or start over with a fresh volume (docker compose down -vthis deletes your data).

Locked out of the app. Five failed sign-ins for one username in fifteen minutes locks that name out; it clears itself after the window. The message for that is "Too many failed attempts", which is different from a wrong password.

Port already in use. Change WEB_PORT in .env.

Nothing at your LAN address, but localhost works. Add that address to APP_HOSTS and restart. Origins are explicit on purpose — the app never trusts the Host header to work out what it is called.

How it is built

Python 3.12 + FastAPI · PostgreSQL 16 · React 19 + TypeScript + Vite · SQLAlchemy 2 + Alembic · nginx, supervised by s6-overlay. No cloud services at runtime, no telemetry, no Redis.

One container holds all three processes with one volume at /data. nginx serves the built frontend and proxies /api to the application over loopback, so the browser only ever sees one origin and every URL in the app is relative.

Licence

MIT — see LICENSE.

apps.marketingCta.appInstallTitle

apps.marketingCta.appInstallDescription

apps.installHelp.stepOpen apps.installHelp.stepSearchApp apps.installHelp.stepReview apps.installHelp.stepInstall

apps.detail.sections.requirements

Set POSTGRES_PASSWORD before the first start — PostgreSQL bakes it into the data directory on first boot and changing it afterwards does not change the user. Put the appdata path on a cache pool rather than a /mnt/user fuse path.

apps.detail.sections.categories

apps.detail.sections.related

apps.detail.sections.details

apps.detail.details.repository
ghcr.io/g0browns/home-ops:latest
apps.detail.details.registry
apps.detail.details.lastUpdated2026-08-13
apps.detail.details.firstSeen2026-08-13

apps.detail.sections.runtime

apps.detail.details.webui
http://[IP]:[PORT:8080]
apps.detail.details.network
bridge
apps.detail.details.shell
bash
apps.detail.details.privileged
false

apps.detail.sections.configuration

WebUIPorttcp

Host port for the web interface. The container always listens on 8080. If you change the host port, set WEB_PORT below to the same number.

apps.detail.config.target
8080
apps.detail.config.default
8080
apps.detail.config.value
8080
DataPathrw

Everything persistent: the PostgreSQL cluster (/data/postgres) and uploaded recipe images (/data/uploads). Put this on a cache pool — e.g. /mnt/cache/appdata/home-ops — rather than a /mnt/user fuse path, which PostgreSQL does not tolerate well. A database dump alone is NOT a complete backup; back up this whole directory.

apps.detail.config.target
/data
apps.detail.config.default
/mnt/user/appdata/home-ops
apps.detail.config.value
/mnt/user/appdata/home-ops
POSTGRES_PASSWORDVariable

Password for the bundled database. SET THIS BEFORE THE FIRST START: PostgreSQL bakes it into the data directory on first boot, and changing it later does not re-create the user. Any characters are fine; it is escaped wherever it is used.

RUN_MIGRATIONSVariable

Apply database migrations on start. Leave this true. With it off the app starts but has no tables, and the first page fails. Turn it off only if you prefer to run 'home-ops-migrate' by hand from the container console before each upgrade.

apps.detail.config.default
true
apps.detail.config.value
true
APP_HOSTSVariable

Every hostname and IP you will open the app on over plain HTTP — your Unraid server's LAN address, a Tailscale name, and so on. NAMES ONLY: no scheme, no port, comma separated. Each becomes an allowed origin on WEB_PORT, which is what lets a browser at that address log in. Example: localhost,192.168.1.10,tower.local

apps.detail.config.default
localhost
apps.detail.config.value
localhost
WEB_PORTVariable

The HOST port you mapped above. It is not used to bind anything — it is used to build the allowed browser origins from APP_HOSTS. If this does not match the port in your address bar, sign-in fails on that address.

apps.detail.config.default
8080
apps.detail.config.value
8080
TZVariable

IANA timezone name, e.g. Europe/London or America/New_York. Used for log timestamps and scheduling. Never an offset like +01:00 — offsets cannot survive daylight saving.

apps.detail.config.default
UTC
apps.detail.config.value
UTC
PUBLIC_BASE_URLVariable

Your HTTPS address, if you put the app behind a reverse proxy or a Cloudflare Tunnel, e.g. https://home.example.com. It becomes an allowed origin and is used for links that leave the app. Leave blank if you have no proxy.

SESSION_COOKIE_SECUREVariable

Mark the session cookie Secure. Keep this false unless EVERY path to the app is HTTPS — a Secure cookie is silently dropped over plain HTTP, which would lock out your LAN address.

apps.detail.config.default
false
apps.detail.config.value
false
SETUP_ALLOW_TUNNEL_PATHVariable

Whether the first-run setup wizard may be completed over the public HTTPS path. While no user exists that page is unauthenticated by nature — whoever reaches it first becomes the administrator — so it is restricted to the LAN by default. It has no effect once any user exists.

apps.detail.config.default
false
apps.detail.config.value
false
LOG_LEVELVariable

CRITICAL, ERROR, WARNING, INFO or DEBUG.

apps.detail.config.default
INFO
apps.detail.config.value
INFO
UPLOAD_MAX_BYTESVariable

Largest single image upload accepted, in bytes, before decoding. Default is 10 MB.

apps.detail.config.default
10485760
apps.detail.config.value
10485760
APP_ENVVariable

Leave as production. 'development' enables the /api/docs page and verbose error responses.

apps.detail.config.default
production
apps.detail.config.value
production
TRUSTED_PROXY_IPSVariable

Peers whose X-Forwarded-For / X-Real-IP headers are honoured. nginx and the API talk over loopback inside this container, so 127.0.0.1/32 is the whole list. Empty means client IPs are all logged as 127.0.0.1.

apps.detail.config.default
127.0.0.1/32
apps.detail.config.value
127.0.0.1/32
DATABASE_URLVariable

Leave blank to use the PostgreSQL bundled in this container. Set it only to point at a PostgreSQL 16 server you run elsewhere (postgresql+psycopg://user:pass@host:5432/home_ops); the bundled one then simply goes unused. Percent-encode any @ in the password.

HEALTHCHECK_TOKENVariable

When set, GET /api/health/ready requires a matching X-Healthcheck-Token header. Leave blank on a private network. The plain /api/health liveness endpoint stays public either way and discloses nothing.