[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host Firefly III on your own Ubuntu VPS Run Firefly III v6.7.9 on your own Ubuntu server with ox: PHP, MariaDB, an hourly cron job and a first owner made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 12 min read On this page - [What you need](https://deploywithox.com/blog/self-host-firefly-iii#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-firefly-iii#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-firefly-iii#config) - [The database and the timer](https://deploywithox.com/blog/self-host-firefly-iii#services) - [Updates and backups](https://deploywithox.com/blog/self-host-firefly-iii#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-firefly-iii#faq) To self-host Firefly III you need an Ubuntu server, a fork of its repo with an `ox.toml` and two small PHP files added, and 14 variables, six of them for the database ox sets up. In our run the first deploy took about 2 minutes, and ox set up MariaDB and an hourly timer for Firefly III's cron command. **How we tested** On Oct 17, 2026, ox deployed Firefly III v6.7.9 from commit [`ba267686`](https://github.com/firefly-iii/firefly-iii/commit/ba26768619842a4926655f315716f1b7788933d7) 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, made a bank account and a withdrawal from it, and saw the withdrawal under Expenses after a reload, and checked the app's services and its backup. [Firefly III](https://github.com/firefly-iii/firefly-iii) is a personal finance manager. You record your bank accounts, spending and income, and it shows balances and reports. It also has budgets, recurring transactions, rules, two-factor sign-in and a REST API. The code is free software under the AGPL, version 3 or later. It is a PHP app built on Laravel. Transactions live in a database, and logs, uploads and keys live in its `storage` folder. ox deployed Firefly III v6.7.9 (commit ba267686) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 2 minutes and a redeploy took 47 seconds. - **First deploy:** 2 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 47 seconds, 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:** 1006 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:** `mysql`. - **Scheduled jobs:** `firefly`. - **Source:** [github.com/firefly-iii/firefly-iii](https://github.com/firefly-iii/firefly-iii) at v6.7.9, commit [`ba267686`](https://github.com/firefly-iii/firefly-iii/commit/ba26768619842a4926655f315716f1b7788933d7). The Firefly III sign-in page The Firefly III Getting started form after the first sign-in The Firefly III asset accounts list with Test Bank at 958.00 EUR after the 42 EUR expense ## What you need - An Ubuntu LTS server. The run used the size in the facts above and measured the memory there. Firefly III's `composer.json` asks for PHP 8.5 or newer, and the config's first comment says ox detects PHP 8.5 and FrankenPHP. - A GitHub account to fork `firefly-iii/firefly-iii`, and an ox account, which you open by signing in with GitHub. - An email address such as `you@example.com` and a password of at least 16 characters, for the first account. Mail was not tested: the run left `MAIL_MAILER` on `log`, so messages go to the app's log. ## The steps Open the ox dashboard and press Add a server. The card gives you a command that works once. Paste it into a root shell on the server. It sets up the ox agent and Caddy. Fork `firefly-iii/firefly-iii` and check out the tag `v6.7.9`, commit `ba267686`, the one the run deployed. Put `ox.toml`, `ox-owner.php` and `ox-translations.php` at the root of the fork, as shown below, then commit and push. Write `firefly.env` on your laptop with one `KEY=value` line for each key in the variables section. For `APP_KEY`, Firefly III's example file asks for a random string of exactly 32 characters. The example file also lists keys for features this setup does not use, such as Papertrail, Pusher, Mailgun, Redis and the demo site. ox asks for each one, so the run marked them Not needed with `unset KEY` lines, and `ox review` stops and names any you leave out. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /firefly-iii ox review firefly-iii --from-file firefly.env --wait ``` ox takes the project name from the repo. The review stores your variables and begins the first deploy, while `--wait` prints the log until the run ends. The [quickstart](https://deploywithox.com/docs/quickstart) shows the same two steps in the browser. When the deploy succeeds, open the address ox shows for the project and go to `/login`. Sign in with `ADMIN_EMAIL` and `ADMIN_PASSWORD`. You land on Getting started. Type a bank name and a balance and submit, and that is your first asset account. To log spending, press the plus icon in the top bar, choose Create new withdrawal, fill in the form and submit. After a reload the withdrawal is listed under Expenses and the bank's balance is lower. The run served Firefly III on its preview address only. A domain of your own was not part of it, and the [domains page](https://deploywithox.com/docs/operate/domains) covers how ox adds one. ## The ox.toml, line by line TOML ```toml # Firefly III v6.7.9 (Laravel 12) as its operator would run it: ox detects # the Composer and npm installs, FrankenPHP and PHP 8.5. The rest follows # the project's own recipe: the release workflow (.github/workflows/ # release.yml) for the build and its Docker image's entrypoint for the # database steps. # The owner ox-owner.php registers (see migrate): Firefly III makes its # first account only through a /register page that closes after it. required_variables = ["ADMIN_EMAIL", "ADMIN_PASSWORD"] [build] # The git tag holds no public/build and no public/v3/i18n/*.json; the # release workflow makes both, and every page loads them. The first two # steps are the detected ones (the v3 Vite build and Laravel's storage # link); ox-translations.php does what the workflow's # `ff3:json-translations v3` step does. commands = [ "cd resources/assets/v3 && npm run build", "ln -sfn ../storage/app/public public/storage", "php ox-translations.php", ] # The Docker entrypoint's steps, in its order: upgrade-database migrates and # seeds, runs the upgrades and stamps the build time (until then every page # redirects to /install), then the OAuth keys and the version. The security # check needs the internet and only prints, so it is left out. Firefly III # opens /register to the first visitor only, so ox-owner.php registers the # owner from ADMIN_EMAIL and ADMIN_PASSWORD the way /register does, once. migrate = "php artisan firefly-iii:upgrade-database && php artisan firefly-iii:laravel-passport-keys && php artisan firefly-iii:set-latest-version --james-is-cool && php ox-owner.php" [cron] # Recurring transactions, auto-budgets, bill warnings and webhooks: the # command the docs give for a server without the cron container. Each job # runs at most once per 12 hours on its own, so an hourly call is safe and # catches up after the server was off at the image's 03:00. firefly = { schedule = "0 * * * *", run = "php artisan firefly-iii:cron" } [services] # The Docker compose file runs MariaDB, and .env.example names mysql. mysql = {} [storage] # Laravel writes storage/: logs, sessions, cached views, uploads, exports # and the OAuth keys. keep = ["storage"] ``` There is no `[app]` section and no install line, because ox detects the Composer and npm installs, FrankenPHP and PHP 8.5 by itself. The file adds only what the git tag lacks. - `required_variables` names `ADMIN_EMAIL` and `ADMIN_PASSWORD`. Firefly III never reads them. `ox-owner.php` does, and the review asks for them before the first deploy. - The build `commands` exist because the tag holds no `public/build` folder and no `public/v3/i18n` language files, and every page loads both. The first two are the commands ox detected: the Vite build of the v3 interface and Laravel's storage link. The third stands in for the release workflow's language step. - `migrate` follows the order of the project's Docker entrypoint. `firefly-iii:upgrade-database` migrates and seeds the database, runs the upgrades and stamps the build time, and until then every page redirects to `/install`. `laravel-passport-keys` makes the OAuth keys. `set-latest-version` needs its `--james-is-cool` flag, which Firefly III's own web installer passes too. `ox-owner.php` comes last. - The `firefly` job runs `php artisan firefly-iii:cron` at minute zero of every hour, in UTC, the command the docs give for a server with no cron container. - `mysql = {}` asks for a MariaDB that belongs to this project alone. Firefly III's Docker compose file runs MariaDB too. - `keep = ["storage"]` names the folder Laravel writes while it runs: logs, sessions, cached views, uploads, exports and the OAuth keys. ox runs a PHP release read-only by default, so it links `storage` to the project's data directory, which stays writable across deploys. `ox-owner.php` Code ``` make(Kernel::class)->bootstrap(); $email = (string) getenv('ADMIN_EMAIL'); $password = (string) getenv('ADMIN_PASSWORD'); $users = User::query()->count(); if ($users > 0) { if (User::query()->where('email', $email)->exists()) { echo "ox-owner: $email exists\n"; } else { echo "ox-owner: the database has $users user(s) and none is $email: left as it is\n"; } exit(0); } $check = Validator::make(['email' => $email, 'password' => $password], [ 'email' => 'required|string|email|max:255', 'password' => 'required|string|min:16', ]); if ($check->fails()) { fwrite(STDERR, 'ox-owner: '.implode(' ', $check->errors()->all())."\n"); exit(1); } // One transaction, so a listener that fails leaves no half-made owner for // the next deploy's guard to take as done. DB::transaction(static function () use ($email, $password): void { $user = User::create(['email' => $email, 'password' => bcrypt($password)]); event(new NewUserRegistered(new OwnerNotifiable(), $user)); if (!$user->hasRole('owner') || null === $user->refresh()->user_group_id) { throw new RuntimeException("ox-owner: the listener did not make $email the owner"); } }); echo "ox-owner: registered $email as the owner\n"; ``` `ox-translations.php` Code ``` .json, the strings the v3 pages load in the // browser, as Firefly III's release workflow does with // `ff3:json-translations v3` (JC5/firefly-iii-dev, GenLanguageJson): for // each language config/translations.php enables that has a folder under // resources/lang, every string config/translations.php lists for v3, in // that language, else in en_US, else "(file.key)". It is stored under the // language's html_language and under its name with a dash (en.json and // en-US.json). The git tag carries en_US only; the release adds the others // from Crowdin, which needs the project's own token. declare(strict_types=1); chdir(__DIR__); $config = require 'config/translations.php'; $english = []; $fallback = static function (string $file, string $key) use (&$english): string { $path = "resources/lang/en_US/$file.php"; if (!is_file($path)) { return "(!$file.$key!)"; } $english[$file] ??= require $path; return array_key_exists($key, $english[$file]) ? (string) $english[$file][$key] : "($file.$key)"; }; $written = []; foreach ($config['languages'] as $language) { if (!is_dir("resources/lang/$language")) { continue; } $content = []; foreach ($config['json']['v3'] as $file => $keys) { $path = "resources/lang/$language/$file.php"; if (!is_file($path)) { $content[$file] = []; continue; } $strings = require $path; $content[$file] = []; foreach (array_unique($keys) as $key) { $content[$file][$key] = (string) ($strings[$key] ?? $fallback($file, $key)); } } $code = $content['config']['html_language'] ?? ''; if ('' === $code) { fwrite(STDERR, "ox-translations: $language has no config.html_language\n"); exit(1); } $json = json_encode($content, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, 16); foreach (array_unique([$code, str_replace('_', '-', $language)]) as $name) { $out = "public/v3/i18n/$name.json"; if (false === file_put_contents($out, $json)) { fwrite(STDERR, "ox-translations: cannot write $out\n"); exit(1); } $written[] = $out; } } if ([] === $written) { fwrite(STDERR, "ox-translations: no language folder under resources/lang\n"); exit(1); } echo 'ox-translations: wrote ', implode(', ', $written), "\n"; ``` `ox-owner.php` makes the first account. Firefly III's `/register` page works only while the database has no user, because `single_user_mode` is on by default. The file registers the owner from `ADMIN_EMAIL` and `ADMIN_PASSWORD` the way that page does: the same checks except the online breach lookup, a hashed password, then the event whose listener makes the first user the owner. Where users exist it changes nothing, so every deploy can run it. A short password or a bad email stops the deploy and says why. `ox-translations.php` writes the language files in `public/v3/i18n`, such as `en.json`, which hold the strings the v3 pages load in the browser, as the release workflow does. The tag carries English only. The release adds the other languages from Crowdin with the project's own token, so a language picked on Getting started still shows English. Shell ```bash ADMIN_EMAIL ADMIN_PASSWORD APP_KEY SITE_OWNER DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD TRUSTED_PROXIES APP_URL MAIL_MAILER MAIL_FROM ``` `ADMIN_EMAIL` and `ADMIN_PASSWORD` are the owner. `APP_KEY` is the session encryption key. `SITE_OWNER` is your email address, shown in some errors to users who are not admins. `APP_URL` must match the address people use, and ox's `${PUBLIC_URL}` reference gives it ([variables](https://deploywithox.com/docs/operate/variables) explains references). `TRUSTED_PROXIES` says which proxies Laravel believes about the visitor's address, and the example file says to set `**` behind a reverse proxy. Caddy is that proxy here. `MAIL_MAILER` and `MAIL_FROM` choose how mail leaves and who sends it. `DB_CONNECTION` is `mysql`, and the other five `DB_` keys are references to the project's database, such as `${mysql.host}` ([configuration page](https://deploywithox.com/docs/config)). ## The database and the timer The database is MariaDB 11.8, which ox calls `mysql`. It runs as a service of this project alone. A staging copy ran beside production with a database of its own, and the memory figure above counts both. On the hour, the test agent saw the timer's job run and end with success. Firefly III's recurring transaction, auto budget, bill warning and exchange rate tasks each skip a run within 12 hours of their last, so an hourly call is safe, and the log often says a task "will not fire now". Firefly III's [cron page](https://docs.firefly-iii.org/how-to/firefly-iii/advanced/cron/) has the details. ## Updates and backups To move to a new release, merge its tag into your fork and push, or run `ox deploy firefly-iii --wait`. ox builds the new release in its own folder, runs `migrate`, and then switches traffic. The run redeployed the same commit. The [rollback page](https://deploywithox.com/docs/operate/rollback) covers a release that goes wrong. Backups need no setup. The run's backup, made with Back up now, held a MariaDB archive and an archive of the `storage` folder. Run `ox backups firefly-iii` to list them. A restore was not tested. The [backups page](https://deploywithox.com/docs/operate/backups) covers copies in an S3 bucket, and [self-hosting](https://deploywithox.com/self-hosting) lists the other apps. ## Frequently asked questions ### Do I need Docker to self-host Firefly III? No. Firefly III's own docs offer a Docker image, but the app is a plain PHP and Laravel project. ox builds it from your fork on the server, runs it with FrankenPHP as a system service, and installs MariaDB beside it. There is no image to pull. An update is a push to your fork or one deploy command. ### How do I make the first Firefly III account? Put ADMIN_EMAIL and ADMIN_PASSWORD in your variables before the first deploy. The migrate line runs ox-owner.php, which registers that person as the owner, and you sign in at /login. Firefly III closes its own register page after the first user. Inviting more people was not tested. ### Which database does Firefly III need? Firefly III reads DB_CONNECTION and works with MySQL-compatible servers and with PostgreSQL. The run used the mysql service ox sets up for the project, which is MariaDB 11.8, and ox fills the DB_ keys from it. PostgreSQL was not tested here. ### How much memory does self-hosted Firefly III use? After the reboot, the whole server in the run used the memory the facts show. That counts Firefly III, its staging copy, two MariaDB servers, Caddy and the ox agent. The build runs npm and Composer on the server too. ox takes any server from 1 GB up, and says so when memory is why the app stopped. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-firefly-iii#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-firefly-iii#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-firefly-iii#config) - [The database and the timer](https://deploywithox.com/blog/self-host-firefly-iii#services) - [Updates and backups](https://deploywithox.com/blog/self-host-firefly-iii#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-firefly-iii#faq) - [Do I need Docker to self-host Firefly III?](https://deploywithox.com/blog/self-host-firefly-iii#faq-docker) - [How do I make the first Firefly III account?](https://deploywithox.com/blog/self-host-firefly-iii#faq-account) - [Which database does Firefly III need?](https://deploywithox.com/blog/self-host-firefly-iii#faq-database) - [How much memory does self-hosted Firefly III use?](https://deploywithox.com/blog/self-host-firefly-iii#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 Forem without Docker on your own VPS](https://deploywithox.com/blog/self-host-forem) [Newer post How to self-host Fider on your own VPS without Docker](https://deploywithox.com/blog/self-host-fider) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/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 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/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://deploywithox.com/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://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)