Contents

Firefly III: Self-Hosted Personal Finance Without the Bank Watching

Budgets, rules and reports that live on your own server

Contents

Every budgeting app I have ever tried eventually wanted three things: my bank login, a monthly subscription, and the right to “anonymise” my spending and sell it to whoever buys that sort of thing. I gave up on all of them and put Firefly III on a box in the corner of my flat instead. It has been quietly tracking every penny I move for years now, and the only entity studying my coffee habit is me.

What Firefly III actually is

Advertisement

Firefly III is a self-hosted personal finance manager. You run it yourself, on your own server, and your financial history never leaves the machine you put it on. That is the whole pitch, and for me it is the entire point.

Under the bonnet it works on a sensible, almost double-entry model. Money does not appear or vanish; it moves. Every transaction has a source and a destination. A withdrawal goes from one of your asset accounts (current account, savings, cash in a tin) to an expense account (Tesco, the landlord, your favourite pub). A deposit comes from a revenue account. Transfers shuffle money between your own accounts. Once that clicks, the reports start telling the truth, because nothing is double-counted or quietly lost.

On top of that you get the usual suspects, done properly:

  • Budgets — envelopes with a monthly amount, so you can see “groceries: £240 of £300 spent” at a glance.
  • Categories and tags — for slicing spending however you like.
  • Bills — expected recurring payments, so Firefly nags you when the council tax has not landed.
  • A rules engine — the part I love most. Match on description, amount, source, anything, and then act: set a category, add a tag, rename the payee. Auto-categorisation that you fully control.
  • Reports — proper breakdowns by period, category, budget and account, with charts that are actually readable.

Getting it running

Firefly III is a PHP application that wants a database behind it. Docker Compose is the least painful way to stand the whole thing up. Here is a stripped-down stack with the app, a MariaDB database, and the companion Data Importer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
version: "3.8"

services:
  app:
    image: fireflyiii/core:latest
    restart: unless-stopped
    depends_on:
      - db
    environment:
      - APP_KEY=GENERATE_A_32_CHARACTER_RANDOM_STRING
      - APP_URL=https://firefly.example.com
      - TZ=Europe/London
      - DB_CONNECTION=mysql
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=firefly
      - DB_USERNAME=firefly
      - DB_PASSWORD=use-a-long-password-here
    volumes:
      - ./firefly_upload:/var/www/html/storage/upload
    ports:
      - "8080:8080"

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      - MYSQL_RANDOM_ROOT_PASSWORD=yes
      - MYSQL_USER=firefly
      - MYSQL_PASSWORD=use-a-long-password-here
      - MYSQL_DATABASE=firefly
    volumes:
      - ./firefly_db:/var/lib/mysql

  importer:
    image: fireflyiii/data-importer:latest
    restart: unless-stopped
    depends_on:
      - app
    environment:
      - FIREFLY_III_URL=http://app:8080
      - FIREFLY_III_ACCESS_TOKEN=paste-a-personal-access-token-here
      - TZ=Europe/London
    ports:
      - "8081:8080"

The APP_KEY must be exactly 32 random characters or the app refuses to start, and the cleanest way to make a valid one is to let Laravel do it. Leave APP_KEY blank in the environment for the very first boot, then once the container is up run:

1
docker compose exec app php artisan key:generate --show

Copy the printed value (it begins with base64:) into APP_KEY, recreate the container, and you are set. Generating it any other way — including with a hand-rolled head -c 32 /dev/urandom | base64 — works too, but the artisan command is the path the project tests against, so use it if you want a quiet life. Whatever you do, never reuse the key and never commit it. Put the whole stack behind a reverse proxy with TLS as well; your transaction history is not something to serve over plain HTTP. The first time you load the web UI you will register the initial account, which automatically becomes the owner — there is no separate admin-creation step.

Getting your money in

Advertisement

This is where honesty matters, because it is the part the glossy apps make look effortless and Firefly does not.

There is no magic, hands-off bank feed for most people. Firefly III itself does not log into your bank. What you get instead is the Data Importer, the companion service in the stack above. It can pull from a few real bank connectivity providers in regions where those exist, and crucially it imports CSV files.

In practice, CSV is how most of us live. You download a statement from your bank’s website, point the importer at it, and map the columns once: which is the date, which is the amount, which is the description. Firefly remembers that mapping as a configuration file, so next month it is a thirty-second job. The rules engine then catches the import and tidies everything — that Tesco line gets categorised, tagged and renamed automatically before you have even looked at it.

It is fiddlier than tapping “connect my bank” in a SaaS app. The trade-off is that nobody is brokering a permanent connection to your accounts on a third party’s servers.

Making the Rules Engine Earn Its Keep

