Workload · Running your own finance tracker

Self-hosted Firefly III, set up privately on your own server

To host Firefly III yourself, you download the official compose file and two settings files, fill in a 32-character key and a database password before anything starts, and bring up three small containers. Then you sign up through a private tunnel, because the first person to register becomes the owner. We stick to the server here; nothing on this page is financial advice.

The short version
  1. Three containers: Firefly III, MariaDB and a tiny scheduler
  2. Secrets live in two files only root can read
  3. You claim the owner account before anyone else can see the site
  4. A nightly job copies the database and your attachments
  5. Month to month, cancel when any month ends

Does it need a big server?

No, and the project gives no hardware figure at all. You run a PHP web app, a MariaDB database and a scheduler that wakes once a day. If PHP ever runs short of memory, the Docker FAQ says to set PHP_MEMORY_LIMIT to 512M or more. Official images exist for 64-bit Intel/AMD and 64-bit ARM; older 32-bit ARM boards are no longer supported.

Our view, not the docs': what makes a server right for a ledger is that you trust where the data sits and can get it back after a bad day. Raw speed comes far behind.

The server that fits a private ledger

Firefly III is made for people who don't want to upload their records to someone else's cloud. A dedicated server carries that idea down to the hardware: one physical computer running your work and nobody else's, with root access, so the only keys to the box are the ones you hand out.

  • A ready server reaches you within 30 minutes of the payment being approved; a custom build takes hours rather than minutes.
  • Rack servers, blades and two cartridge models (m510, m710x) come with a console you open yourself, screen plus power button, so a firewall mistake never locks you out for good. Other cartridge models give you a power switch alone.
  • Reinstalling the operating system is self-service in the control panel, with no ticket and no limit.
  • IPv6 comes at no charge, so set the firewall up for both kinds of address from the start.
  • You manage the server; we keep the hardware and the network running. Backing up your data is your job, and the steps below cover it.

Getting the files and secrets right first

Firefly III reads two settings files that must sit beside the compose file under exactly these names: .env holds the app's settings and .db.env the database's. Three values need care before the very first start:

  • APP_KEY protects sessions. It must be 32 random characters, no more and no fewer, and the docs ask you to leave out symbols like = and #.
  • The database password goes in twice, identical: as MYSQL_PASSWORD inside .db.env and as DB_PASSWORD inside .env. Set it before you start anything. The database remembers whatever password it first started with, and changing it later just leaves Firefly III locked out.
  • STATIC_CRON_TOKEN, also 32 characters, lets the scheduler call Firefly III's daily jobs.

The commands below create all three and write them straight into the files, readable by root only. printf is part of the shell itself, so the secrets never appear in the process list. Run them after sudo -i.

install -d -m 700 /srv/firefly && cd /srv/firefly
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/firefly-iii/docker/main/docker-compose.yml
curl -fsSL -o .env https://raw.githubusercontent.com/firefly-iii/firefly-iii/main/.env.example
curl -fsSL -o .db.env https://raw.githubusercontent.com/firefly-iii/docker/main/database.env
chmod 600 .env .db.env
rnd() { head /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 32; }
sed -i -e '/^APP_KEY=/d' -e '/^DB_PASSWORD=/d' -e '/^STATIC_CRON_TOKEN=/d' -e '/^TRUSTED_PROXIES=/d' -e '/^APP_URL=/d' .env
sed -i -e '/^MYSQL_PASSWORD=/d' .db.env
dbpass=$(rnd)
printf 'DB_PASSWORD=%s' "$dbpass" >> .env; echo >> .env
printf 'MYSQL_PASSWORD=%s' "$dbpass" >> .db.env; echo >> .db.env
unset dbpass
printf 'APP_KEY=%s' "$(rnd)" >> .env; echo >> .env
printf 'STATIC_CRON_TOKEN=%s' "$(rnd)" >> .env; echo >> .env
cat >> .env <<'EOF'
APP_URL=https://finance.example.org
TRUSTED_PROXIES=**
EOF
grep -cE '^(APP_KEY|STATIC_CRON_TOKEN)=[A-Za-z0-9]{32}$' .env

