Self-hosted service
afairgiant/MediKeep avatar
afairgiant/MediKeep

MediKeep: a self-hosted keeper for your own medical records

A lightweight, self-hosted application for managing your personal medical information. Keep your health records organized and accessible while maintaining complete control over your data privacy.

547 stars47 forksPythonNOASSERTION

At a glance

What is it?
MediKeep is a React and FastAPI application for storing your own health records on your own hardware, and what makes it interesting to read is the operational detail: a single port for both halves of the app, Docker secrets for every credential, and an explicit control on whether the app may reach private addresses.
Who is it for?
MediKeep is the right tool for a person who wants their own medical records stored on their own machine and who is comfortable running a Docker Compose stack, especially where a hosted service would be unacceptable. It is the wrong tool for a clinic, where a multi user system with a proper audit trail is needed, and the wrong choice if you need the data to move in a standard clinical format, since the project describes no import or export in an interoperable format.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the application is, and the stack behind it

MediKeep is described as a lightweight, self-hosted application for managing your personal medical information, keeping health records organised and accessible while maintaining complete control over data privacy. The one line summary in the readme is that it is your personal health record keeper, built with a React frontend and a FastAPI backend.

The feature set visible in the documentation is modest and clinical. The dashboard gives an overview of your health records and recent activity. The medications page lets you track and manage all your medications, dosages and schedules in one place. And the report builder generates custom health reports and exports your medical data so it can be shared with healthcare providers, which is the feature that turns a personal database into something a doctor can read.

The back end is a FastAPI application on PostgreSQL with Alembic for migrations, and the front end is a React app built with Vite and Tailwind. The requirements file pins the whole stack tightly, which is the right choice for a self-hosted application where the operator controls the environment: FastAPI and Uvicorn for the server, SQLAlchemy and psycopg2 for the database layer, Pydantic for the schemas, PyJWT and bcrypt for authentication, and Jinja2 and Mako for templating.

There are also three dependencies that reveal what the application does beyond storing rows. APScheduler is there for background task scheduling and auto-backups, Apprise is a multi-channel notification library, and the PDF stack of pdfplumber, pdf2image and pytesseract is for text extraction and OCR, which is how a scanned document in a records upload becomes searchable data.

The rename broke your compose file, and the readme says so

The first thing the readme does is warn about a rename, and the warning is unusually specific, which tells you how many people were bitten by it.

The project was formerly known as Personal-Medical-Records-Keeper and has been renamed to MediKeep. The breaking changes are listed in three parts. The Docker image has moved from the personal medical records keeper path to ghcr.io/afairgiant/medikeep. The repository will move from afairgiant/Personal-Medical-Records-Keeper to afairgiant/MediKeep. And the container names have changed from the medical-records prefix to the medikeep prefix.

The instruction is to update your configurations accordingly.

This is a good example of a migration that the project could not avoid. The old name was descriptive and the new one is short, but the cost lands on every deployment that pinned the old image tag, and the two resource naming changes, the image path and the container names, break a compose file in two independent places. If you are adopting the project now you get the new names, and if you are upgrading from the old one, the readme is precise enough to make the change mechanical.

The badge links in the readme still point at the old repository path, which is a small inconsistency in a document whose entire purpose at the top of the page is to tell you the paths have changed.

One port for the React app and the FastAPI API

The compose file is where the deployment design becomes clear, and the first thing to notice is that the application is a single service with a single port.

The app service is built from the published image, and the port mapping exposes a single port that serves both the React application and the FastAPI backend. The comment in the file says so explicitly. In practice the browser loads the built front end from the same origin that serves the API, so there is no separate frontend host, no CORS configuration to get wrong and no second container to keep in step.

The environment block is where the configuration surface shows.

yaml
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: ${DB_NAME:-medical_records}
      DB_USER: ${DB_USER:-medapp}
      DB_PASSWORD: ${DB_PASSWORD}
      SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in .env or Docker secrets for persistent JWTs}
      DEBUG: ${DEBUG:-false}
      ENABLE_API_DOCS: ${ENABLE_API_DOCS:-false}
      LOG_LEVEL: ${LOG_LEVEL:-INFO}
      TZ: ${TZ:-America/New_York}