The rules engine is what turns a chore into a thirty-second habit. A rule is a trigger and a set of actions, and Firefly applies them to transactions as they arrive (or in bulk over your existing history). The patterns that pay off most are the boring ones: anything from a supermarket gets the Groceries category, anything matching your gym’s payee gets tagged subscription, and anything above a threshold gets flagged for review. You can think of a rule loosely as:

1
2
3
4
5
IF  description CONTAINS "TESCO"
    OR description CONTAINS "SAINSBURY"
THEN set category to "Groceries"
     AND add tag "supermarket"
     AND set notes to "auto-categorised"

Build five or six of these and the import workflow becomes: download statement, point importer at it, glance at the handful of transactions the rules did not recognise, done. The genuine payoff is that because you wrote the rules, the categories mean exactly what you think they mean — there is no opaque vendor model deciding that your corner shop is “Entertainment.”

Worth knowing for the tinkerers: Firefly III ships a full REST API, and the personal access token you generate for the importer also unlocks it. That is how people wire up bank-feed scripts, scheduled CSV pulls, or a quick dashboard widget showing this month’s spend on a wall display. You do not need any of it to get value, but the door is open the day you want to automate the boring parts rather than clicking through the web UI each week.

Backing It Up

A finance ledger you cannot restore is worse than no ledger, because you will trust it right up until the disk dies. Firefly’s state is the database plus the upload volume. The simplest robust backup is a nightly database dump copied off the box:

1
2
3
docker compose exec -T db \
  mysqldump -u firefly -p"$DB_PASSWORD" firefly \
  | gzip > firefly-$(date +%F).sql.gz

Pair that with a copy of the firefly_upload directory (it holds attachments) and push both to another machine — not the same disk they came from. I treat this exactly like every other service I run; the discipline of off-box backups is the same one I argue for in the real cost of self-hosting, where the “time” line item is mostly backup-and-restore practice.

Troubleshooting the Usual Snags

A handful of issues catch nearly everyone on first setup.

The app shows a blank page or 500 on first load. Almost always the APP_KEY is missing, malformed, or the database migrations have not run. Check docker compose logs app; if you see a cipher or key-length error, the key is wrong. If you see “table not found,” the migration step has not completed — Firefly runs migrations automatically on start, so give it a minute and check the logs again before panicking.

The importer cannot reach the app. Inside the compose network the app is reachable as http://app:8080, not localhost. If you set FIREFLY_III_URL to localhost it will fail, because that resolves to the importer’s own container. Use the service name.

Personal access token rejected. The importer authenticates with a token you generate inside Firefly under Profile → OAuth → Personal Access Tokens. A fresh token, pasted whole, fixes most “401 Unauthorised” import failures. Tokens are long; make sure your editor has not wrapped or truncated it.

Duplicate transactions after re-importing a statement. Firefly de-duplicates on a hash, but overlapping date ranges across two CSV exports can still slip doubles through. Import non-overlapping ranges, and use the bulk-edit search to catch any that sneak in.

This is the same class of small, self-inflicted networking and permissions snags you hit with any container stack — if Docker itself is new to you, the mental model carries straight over from running something like a self-hosted password manager.

The honest trade-offs

Firefly III asks for discipline. If you go three months without importing a statement, you have three months of mapping and reconciling to slog through, and the temptation is to give up. The data is only as good as the habit feeding it. Entry is semi-manual: you are running imports, not living inside a fire-and-forget app.

The reward, and it is a real one, is clarity you cannot get any other way. Because you built the categories and wrote the rules, the reports mean exactly what you think they mean. I have caught two creeping subscriptions and one genuinely wrong direct debit purely because the numbers were finally trustworthy.

Verdict: who should bother

If you want zero effort and do not mind an app studying your spending, a hosted budgeting service will be less work — go and enjoy it. Firefly III is for the person who already self-hosts, owns the privacy argument, and is willing to spend ten minutes a week importing a statement in exchange for owning their entire financial picture outright.

That person is me, and after years of it I would not go back. The clarity is the thing that keeps me importing statements: when the numbers are finally trustworthy, you stop guessing about your own money. I caught a streaming subscription I had cancelled in my head but never in fact, a “free trial” that had quietly started billing, and a direct debit that had crept up by a third over two years — none of which a glossy auto-categorising app would have surfaced, because they would have buried all three under a vague “Bills” total.

Stand up the stack, import a single month, write three rules, and watch the first report come out. If the picture it paints makes you sit up — and it usually does, because almost nobody’s spending matches their assumptions — you have found your tool. If instead you resent the ten minutes a week, no shame in it: go back to the hosted app and let it watch. Firefly III rewards the person who wants to own the whole picture, end to end, and is willing to do a little work for it. For me, that trade has paid for itself many times over.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.