You should see 2. Now open .env in a text editor: change APP_URL to the web address you'll really use (Firefly III wants it to match what visitors see), TZ to your time zone and SITE_OWNER to your email.

Up and running in an evening

As shipped, the compose file opens Firefly III on port 80 of every address the server has, IPv4 (0.0.0.0) and IPv6 ([::]) alike. Docker sends that traffic around ufw's IPv4 rules, and around the IPv6 ones too if IPv6 support in Docker itself is turned on; if it isn't, the IPv6 side goes through docker-proxy, which ufw can filter. The cure: publish the app on 127.0.0.1 only, where just the server can reach it, and switch the firewall on, SSH allowed, before anything listens. Check the Engine version with docker version: from 28 onwards, a 127.0.0.1 port stays local to the machine.

sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow 22/tcp
ufw --force enable
cd /srv/firefly
sed -i 's/- 80:8080/- "127.0.0.1:8080:8080"/' docker-compose.yml
grep -n '127.0.0.1:8080' docker-compose.yml
docker compose up -d --pull=always
docker compose logs -f

The first start builds the database. When the log shows the installation finished (Firefly III says thank you), press Ctrl+C.

Claim the owner account while nobody else can see it

The sign-up form works only for the first person who uses it, and that person becomes the owner, the one who can later allow more users. You want to be that person, privately, while the app answers on the server's loopback alone.

  1. On your laptop, connect with your personal SSH account: ssh -N -L 9000:127.0.0.1:8080 yourname@your.server.
  2. Open http://localhost:9000 and register.
  3. In /settings, open the configuration options and keep Disable user registration ticked unless you really want more people on it. Each user gets separate books.
  4. Close the tunnel. Now it's ready for the web.

Keep recurring transactions firing every day

Recurring transactions, auto-budgets, subscription reminders and exchange-rate updates happen only when Firefly III's cron job runs. The image itself has no cron, so the compose file brings a small Alpine container for it: every day at 03:00, in your TZ, it calls Firefly III over the private Docker network with the STATIC_CRON_TOKEN kept in .env, so you never put the token in the server's own crontab. The container does hold it in its crontab, though, and its log prints the full address, token included, each time the job runs. To see whether it ran, read docker logs firefly_iii_cron, and keep what you see private: never paste that log into a forum or a public issue. A good run reports job_fired and job_succeeded for each job.

Your own web address with HTTPS

A web server on the host (nginx or Apache) takes the HTTPS connections and hands them to http://127.0.0.1:8080. Firefly III's install FAQ lists the nginx settings: pass on Host, X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto, accept uploads up to 64M via client_max_body_size, and allow slow pages 300 seconds. Seeing 502 errors after signing in? It suggests bigger proxy buffers. Once the proxy works, open ports 80 and 443 in ufw (ufw allow 80,443/tcp). The proxy runs directly on the server rather than in Docker, so until you do, the firewall turns away all visitors, and the certificate checks too, whether they arrive over IPv4 or IPv6.

TRUSTED_PROXIES tells Firefly III to believe the proxy's headers. Left empty, the FAQ says, you get links pointing at http and security-header errors; the settings template recommends **, which you already set. It's safe because only the server itself can reach port 8080. Prefer a point-and-click proxy? See Nginx Proxy Manager on your own server.

Importing from your bank: a separate app