Two of those lines carry decisions rather than defaults. The SECRET_KEY uses the compose required variable syntax, so the stack refuses to start without it, and the comment explains why it matters: it is used for persistent JWTs, so losing it invalidates every issued token. And ENABLE_API_DOCS defaults to false, which means the interactive API documentation is opt in, a sensible default for something holding medical data.

Around the environment block sit the operational details. The database service runs postgres:15.8-alpine with a comment that PostgreSQL 15 to 18 are supported and that changing the major version on an existing volume requires pg_upgrade. The application waits for the database to report healthy before it starts, and its own health check curls the /health endpoint on port 8000. Named volumes hold the database, the uploads, the logs and the backups, and the whole stack sits on a medikeep-network.

Secrets, single sign-on and the private address control

Three security decisions in the compose file are worth reading closely, because they are the difference between a self-hosted health record store that is safe to put on a network and one that is not.

The first is secrets. Every credential in the environment block has a Docker secrets counterpart with the _FILE suffix, so you can point the application at mounted files instead of passing values as plain environment variables, and the file comments are explicit that when you use the _FILE form you remove the corresponding plain variable. The secrets block at the bottom is present but commented out, ready for Docker Swarm or compose secrets. That is a well thought out default for a project that handles sensitive data.

The second is single sign-on, which is disabled by default and configured with an OIDC provider type, a client id, a client secret, an issuer URL, a redirect URI and an allowed domains list. Recent releases have been about exactly this area, with 0.70.0 described as SSO improvements and 0.71.0 as insurance and security updates.

The third is the one that most concerns me, and it is easy to miss because it is a comment rather than a setting. The environment has an ALLOW_PRIVATE_INTEGRATION_URLS switch, commented out, defaulting to true, and the comment explains that integration URLs pointing at private LAN addresses, for things like Paperless or Papra, are allowed by default, that it should be set to false on internet exposed instances to restrict to public URLs, and that the 169.254.x cloud metadata range is always blocked.

The cloud metadata protection is a good sign, since that is the address range used for instance credentials. But the default of allowing private addresses means an operator who exposes this stack without reading the comment has an SSRF path to their own network, and that is the first thing to change on any public deployment.

The security tooling in the project configuration

The Python project configuration is where you can see how seriously the project takes the static analysis side of security, and it is worth reading because these settings are usually the first thing a security reviewer asks about.

toml
[tool.bandit]
exclude_dirs = ["tests", ".venv", "node_modules", "frontend", "dist", "build", "alembic"]
skips = [
    "B101",  # assert usage - acceptable in application code
    "B110",  # try/except/pass - common Python pattern
    "B112",  # try/except/continue - common Python pattern
    "B404",  # subprocess import - flagging imports alone is not actionable
]

The Bandit configuration excludes the directories where findings are not actionable, and every skipped rule carries a comment saying why. There are notes that the scan targets must be passed on the command line rather than configured, and that the blocking CI run uses the medium and high severity and confidence levels while SARIF output captures the remaining findings, which is the usual way to keep a security gate honest without drowning in noise.

Pylint is configured alongside it, with a set of ignore paths expressed as regular expressions, all CPU cores enabled, and a maximum line length of 120. The disabled rule list is long and every entry is justified in a comment, and the reasoning is consistent: stylistic rules that clash with framework conventions are turned off, while the bug finding rules such as unused imports, undefined variables and raise without from are left enabled. isort is set to the black profile with a line length of 120 to match.

Taken together, this is the profile of a project where the security posture is a stated goal rather than an afterthought, which is a reasonable expectation for software whose entire purpose is holding medical records.

Where MediKeep is the wrong tool

Four limits, and the first one is about who the software is for.

This is a personal record keeper, and the readme says so repeatedly, in the project description and in the tagline. A clinic with several staff, a duty roster and a requirement for a defensible audit trail needs a different class of system, and MediKeep's design decisions, one local database, one administrator, one report builder, do not stretch to that. The permissions model in the compose file is a single administrator password.

