If your bank account, credit cards, and that one savings goal you keep meaning to hit are scattered across three different apps, you've probably looked at Mint's shutdown or a budgeting app's price hike and thought "there has to be a better way." There is. Firefly III is a free, open-source personal finance manager you run yourself, and if you've already got a Proxmox VE box humming away in a closet or under a desk, you have everything you need to host it.
This guide walks through running Firefly III in its own LXC container on Proxmox VE, using Docker Compose the way the Firefly III project itself recommends. No cloud subscription, no third party reading your transaction history, and no monthly fee. Budget maybe fifteen to twenty minutes if this is your first time touching Docker inside an LXC container.
What You Will Learn
By the end of this walkthrough you'll have a working Firefly III instance running in its own container, reachable from any browser on your network. Specifically, you'll:
- Create a properly sized LXC container on Proxmox VE and turn on the two features Docker needs to run inside it
- Install Docker Engine and the Docker Compose plugin on a fresh Debian 13 container
- Write a docker-compose.yml file that runs Firefly III alongside a MariaDB database and a small cron container for scheduled tasks
- Finish the first-run setup wizard and create your admin account
- Know what to check when the container won't start, the web page won't load, or the setup wizard throws an error
What Is Firefly III?
Firefly III is a self-hosted web app for tracking money: income, expenses, transfers between accounts, budgets, recurring bills, and long-term savings goals. Think of it as a spreadsheet that got serious about double-entry bookkeeping, with a web interface instead of rows and columns. It can import bank statements as CSV files, categorize spending, and chart where your money actually goes each month rather than where you assumed it went.
It's built on PHP and Laravel, stores its data in a MySQL/MariaDB or PostgreSQL database, and ships as an official Docker image, which is why Docker is the easiest way to run it. The project has been actively maintained for close to a decade at this point, which matters a lot for something you're trusting with years of financial history.
Why Would You Use It?
A few reasons people move their budgeting off a phone app and onto their own server:
Your transaction data stays on hardware you control. That's a bigger deal for a finance app than for, say, a media server — nobody's account numbers are sitting in someone else's database. Firefly III also doesn't care about ads, doesn't sell usage data, and won't get acquired and shut down next quarter.
There's also just more control. You can define your own budget categories, build custom reports, and connect it to Firefly III's data importer for semi-automated bank imports, without waiting on a vendor to add the feature you want.
I'll be honest about the tradeoff too: you're now responsible for backups. A cloud budgeting app losing your data is annoying. A self-hosted one losing your data because nobody set up backups is on you. Keep that in mind — it comes up again in the Best Practices section below.
Prerequisites
Before starting, make sure you have:
- A working Proxmox VE 9.x host with enough free resources for a small container (2 CPU cores, 2 GB RAM, and about 8 GB of disk is comfortable for a single-user Firefly III instance)
- A Debian 13 (Trixie) LXC container template downloaded, or access to download one from the built-in template list
- Basic comfort with a Linux terminal — you'll be pasting commands into the container's console, not the Proxmox host
- A rough idea of your network's IP range, so you can find the container once it's running
You don't need to know Docker already. You don't need to know PHP either — the official image handles all of that. What you do need is patience with the container's Features settings, because that's where almost everyone gets tripped up on the first attempt.
Step-by-Step Tutorial
Step 1: Create the LXC container
In the Proxmox VE web interface, select your node and click Create CT. Give it a hostname like firefly, set a root password, and pick the Debian 13 template on the Template tab. On the Resources tab, 2 CPU cores and 2048 MB of RAM is plenty to start — you can always bump it later with pct set if the container feels sluggish.
One decision matters more than the others here: leave Unprivileged container checked. Docker runs fine in an unprivileged LXC container these days, and unprivileged is the safer default — it maps the container's root user to an unprivileged user on the Proxmox host, so a compromise inside the container doesn't hand over the whole host.
Step 2: Turn on nesting and keyctl
This is the step people skip and then spend an hour debugging. Docker needs two LXC features that are off by default: nesting, which lets the container create its own mount namespaces and cgroups (Docker can't run containers of its own without it), and keyctl, which some container runtimes use for kernel keyring operations.
Do this before you boot the container. In the container's Options tab, double-click Features and tick both Nesting and Keyctl. From the Proxmox host shell you can also set both at once with:
pct set 200 --features nesting=1,keyctl=1
Replace 200 with your container's actual VMID. If the container is already running, stop it first — feature changes don't apply to a running container.
Step 3: Start the container and update it
Start the container, open its console from the Proxmox web UI (or SSH in once you know its IP), and run a standard update:
apt update && apt full-upgrade -y
Then install a couple of tools Docker's install script needs:
apt install -y ca-certificates curl gnupg
Step 4: Install Docker Engine and Docker Compose
Add Docker's official APT repository rather than pulling from Debian's own repos, since Debian's Docker packages tend to lag well behind upstream:
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
Now install Docker and the Compose plugin:
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Confirm it's working:
docker compose version
If that prints a version number instead of an error about cgroups or overlayfs, nesting is configured correctly and you're clear to move on.
Step 5: Write the docker-compose.yml file
Create a directory to hold everything and drop into it:
mkdir -p /opt/firefly-iii && cd /opt/firefly-iii
Firefly III needs an APP_KEY, a random 32-character encryption key used to protect session data and encrypted database fields. Generate one before you write the compose file, since you'll paste it straight into your environment file:
docker run --rm fireflyiii/core:latest artisan key:generate --show
Copy the output — it'll look something like base64:XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX=. You'll also want a second random token for the cron container, which you can generate with:
openssl rand -hex 16
Now create .env in that same directory:
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=firefly
DB_USERNAME=firefly
DB_PASSWORD=change-this-password
MYSQL_RANDOM_ROOT_PASSWORD=yes
MYSQL_DATABASE=firefly
MYSQL_USER=firefly
MYSQL_PASSWORD=change-this-password
APP_KEY=paste-your-generated-key-here
APP_URL=http://192.168.1.50:8080
TRUSTED_PROXIES=**
STATIC_CRON_TOKEN=paste-your-openssl-token-here
Two things to change before saving: swap change-this-password for an actual password, and set APP_URL to the container's real IP address (or hostname, if you've set one up in your local DNS). Firefly III checks this value and will misbehave — mixed-content warnings, broken redirects after login — if it doesn't match how you actually reach it.
Then create docker-compose.yml:
services:
app:
image: fireflyiii/core:latest
container_name: firefly_iii
restart: always
env_file: .env
volumes:
- firefly_iii_upload:/var/www/html/storage/upload
ports:
- "8080:8080"
depends_on:
- db
db:
image: mariadb:11
container_name: firefly_iii_db
restart: always
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: firefly
MYSQL_USER: firefly
MYSQL_PASSWORD: change-this-password
volumes:
- firefly_iii_db:/var/lib/mysql
cron:
image: alpine
container_name: firefly_iii_cron
restart: always
command: >
sh -c "echo \"0 3 * * * wget -qO- http://app:8080/api/v1/cron/$$STATIC_CRON_TOKEN\" | crontab - && crond -f -L /dev/stdout"
depends_on:
- app
volumes:
firefly_iii_upload:
firefly_iii_db:
Make sure the MYSQL_PASSWORD under the db service matches whatever you put in .env for DB_PASSWORD — that trips people up more often than anything else in this file.
Step 6: Bring it up
docker compose up -d
Give it about thirty seconds on first boot — the app container waits for the database to accept connections, then runs its own migrations automatically. Check progress with:
docker compose logs -f app
You're looking for a line mentioning the built-in web server starting. Once you see it, open http://<container-ip>:8080 in a browser.
Step 7: Finish the setup wizard
The first-run wizard asks you to create an admin account with an email address and password. Firefly III doesn't send a verification email by default in a self-hosted setup, so don't worry if nothing arrives — just log in with what you set. From there it walks you through creating your first asset account (your checking account, typically) and setting a currency.
Skip the sample data unless you specifically want to poke around a demo budget. Most people find it easier to start empty and add real accounts.
Commands Explained
| Command | What it does |
|---|---|
pct set 200 --features nesting=1,keyctl=1 | Enables the two LXC kernel features Docker needs, on container 200 |
docker run --rm fireflyiii/core:latest artisan key:generate --show | Starts a throwaway container just to generate an APP_KEY, then removes itself |
docker compose up -d | Reads docker-compose.yml, pulls any missing images, and starts every service in the background |
docker compose logs -f app | Streams the Firefly III app container's logs so you can watch startup or debug errors |
docker compose down | Stops and removes the containers, but leaves your named volumes (and data) intact |
Common Errors
"Cannot connect to the Docker daemon" right after installing Docker usually means nesting isn't actually enabled, even if you ticked the box — the container needs to be fully stopped and restarted after you change Features, not just rebooted from inside.
A blank white page or an "Environment file not found" error on first load almost always means docker compose was run from a directory that doesn't contain your .env file, or the env_file: .env line has a typo.
If the app container keeps restarting in a loop, run docker compose logs app and look for a database connection error near the top. Nine times out of ten it's a password mismatch between .env and the db service's environment block.
SQLSTATE[HY000] [2002] Connection refused means the app container started faster than the database was ready to accept connections. The official image retries automatically for a while, so this usually clears up on its own within a minute — if it doesn't, check that the db service name in DB_HOST matches the service name in your compose file exactly.
Troubleshooting
Start by confirming all three containers are actually up:
docker compose ps
Anything showing Restarting instead of Up needs its logs checked next. For the database specifically:
docker compose logs db
If you can't reach the web interface at all, first check the container has an IP and can reach the network:
ip a
ping -c 3 1.1.1.1
If that works but the browser still times out, double-check the Proxmox firewall isn't blocking port 8080 on the container, and that nothing else on your network is already using that port.
One more thing worth checking if login redirects loop forever: go back to .env and confirm APP_URL exactly matches the address in your browser's URL bar, including http:// versus https://. A mismatch here is a classic cause of redirect loops after login.
Best Practices
Back up the firefly_iii_db volume regularly — that's where every transaction lives. A simple docker compose exec db mysqldump cron job to a file outside the container, combined with a Proxmox VE scheduled backup (vzdump) of the whole container, covers you twice over. I'd genuinely rather have a boring nightly backup than a clever restore script I've never tested.
Don't expose port 8080 directly to the internet. If you want remote access, put a reverse proxy with a real TLS certificate in front of it — Proxmox VE's own community has plenty of Nginx Proxy Manager guides, and Firefly III itself is happy to sit behind one.
Set the container's memory limit a bit above what it needs (2 GB rather than 512 MB) even though Firefly III is light at idle. Import jobs and report generation spike memory briefly, and getting OOM-killed mid-import is a bad time.
Take a Proxmox snapshot of the container before every major Firefly III version upgrade. Rolling back a bad update is a lot less stressful than restoring from a database dump.
Frequently Asked Questions
Do I need a GPU or a powerful CPU for this?
No. Firefly III is a lightweight PHP app. A single core and 1 GB of RAM will technically run it, though 2 GB gives you comfortable headroom for imports and reports.
Can I use PostgreSQL instead of MariaDB?
Yes — set DB_CONNECTION=pgsql in .env and swap the db service image to a PostgreSQL image with matching credentials. Both databases are officially supported.
Is my financial data encrypted?
Sensitive fields are encrypted using your APP_KEY, and the whole app runs on your own hardware rather than a third party's server. It's not end-to-end encrypted like a password manager, so treat backups of the database with the same care you'd give the live instance.
Can I connect it to my actual bank?
Not directly and automatically in most regions. Firefly III's companion Data Importer tool handles CSV imports and, depending on your bank and country, some import formats like CAMT.053. Full live bank-sync APIs are a separate, paid third-party integration some users add on top.
What happens if I lose the APP_KEY?
Any data encrypted with it becomes unreadable, which is exactly why it belongs in your backup routine alongside the database dump — write it down somewhere outside the container too.
Conclusion
You now have a private budgeting tool running entirely on hardware you own, with your transaction history nowhere near a third-party server. The setup takes a bit more effort than signing up for a hosted app, mostly around the nesting and keyctl step, but the ongoing maintenance is light: pull new images occasionally, back up the database volume, and it mostly runs itself.
From here, worth exploring: setting a Proxmox VE backup job so the whole container gets snapshotted automatically, and putting a reverse proxy in front of it if you want to check your budget from your phone while you're out.