[Blog](https://console.t12.sorv.dev/blog) [Guides](https://console.t12.sorv.dev/blog#c-guides) # 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. Saurav Sharma Oct 17, 2026 10 min read On this page - [What you need](https://console.t12.sorv.dev/blog/self-host-mealie#what-you-need) - [The steps](https://console.t12.sorv.dev/blog/self-host-mealie#steps) - [The ox.toml, line by line](https://console.t12.sorv.dev/blog/self-host-mealie#config) - [Postgres and the data folder](https://console.t12.sorv.dev/blog/self-host-mealie#services) - [Updates and backups](https://console.t12.sorv.dev/blog/self-host-mealie#updates) - [Frequently asked questions](https://console.t12.sorv.dev/blog/self-host-mealie#faq) 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`](https://github.com/mealie-recipes/mealie/commit/70b5865dce88c9ddeef4d83155fb794bb599bc57) 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](https://github.com/mealie-recipes/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](https://github.com/mealie-recipes/mealie) at v3.9.2, commit [`70b5865d`](https://github.com/mealie-recipes/mealie/commit/70b5865dce88c9ddeef4d83155fb794bb599bc57). The empty Mealie recipes page after signing in The Mealie recipes page with the new recipe Verify Tomato Soup ## 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](https://console.t12.sorv.dev/docs/cli) signed in, run: Shell ```bash ox new /mealie ox review mealie --from-file mealie.env --wait ``` The 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](https://console.t12.sorv.dev/docs/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](https://console.t12.sorv.dev/docs/operate/domains) covers how ox adds one. ## The ox.toml, line by line TOML ```toml # 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] start` runs `mealie`, the command `pyproject.toml` maps to Mealie's uvicorn launcher. Left alone it listens on every interface at port 9000, so the line sets `API_HOST` to the loopback address and `API_PORT` to the port ox gives it, with Caddy in front. - `health` is `/api/app/about`, the address Mealie's own Docker health check calls. ox sends visitors to a new release only after it answers. - `install` runs `uv sync` with `--frozen`, so the versions in `uv.lock` install as written, and `--no-dev`, which skips the development tools. `--extra pgsql` adds psycopg2, the Postgres driver, as the Docker image does. - The two build `commands` are the Dockerfile's front end stage. The first installs the packages in `yarn.lock` and runs `yarn generate`, which turns the Nuxt app into static files in `frontend/dist`. The second copies them to `mealie/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, where `package.json` names Yarn 1. Mealie's `requires-python` allows 3.12 only, and ox's default Python is newer, so the file pins 3.12. - `[services]` asks for Postgres. - `required_variables` names `MEALIE_ADMIN_PASSWORD`, so ox asks for it before the first deploy and `ox check` lists it. - `migrate` runs the script below with the Python that `uv sync` set 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` Code ``` """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. Shell ```bash PRODUCTION NLTK_DATA DB_ENGINE POSTGRES_SERVER POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB BASE_URL DATA_DIR MEALIE_ADMIN_PASSWORD ``` - `PRODUCTION` is `true`, as the Dockerfile sets it. In production mode Mealie serves the front end files and, on its first start, writes random `.secret` and `.session_secret` files into its data folder. With `PRODUCTION` false it uses a fixed test secret, so no secret of yours goes in the variables. - `DB_ENGINE` is `postgres`, which switches Mealie from its default, SQLite. `POSTGRES_SERVER`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_DB` are `${postgres.host}`, `${postgres.port}`, `${postgres.user}`, `${postgres.password}` and `${postgres.database}`, references to the Postgres service. - `BASE_URL` is `${PUBLIC_URL}`, a reference ox fills in with the project's address. Mealie's docs say it is used for notifications. - `DATA_DIR` is `${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_PASSWORD` is the first admin's password, and you choose it. Mealie itself does not read it, only the migrate script does. - `NLTK_DATA` is `${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](https://console.t12.sorv.dev/docs/operate/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](https://console.t12.sorv.dev/docs/operate/backups) 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 Nov 4, 2026: 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](https://console.t12.sorv.dev/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://console.t12.sorv.dev/blog/self-host-mealie#what-you-need) - [The steps](https://console.t12.sorv.dev/blog/self-host-mealie#steps) - [The ox.toml, line by line](https://console.t12.sorv.dev/blog/self-host-mealie#config) - [Postgres and the data folder](https://console.t12.sorv.dev/blog/self-host-mealie#services) - [Updates and backups](https://console.t12.sorv.dev/blog/self-host-mealie#updates) - [Frequently asked questions](https://console.t12.sorv.dev/blog/self-host-mealie#faq) - [Do I need Docker to self-host Mealie?](https://console.t12.sorv.dev/blog/self-host-mealie#faq-docker) - [What is Mealie's default login?](https://console.t12.sorv.dev/blog/self-host-mealie#faq-login) - [Which database does Mealie need?](https://console.t12.sorv.dev/blog/self-host-mealie#faq-database) - [Does Mealie need an email server?](https://console.t12.sorv.dev/blog/self-host-mealie#faq-email) - [How much memory does Mealie use?](https://console.t12.sorv.dev/blog/self-host-mealie#faq-memory) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host Meilisearch on your VPS without Docker](https://console.t12.sorv.dev/blog/self-host-meilisearch) [Newer post How to self-host Maybe without Docker on your own VPS](https://console.t12.sorv.dev/blog/self-host-maybe) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://console.t12.sorv.dev/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 17 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://console.t12.sorv.dev/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://console.t12.sorv.dev/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://console.t12.sorv.dev/login/github) [Read the docs](https://console.t12.sorv.dev/docs)