How to self-host Mealie on your own VPS, no Docker
Run Mealie v3.9.2, the recipe manager, on your own Ubuntu server with ox: a FastAPI backend, a Nuxt front end and Postgres, from the config that ran.
To self-host Mealie you need an Ubuntu server, a fork of the Mealie repo with an ox.toml and one small Python script added, and eleven variables: ten you copy as they are and the admin's password, which you choose. In our run the first deploy took about 4 minutes, and ox set up Postgres for it.
How we tested On Nov 4, 2026, ox deployed Mealie v3.9.2 from commit 70b5865d on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, created a recipe and saw it listed after a reload, and checked the app's services and its backup.
Mealie is a recipe manager, meal planner and shopping list for a household. You add a recipe by typing it in or by giving Mealie the address of a recipe page, then plan the week and send ingredients to a shopping list. The license is AGPL-3.0.
The backend is a FastAPI app. The front end is a Vue app that Nuxt builds into plain files, and the backend serves those files itself, so Mealie runs as a single process. Mealie stores its data in SQLite or PostgreSQL, and the run used Postgres.
ox deployed Mealie v3.9.2 (commit 70b5865d) on Nov 4, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 4 minutes and a redeploy took 2 minutes.
- First deploy: 4 minutes, from adding the project until the app passed the run's first checks.
- Redeploy of the same commit: 2 minutes, until the app passed the checks again.
- Server: DigitalOcean, 8 vCPUs and 16 GB of memory (
s-8vcpu-16gb), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - Memory in use after the reboot: 1183 MB of 15986 MB, for the whole server: Ubuntu, the app and its staging copy, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less.
- Services ox runs for it:
postgres. - Source: github.com/mealie-recipes/mealie at v3.9.2, commit
70b5865d.