The second is interoperability, and it is the gap that would matter most to a user trying to move records in from a hospital. The project describes storing records, generating reports and exporting data for sharing with providers, but it names no import format, no export format, and no clinical standard such as FHIR or HL7. A report that a clinician can read is not the same as a file that another system can ingest, and the distinction matters the moment you leave one system and enter another.

The third is that the defaults are tuned for a home network, and the readme says as much in the environment comments. Private integration URLs are allowed by default, the API documentation is off by default which is right, and the administrator password defaults to admin123 inside the application when the variable is not set, which is fine on a private network and unacceptable on a public one.

The fourth is the licence. The licence field is shown as other, described as a custom licence that GitHub cannot classify, and the file itself is the only authoritative statement. For software handling health data, a licence that nobody can summarise in a sentence is a procurement problem, and that is a question to settle before deployment rather than after.

Licence, releases and how the project is moving

The licence situation is the first thing to sort out, because it is the one place where the metadata is deliberately unhelpful. The licence field is listed as other, described as a custom licence GitHub cannot classify, with a pointer to the LICENSE file. The readme itself does not summarise the terms, and it is worth asking the maintainers what they are before you deploy.

On the project itself, the readme says it is actively being worked on, and the release history supports that. Version 0.70.0 was published on 2026-09-11 with SSO improvements and bug fixes, 0.71.0 on 2026-09-22 with insurance and security updates, and 0.72.0 on 2026-10-04 with bug fixes. The last push to the default branch was on 2026-10-20, and the repository is not archived.

So the cadence is roughly fortnightly at version 0.7x, which is quick for software that stores medical data, and the release titles are full of emoji, which tells you the tone of the project as well as the schedule.

The repository layout supports a serious reading of the project. There is an app directory for the backend, a frontend directory for the React client, and a shared directory between them, plus config, alembic for migrations with its own alembic.ini, a tests directory, a scripts directory, a docker directory, and a docs directory. A medikeep.spec file sits at the root for packaging the application as a standalone binary, pytest.ini and conftest.py are present for the test suite, and the release configuration is in .releaserc.json.

For a self-hosted user, the documentation is on the MediKeep wiki with a User Guide, an Admin Guide and a Developer Guide, which is the right place to look before you copy a compose file into production.

Editorial conclusion

MediKeep is the right tool for a person who wants their own medical records stored on their own machine and who is comfortable running a Docker Compose stack, especially where a hosted service would be unacceptable. It is the wrong tool for a clinic, where a multi user system with a proper audit trail is needed, and the wrong choice if you need the data to move in a standard clinical format, since the project describes no import or export in an interoperable format. Before deploying it, change the default administrator password, set ALLOW_PRIVATE_INTEGRATION_URLS to false on any internet facing instance, and read the LICENSE file, because the project does not use a standard open source licence.

Frequently asked questions

What is MediKeep?

It is a lightweight, self-hosted application for managing personal medical information, built with a React frontend and a FastAPI backend, and its stated purpose is to keep health records organised and accessible while the data stays under your own control. The documented features are a dashboard, medication tracking with dosages and schedules, and a report builder for exporting data to share with providers.

How do I deploy MediKeep?

With Docker and Docker Compose, using the compose file from the readme. It runs a PostgreSQL service and an application service built from the ghcr.io/afairgiant/medikeep image, with the database, uploads, logs and backups in named volumes and a single application port serving both the React app and the API.

What changed when the project was renamed to MediKeep?

The Docker image moved to ghcr.io/afairgiant/medikeep, the repository moves from afairgiant/Personal-Medical-Records-Keeper to afairgiant/MediKeep, and the container names changed from the medical-records prefix to medikeep. The readme asks existing users to update their configurations accordingly.

What security options does the MediKeep compose file offer?

Every credential has a Docker secrets counterpart with the _FILE suffix, single sign-on with an OIDC provider can be enabled, and the SECRET_KEY is a required variable so the stack refuses to start without it. There is also a switch for whether the application may reach private integration URLs, which is allowed by default and should be turned off on an internet facing instance.

Official sources

  1. afairgiant/MediKeep on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/afairgiant-medikeep.svg)](https://hysenlabs.com/projects/afairgiant-medikeep)

Was this useful?