Firefly III doesn't import by itself. The Data Importer is a second app (fireflyiii/data-importer) that the author keeps apart on purpose; it writes into Firefly III through the API. It takes files like CSV and CAMT, and connects to bank-data services like SimpleFIN or GoCardless. The project's guide tells you to save docker-compose-importer.yml as your docker-compose.yml. Careful: that file opens Firefly III again on port 80, written as '80:8080' with quotes so the earlier sed misses it, and the importer on port 81, both to the whole internet. The tidy way is to paste just its importer: section into the compose file you already have and give it the port "127.0.0.1:8090:8080"; the other way is to change both port lines by hand so they start with 127.0.0.1. Afterwards, run grep -n ':8080' docker-compose.yml: each port line it lists should begin with 127.0.0.1. Lines showing http://app:8080 are fine; that is how the containers reach each other privately, not a port opened on the server. Its settings file, .importer.env, is the data importer repository's .env.example, saved under that name: point VANITY_URL at your Firefly III address and FIREFLY_III_URL at http://app:8080, and give it mode 600, because your provider keys go there. Open a second tunnel for the importer (ssh -N -L 8090:127.0.0.1:8090 yourname@your.server). Then, in Firefly III's /profile, create the OAuth client with Confidential unticked and http://localhost:8090/callback as its redirect address; that address only works because the tunnel makes it exist on your laptop.

Backups you can restore, and safe updates

There is no built-in backup, and the docs say plainly the export feature isn't one. You need three things: a database dump, the attachments folder, and your settings files (they hold APP_KEY and the passwords). Save this as /srv/firefly/backup.sh with mode 700. The database password stays inside the database container and reaches the dump tool through MYSQL_PWD, never typed anywhere.

# /srv/firefly/backup.sh
set -euo pipefail
dest=/srv/backups/firefly
stamp=$(date +%F)
install -d -m 700 "$dest"
cd /srv/firefly
docker exec firefly_iii_db sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" exec mariadb-dump --single-transaction -u "$MYSQL_USER" "$MYSQL_DATABASE"' | gzip > "$dest/database-$stamp.sql.gz"
docker cp firefly_iii_core:/var/www/html/storage/upload - | gzip > "$dest/attachments-$stamp.tar.gz"
tar -czf "$dest/settings-$stamp.tar.gz" docker-compose.yml .env .db.env
find "$dest" -name '*.gz' -mtime +14 -delete
  1. Run it nightly: create /etc/cron.d/firefly-backup containing the single line 15 2 * * * root umask 077; mkdir -p -m 700 /srv/backups/firefly; bash /srv/firefly/backup.sh > /srv/backups/firefly/log-$(date +\%F).txt 2>&1.
  2. Copy the files somewhere else regularly. If the server's disks fail, copies on the same disks go with them.
  3. Restore one onto a spare machine early on; the docs call that the first thing to do after any backup.
  4. To update, take a fresh backup, then docker compose stop, then docker compose pull, and finally docker compose up -d --remove-orphans. The docs warn some upgrades clean up or remove old data.
  5. Re-downloading the compose file can bring a newer MariaDB your data volume isn't ready for, and it undoes your 127.0.0.1 change. Compare the two files first.

Questions people ask

Is Firefly III free to run on my own server?
Yes. Firefly III and its Data Importer are open source under AGPL-3.0, the Affero GPL, version 3. If you change the code, the licence page says your changes must be released under it too, and that applies when you run a modified copy on a web server. Running it unchanged for yourself asks nothing more of you.
What if I set the DB password after starting it once?
Firefly III can't log in to its database, because MariaDB keeps the password from its first start inside its volume. The Docker FAQ's answer is to delete the containers plus both data volumes (db and upload) and start fresh with the right values. That's why the setup above writes both passwords before anything runs.
Can my partner and I share the same books?
Not today. Firefly III can host several users, but each one has separate books, and the FAQ says one administration can't be shared. It also points out that whoever can open the database sees all users' books, so only host people who trust whoever runs the server.
Where do I find STATIC_CRON_TOKEN?
You make it yourself: any random 32-character string, written into .env. The setup commands above generate one, and the scheduler container reads it from that file, so you never type it.
Can I move an existing Firefly III install onto a new server?
Yes. Carry over the same .env and .db.env, because the APP_KEY and the passwords must not change, then load the database dump and put the attachments back on the new machine. Moving more than this one service? Our migration page covers planning the switch.

Give your finance tracker a home of its own

Pick a server, set it up privately, and keep your records on hardware nobody else uses.

Get a server for Firefly III

Read next