What you need
An Ubuntu LTS server with root access. The facts above name the size the run used.
A GitHub account to fork
mealie-recipes/mealie, and an ox account, which you open by signing in with GitHub.A password for Mealie's first admin. Mealie makes the admin itself with a default password its docs publish, and the script replaces that password with yours before the site serves anyone.
A mail server, only if you want invites and password resets by email. The run set no mail, so email was not tested. Importing a recipe from a web address was not tested either.
The steps
In the ox dashboard, press Add a server and run the install command it prints, as root, on your server. It installs only the ox agent and Caddy.
Fork mealie-recipes/mealie on GitHub and check out commit 70b5865d, the v3.9.2 code the run deployed. Put the ox.toml shown below at the root of the fork and the script shown below at ox/admin_password.py, then commit and push. Next write mealie.env on your laptop with the eleven keys from the variables section: ten with the value given there, and MEALIE_ADMIN_PASSWORD with a password of your own. With the ox CLI signed in, run:
ox new <you>/mealie
ox review mealie --from-file mealie.env --waitThe project takes the repo's name. The review saves your values and starts the first deploy, and --wait prints the log until it ends. The quickstart shows the same two steps in the browser.
When the deploy is green, open the project's address and sign in as changeme@example.com, Mealie's first admin, with the password you set in MEALIE_ADMIN_PASSWORD. The test agent tried Mealie's published default password first, which Mealie refused, and then signed in with the one from the variables.
The first sign-in opens a First Time Setup page. Its steps were not tested. Mealie shows a notice until you change the admin's email in your user settings, and changing it was not tested.
To try the app, press Create in the left menu, choose Create a recipe manually, type a name and create it. The test agent made a tomato soup this way, reloaded the page, and found it listed under Recipes.
The run served Mealie on its preview address only. A domain of your own was not part of it, and the domains page covers how ox adds one.
The ox.toml, line by line
# Mealie v3.9.2 (FastAPI + a Nuxt SPA it serves itself), run outside
# Docker the way docker/Dockerfile and docker/entry.sh build and run it.
# The admin's password: ox/admin_password.py sets it in migrate.
required_variables = ["MEALIE_ADMIN_PASSWORD"]
[app]
# The `mealie` entry point runs uvicorn on API_HOST and API_PORT.
start = "API_HOST=127.0.0.1 API_PORT=$PORT exec uv run mealie"
# Mealie's own about endpoint, the one its Docker healthcheck calls.
health = "/api/app/about"
[build]
# The pgsql extra carries psycopg2, as the Dockerfile's export does.
install = "uv sync --frozen --no-dev --extra pgsql"
# The Dockerfile's frontend stage, copied where Mealie serves the SPA from.
commands = [
"cd frontend && yarn install --frozen-lockfile --non-interactive && yarn generate",
"rm -rf mealie/frontend && cp -r frontend/dist mealie/frontend",
]
# The migrations the app also runs at start, then the seeded admin's
# password from MEALIE_ADMIN_PASSWORD (ox/admin_password.py), in the venv
# uv sync made.
migrate = ".venv/bin/python ox/admin_password.py"
[tools]
# The Dockerfile's versions: node 24, python 3.12 (requires-python <3.13).
node = "24"
"aqua:yarnpkg/yarn" = "1.22.22"
python = "3.12"
[services]
# Mealie runs on SQLite or PostgreSQL; PostgreSQL for a hosted install.
postgres = {}The header says what the file is: Mealie run outside Docker, the way its docker/Dockerfile and docker/entry.sh build and start it.
[app] startrunsmealie, the commandpyproject.tomlmaps to Mealie's uvicorn launcher. Left alone it listens on every interface at port 9000, so the line setsAPI_HOSTto the loopback address andAPI_PORTto the port ox gives it, with Caddy in front.healthis/api/app/about, the address Mealie's own Docker health check calls. ox sends visitors to a new release only after it answers.installrunsuv syncwith--frozen, so the versions inuv.lockinstall as written, and--no-dev, which skips the development tools.--extra pgsqladds psycopg2, the Postgres driver, as the Docker image does.The two build
commandsare the Dockerfile's front end stage. The first installs the packages inyarn.lockand runsyarn generate, which turns the Nuxt app into static files infrontend/dist. The second copies them tomealie/frontend, the folder Mealie serves its pages from.[tools]pins node 24, the Dockerfile's front end image, and Yarn 1.22.22, the last Yarn 1 release, wherepackage.jsonnames Yarn 1. Mealie'srequires-pythonallows 3.12 only, and ox's default Python is newer, so the file pins 3.12.[services]asks for Postgres.required_variablesnamesMEALIE_ADMIN_PASSWORD, so ox asks for it before the first deploy andox checklists it.migrateruns the script below with the Python thatuv syncset up. ox runs it after the build and before traffic switches to the new release. Once the database has tables, ox dumps it first.
ox/admin_password.py
"""Mealie's first admin with the password MEALIE_ADMIN_PASSWORD, so no
visitor can sign in with the documented default. Mealie seeds the user
changeme@example.com with the password MyPassword (mealie/repos/seed/
init_users.py), which its docs tell the operator to change after the first
sign-in; this sets it the way mealie/scripts/change_password.py does. Safe to
rerun: a password already changed is left alone."""
import os
from mealie.core.security.providers.credentials_provider import CredentialsProvider
from mealie.core.security.security import hash_password
from mealie.db import init_db
from mealie.db.db_setup import session_context
from mealie.repos.repository_factory import AllRepositories
# init_db.main is what the app runs at start: the migrations, then the
# seeded group, household and admin when the database has no user.
init_db.main()
with session_context() as session:
repos = AllRepositories(session, group_id=None, household_id=None)
user = repos.users.get_one("changeme@example.com", "email")
if user and CredentialsProvider.verify_password("MyPassword", user.password):
repos.users.update_password(user.id, hash_password(os.environ["MEALIE_ADMIN_PASSWORD"]))
print("Set the password of the Mealie admin changeme@example.com.")
else:
print("The Mealie admin's password is already changed: left alone.")The script first runs Mealie's own init_db, which the app also runs at start: it upgrades the tables and, on an empty database, makes the admin changeme@example.com with Mealie's default password. If the admin still has that default password, the script sets yours, the way Mealie's change_password.py does. A password already changed is left alone, so later deploys do nothing and changing MEALIE_ADMIN_PASSWORD afterwards does not change the account. The script never prints the password.
There is no worker and no cron entry. Mealie's scheduler runs inside the web process. No [storage] line is needed: DATA_DIR points at ox's data folder, which already lives outside the release.
PRODUCTION
NLTK_DATA
DB_ENGINE
POSTGRES_SERVER
POSTGRES_PORT
POSTGRES_USER
POSTGRES_PASSWORD
POSTGRES_DB
BASE_URL
DATA_DIR
MEALIE_ADMIN_PASSWORDPRODUCTIONistrue, as the Dockerfile sets it. In production mode Mealie serves the front end files and, on its first start, writes random.secretand.session_secretfiles into its data folder. WithPRODUCTIONfalse it uses a fixed test secret, so no secret of yours goes in the variables.DB_ENGINEispostgres, which switches Mealie from its default, SQLite.POSTGRES_SERVER,POSTGRES_PORT,POSTGRES_USER,POSTGRES_PASSWORDandPOSTGRES_DBare${postgres.host},${postgres.port},${postgres.user},${postgres.password}and${postgres.database}, references to the Postgres service.BASE_URLis${PUBLIC_URL}, a reference ox fills in with the project's address. Mealie's docs say it is used for notifications.DATA_DIRis${OX_DATA_DIR}, ox's data folder for the project. Mealie keeps recipe images, its own backup files and the two secret files there, and the folder survives every deploy.MEALIE_ADMIN_PASSWORDis the first admin's password, and you choose it. Mealie itself does not read it, only the migrate script does.NLTK_DATAis${OX_DATA_DIR}too. Mealie's ingredient parser needs a small language model from NLTK. It looks for the model when the app loads and downloads it if it is missing, so the first start needs internet access. The download goes into the data folder, and later starts find it there.
See variables for how references work.
Postgres and the data folder
Postgres is shared with other apps on the server, and Mealie gets a database and a login role of its own. On Postgres, Mealie creates the pg_trgm extension itself at start, for fuzzy search. The staging copy the run deployed beside production has its own database, and the memory figure above counts both.
Updates and backups
To update, merge a newer Mealie release into your fork and push, or run ox deploy mealie --wait. ox builds the new release beside the running one and switches when /api/app/about answers. The run redeployed the same commit, and a newer version was not tested. Mealie's update notes say to read the release notes first.
Backups need no setup. The run's backup, made with Back up now, was a dump of Mealie's Postgres database, and pg_restore could read it. Run ox backups mealie to list it. ox also made a dump before each migrate step after the first deploy, and the list held those too. Recipe images live in the data folder, which no entry covered, and the run added no images. Mealie's own zip backups and a restore were not tested. The backups page covers keeping copies in an S3 bucket.
Frequently asked questions
Do I need Docker to self-host Mealie?
No. Mealie's docs steer you to its Docker image, but the app is a Python package with a front end built into static files. ox installed Python 3.12 and node 24 on the server, built both from your fork, and ran the mealie command as a system service beside Postgres.
What is Mealie's default login?
A new install has one admin with the email changeme@example.com and a default password that Mealie's docs publish. With ox, the migrate script swaps that password for MEALIE_ADMIN_PASSWORD before the site serves anyone, so you sign in as changeme@example.com with your own password. The run checked that the default password was refused. Changing the admin's email was not tested.
Which database does Mealie need?
SQLite or PostgreSQL. SQLite is the default and needs nothing else. The run set DB_ENGINE to postgres and five POSTGRES_ keys that point at the Postgres ox set up for the project, and Mealie made its tables there when it started. Mealie's docs say fuzzy search works only on Postgres, and search was not tested.
Does Mealie need an email server?
Only for account invites and password resets, Mealie's docs say. The mail settings are SMTP_HOST, SMTP_PORT and a few more. The run set none and Mealie ran without them: sign-in, creating a recipe and the backup all worked. Sending mail itself was not tested.
How much memory does Mealie use?
The facts above give what the whole server used after the run's reboot, with Mealie, its staging copy, Postgres, Caddy and the ox agent running. ox accepts any server from 1 GB up, and names memory when that is why Mealie stopped.
Updated : a new run on Nov 4. The fork now sets the first admin's password from MEALIE_ADMIN_PASSWORD before the site serves anyone, so Mealie's published default no longer works, and the timings and memory are the new run's.
New to running apps on your own server? How ox runs your apps shows what ox sets up on your server.
