HortusFox forces every plant into a location, and the schema shows why
Self-hosted collaborative plant management and tracking system for plant enthusiasts
At a glance
- What is it?
- HortusFox is an MIT-licensed self-hosted PHP and MariaDB plant management system for households and plant-sharing groups, with tasks, inventory, a calendar and a REST API. Its central design decision, assigning every plant to a location, is what most of its features hang off.
- Who is it for?
- HortusFox fits a household or a plant-sharing group that wants its plant records on its own hardware with photo storage it controls, and that accepts MariaDB as the only supported database. It does not fit someone who wants a mobile-first capture app, since the project documents a web interface, a REST API and QR codes but no native client.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Per-location assignment is the schema, not a label
The README states the design decision early and builds everything on it. The system uses a per-location approach, which means plants are always assigned to locations. Not optionally, not as a category you can leave empty.
That single rule does a lot of work. Tasks, photos, notes, logs and the calendar all hang off a plant that hangs off a location, so "water everything on the third shelf" is a query rather than a convention you enforce by remembering. Custom locations are a listed feature, which suggests rooms, shelves or specific spots rather than a generic room field.
The feature list is long enough that it is worth sorting by kind. Care and record-keeping: plant management, history, photos, notes, logs. Scheduling: a tasks system, reminders, a calendar. Collection management beyond care: an inventory system. Social, because the project calls itself collaborative: a group chat. Then the operational set: search, weather, themes, profile management, an admin dashboard, backups, QR codes, a REST API, and plant identification.
Inventory and group chat are the two that most clearly push this past a single-user plant tracker. Inventory implies consumables and inventory counts rather than only living specimens, and group chat implies more than one household sharing one instance.
Docker Compose is the primary install path
The README leads installation with Docker and Docker Compose, requiring Docker and Docker Compose as prerequisites. Clone first:
git clone https://github.com/danielbrendel/hortusfox-web.git
cd hortusfox-webThen edit docker-compose.yml, which ships with two services, app and db. The app image is ghcr.io/danielbrendel/hortusfox-web:latest and publishes port 8080 to 80 on the host. The database is a plain mariadb image with restart: always.
The administrator credentials are environment variables on the app service, not a setup wizard:
APP_ADMIN_EMAIL: "[email protected]"
APP_ADMIN_PASSWORD: "password"
APP_TIMEZONE: "UTC"Database credentials are set on both services and must agree. The app connects over the internal network with DB_HOST: db and DB_PORT: 3306, while the db service also publishes 3306 to the host:
DB_HOST: db
DB_PORT: 3306
DB_DATABASE: hortusfox
DB_USERNAME: user
DB_PASSWORD: password
DB_CHARSET: "utf8mb4"After editing, start it:
docker compose pull
docker compose up -dThat same pair of commands is the documented upgrade path, which is a useful property: no separate migration step is described in the README, and the compose file mounts a named volume at /var/www/html/app/migrations so migration files survive a container replacement.
Six named volumes decide what survives a rebuild
The compose file declares six volumes, and each one marks a directory whose contents must not live inside the container image. Mapping them out tells you what the application treats as persistent state.
app_images maps to /var/www/html/public/img, so plant photographs are outside the image. app_attachments maps to /var/www/html/public/attachments, which is whatever else you attach to a plant record. app_logs maps to /var/www/html/app/logs. app_backup maps to /var/www/html/public/backup, which is where the backups feature writes. app_themes maps to /var/www/html/public/themes, so custom themes persist. app_migrate maps to /var/www/html/app/migrations.
The seventh, db_data at /var/lib/mysql on the database service, is the MariaDB data directory.
Two things follow. First, anything the application writes outside those six directories and outside the database is lost when the container is replaced, which is worth knowing before you install custom themes or drop files somewhere else under public/. Second, backing up requires copying these volumes as well as dumping the database; a database dump alone will not bring your photographs back.
The compose file also exposes 3306 on the host for the database alongside the internal app-to-db connection. That is convenient for a desktop client and unnecessary exposure for anything else.
Reverse proxy authentication and the trap in PROXY_OVERWRITE_VALUES
HortusFox accepts identity from a reverse proxy that already authenticated the user, and the configuration block is worth reading closely because of one mandatory flag:
PROXY_ENABLE: "true"
PROXY_HEADER_EMAIL: "Your e-mail header identifier here"
PROXY_HEADER_USERNAME: "Your username header identifier here"
PROXY_AUTO_SIGNUP: "true"
PROXY_WHITELIST: ""
PROXY_HIDE_LOGOUT: "true"
PROXY_OVERWRITE_VALUES: "true"The headers tell the app which upstream header carries the authenticated user's email and username. `PROXY_AUTO_SIGNUP` creates an account on first arrival, which is what makes single sign-on through an authenticating proxy work at all. `PROXY_WHITELIST` restricts it to a list of domains. `PROXY_HIDE_LOGOUT` removes the logout option, because in proxy mode there is nothing to log out of locally.
The trap is stated explicitly in the README: setting `PROXY_OVERWRITE_VALUES` to true is mandatory if you want to update your proxy authentication settings on container launch. Change the header names, leave that flag false, and the new values are ignored on restart with no obvious symptom beyond users failing to appear.
Enabling this means trusting whatever sits in front of the app. If the proxy is reachable without authentication, header injection is account takeover for anything the proxy protects, which is a configuration review item rather than a code one.
The web stack behind the .env settings
The repository is PHP with a custom framework in the asatru directory. .env.example names it Asatru PHP and carries the full application configuration, which is the best single file for understanding the deployment surface.
Version and identity first: APP_NAME is HortusFox, APP_VERSION reads 6.4 in the example, APP_LANG is en, and APP_WORKSPACE is a human-readable workspace name.
Then the knobs that change behaviour rather than branding. APP_DEBUG is true in the example file, which is right for development and wrong for an instance reachable from the internet. APP_OVERDUETASK_HOURS is 10, presumably the grace period before an unfinished task counts as overdue, and APP_CRONJOB_MAILLIMIT is 5, a cap on mail sent per cron run. There is a cronjobs section in the README's table of contents, so these two settings only take effect once cron is configured.
Sessions are configured explicitly: SESSION_ENABLE true, SESSION_DURATION set to 32532000 seconds, which is over a year, and SESSION_NAME left null so PHP picks a default. A year-long session lifetime is generous for an instance handling group chat and photos.
Photo handling is the one place with a numbered scale. PHOTO_RESIZE_FACTOR_DEFAULT is 1.0 and factors 1 through 5 descend 0.5, 0.4, 0.4, 0.3, 0.2, which is a ladder of derivative sizes generated once at upload rather than per request.
The database section is MariaDB and MySQL only: DB_DRIVER is mysql, DB_CHARSET is utf8mb4. There is no SQLite option in this file, which is a real deployment constraint.
The frontend is built with webpack, configured in webpack.config.js, with Sass, FontAwesome, axios, Chart.js and date-fns as dependencies. Charts and date adapters explain the calendar and history views.
External services, the installer path, and what the tests do not cover
Some features depend on services outside your instance, and the README's table of contents lists them: app icons, weather forecast, plant identification and plant information. The weather feature is therefore a call to an external forecast provider and plant identification is a call to an identification service, both of which mean plant photos leaving your network if you enable them. The README does not document which providers are used or what the API keys are, so that has to be read from the documentation site rather than assumed.
There is a second install route besides Docker: an integrated installer, activated by creating a file named do_install with no extension in the project root. The README is specific about the constraint, which is that you must not be running through the internal asatru development server, and suggests running from a real webserver environment such as XAMPP instead. So the manual route assumes a PHP-capable host and a document root, not a container.
The test story is the weak point, and it is visible in package.json rather than hidden. The test script is `echo "Error: no test specified" && exit 1`, which is the npm default and does nothing. There is also a .env.testing file and a phpunit.xml at the root, so a PHPUnit configuration exists alongside the Node build, but the npm script itself is a placeholder. Anyone planning to modify the codebase should not assume a green test suite is protecting them, because there is not one wired up through the documented commands.
Where a spreadsheet or a hosted app fits better
The alternatives fail in different ways, and the choice depends mostly on whether the collaboration is real.
A spreadsheet plus a photo folder is the honest baseline. It costs nothing, needs no server, and survives the maintainer losing interest. What it cannot do is the per-location query model, the recurring task schedule with overdue tracking, the calendar across many plants, or anything shared with another person without sending them the file. If you have six plants and one person, this is a real option and HortusFox is overkill.
A hosted plant app solves the maintenance problem: someone else runs the database, handles upgrades and backs it up. You give up some control over your data, and whether you retain API access depends on the hosted app. For a collection you care about long-term, that trade is often the wrong one, which is the reason self-hosted options like this exist at all.
General-purpose home management systems are the third comparison, and they cut the other way. They cover plants, fish and appliances in one place, which is what you want if the plant collection is one part of a house. HortusFox is narrow enough to model a plant well, with per-location assignment, care history, inventory and plant identification, and it will never grow a module for your boiler.
The REST API and QR codes are the extension points worth weighing. QR codes suggest printing a label per plant so you can scan it from the shelf, and the API means the group chat and tasks could be driven from something you write yourself.
Editorial conclusion
HortusFox fits a household or a plant-sharing group that wants its plant records on its own hardware with photo storage it controls, and that accepts MariaDB as the only supported database. It does not fit someone who wants a mobile-first capture app, since the project documents a web interface, a REST API and QR codes but no native client. Before deploying, change DB_PASSWORD and MARIADB_ROOT_PASSWORD from the example values, decide whether you want APP_DEBUG left on, and read what PROXY_AUTO_SIGNUP does in your reverse proxy, because open signup on a self-hosted instance is a decision you have to make explicitly.
Frequently asked questions
How do I install HortusFox with Docker Compose?
Clone the repository, set APP_ADMIN_EMAIL, APP_ADMIN_PASSWORD and the database credentials in docker-compose.yml, then run docker compose pull followed by docker compose up -d. The app is then reachable at http://localhost:8080 with the admin dashboard at /admin.
Which database does HortusFox use?
MariaDB, through the official mariadb image in docker-compose.yml, connected to the app as the db service on port 3306 with charset utf8mb4. The .env.example file sets DB_DRIVER to mysql and offers no alternative driver.
How do I upgrade HortusFox to a new version?
The README states that running docker compose pull and then docker compose up -d is the correct way to upgrade to a new version. Persistent state lives in named volumes for images, attachments, logs, backups, themes and migrations, plus the database volume.
What is the per-location approach in HortusFox?
The README states the system is designed using a per-location approach, meaning plants are always assigned to locations. Custom locations are a listed feature, and the tasks, calendar and history features build on that assignment.
Does HortusFox have a REST API?
Yes. A REST API is listed among the features, with the README stating developers can use the integrated API to create third-party software. QR codes are also listed, which supports printing labels for individual plants.
Official sources
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.
[](https://hysenlabs.com/projects/danielbrendel-hortusfox-web)Was this useful?