Skip to content

How to Deploy Firefly III on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Firefly III, the double-entry personal finance manager, with Docker Compose and MariaDB, including the cron job and the proxy setting most installs get wrong.

Before you start
  • A small VPS: 1 vCPU / 1–2 GB RAM
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Firefly III is

Firefly III is a self-hosted personal finance manager built on double-entry bookkeeping. Every transaction moves money from one account to another, so your balances always reconcile. On top of that you get budgets, categories, tags, bills and subscriptions, recurring transactions, a rule engine that tags and files transactions as they arrive, reports, and a REST API. It is a PHP (Laravel) app under the AGPL-3.0 licence.

It is not a quick-entry budgeting app. If you want to assign every dollar to an envelope and move on, Actual Budget vs Firefly III lays out the difference. Firefly III suits people who want a full ledger of their finances and are willing to set up accounts and rules to get it.

The official install is three containers: the app, a MariaDB database, and a tiny Alpine container that runs Firefly III's daily cron job. Skipping that third one is the most common mistake, because several features quietly stop working without it.

Server sizing

The catalog lists 512 MB RAM as the minimum. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1) the idle stack measured about 159 MB of RAM and about 2 GB of disk for the images and volumes.

  • 1 vCPU / 1 GB RAM runs it comfortably for one household.
  • 2 GB RAM leaves room for the optional Data Importer and a reverse proxy.
  • Disk: 20 GB covers the OS, the images, the database and uploaded attachments for years.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed, along with a non-root user and a ufw firewall. If not, start with Docker & Compose on Ubuntu.

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Firefly III (Docker Compose)

Upstream's Docker docs have you download three files: the compose file from the firefly-iii/docker repository, the app's .env.example saved as .env, and database.env saved as .db.env. Keep those names, because the compose file refers to them.

mkdir -p ~/firefly && cd ~/firefly
curl -fsSLo docker-compose.yml https://raw.githubusercontent.com/firefly-iii/docker/main/docker-compose.yml
curl -fsSLo .env https://raw.githubusercontent.com/firefly-iii/firefly-iii/main/.env.example
curl -fsSLo .db.env https://raw.githubusercontent.com/firefly-iii/docker/main/database.env

Now set the secrets. The docs are specific about each one:

  • DB_PASSWORD in .env and MYSQL_PASSWORD in .db.env must be the same value. Set them before the first start. Once MariaDB has initialised its volume, it keeps the old password.
  • APP_KEY must be exactly 32 characters, with no special characters such as = or #.
  • STATIC_CRON_TOKEN must also be exactly 32 characters. The cron container uses it to call Firefly III.
  • TRUSTED_PROXIES=** tells Firefly III to trust the reverse proxy's headers. Without it, links and forms come out as http:// behind HTTPS.

openssl rand -hex 16 prints exactly 32 hex characters, which meets both length rules. Replace money.example.com with your own hostname:

cd ~/firefly
DOMAIN=money.example.com
DBPW=$(openssl rand -hex 16)
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$DBPW|; s|^APP_KEY=.*|APP_KEY=$(openssl rand -hex 16)|; s|^STATIC_CRON_TOKEN=.*|STATIC_CRON_TOKEN=$(openssl rand -hex 16)|; s|^TRUSTED_PROXIES=.*|TRUSTED_PROXIES=**|; s|^APP_URL=.*|APP_URL=https://$DOMAIN|" .env
sed -i "s|^MYSQL_PASSWORD=.*|MYSQL_PASSWORD=$DBPW|" .db.env
grep -E '^(DB_PASSWORD|APP_KEY|STATIC_CRON_TOKEN|TRUSTED_PROXIES|APP_URL)=' .env | sed 's/=.*/=(set)/'

Also look through .env for SITE_OWNER (your email address, shown in some error messages) and TZ (it defaults to Europe/Amsterdam).

The upstream compose file publishes the app on host port 80. Caddy needs that port, so move the app to loopback-only port 8080:

cd ~/firefly
sed -i 's|- 80:8080|- "127.0.0.1:8080:8080"|' docker-compose.yml
grep -n '8080' docker-compose.yml

Start the stack. The first boot creates the database schema, which takes a minute or two:

cd ~/firefly
docker compose -f docker-compose.yml up -d --pull=always
timeout 600 bash -c 'until curl -fsS -o /dev/null http://127.0.0.1:8080/; do sleep 5; done'
docker compose ps
docker compose logs -f app

Upstream says Firefly III "will thank you for installing it" in the log when it is ready.

HTTPS + domain

Point an A record for money.example.com at the server and wait for it to resolve. Then put Caddy in front of the loopback port, following Automatic HTTPS with Caddy:

money.example.com {
    reverse_proxy 127.0.0.1:8080
}

Caddy sends the X-Forwarded-* headers by default, and TRUSTED_PROXIES=** lets Firefly III believe them. If you run Caddy as a container, proxy to firefly_iii_core:8080 on a shared Docker network instead of 127.0.0.1.

First login

Open https://money.example.com and register. The first user to register becomes the owner, and registration is then blocked for everyone else. That is Firefly III's default, as a security measure. If you later want separate logins for family members, the owner can re-enable registration under /settings → "Configuration options for Firefly III".

Then:

  1. Turn on two-factor authentication from your /profile page. The same page can log out your other sessions.
  2. Create your asset accounts (checking, savings, cash) before you enter transactions. The upstream "first steps" tutorial walks through this.
  3. For bank imports, install the separate Data Importer. It has its own compose file (docker-compose-importer.yml in the same repository) and handles CSV files and bank providers such as GoCardless.

The cron job

Automated budgets, recurring transactions, subscription warnings and exchange rate updates only work while the cron job runs, according to upstream. The cron service in the compose file handles this. At 03:00 every day, it calls http://app:8080/api/v1/cron/<STATIC_CRON_TOKEN>. Check that it registered:

cd ~/firefly
docker compose ps cron
docker compose logs --tail 20 cron

If you would rather not run the extra container, the token on your /profile page ("Command line token") can drive the same URL from the host's crontab.

Backups

Upstream is explicit about two things. Firefly III's export function is not a backup, and a Docker backup needs three pieces:

  • the .env and .db.env files (above all the APP_KEY)
  • the database volume
  • the upload volume

The volumes are prefixed with the project directory name. docker volume ls shows the exact names, which here are firefly_firefly_iii_db and firefly_firefly_iii_upload. Stop the stack for a consistent copy:

cd ~/firefly
mkdir -p ~/backups/firefly
docker compose stop
for v in firefly_iii_db firefly_iii_upload; do
  docker volume inspect firefly_$v >/dev/null && \
  docker run --rm -v firefly_$v:/data -v ~/backups/firefly:/backup alpine \
    tar czf /backup/$v-$(date +%F).tar.gz -C /data .
done
cp .env .db.env docker-compose.yml ~/backups/firefly/
docker compose start
ls -lh ~/backups/firefly

The docker volume inspect guard matters. Upstream warns that backing up a volume name that doesn't exist makes Docker create an empty one, and you end up archiving nothing. Copy the backup folder off the box, then restore it once on a scratch machine to prove it works. The docs say to test the restore before anything else.

Upgrades

This is the upstream Docker Compose procedure:

cd ~/firefly
docker compose stop
docker compose pull
docker compose -f docker-compose.yml up -d --remove-orphans

Back up first. Upstream warns that some upgrades are destructive migrations that can clean up or remove data. If you ever re-download docker-compose.yml, compare the database image before you start it. A newer file may point at a MariaDB release that cannot read your existing volume.

Troubleshooting

The app waits for db:3306 forever. The MariaDB container has exited. Its log usually says the database is uninitialised with no password option. Check that .db.env exists in the same folder and holds MYSQL_PASSWORD plus a root password option.

"Access denied" for the database after changing the password. MariaDB stored the original password in its volume on first boot. Put the old password back, or change it inside MariaDB. Editing .db.env afterwards does nothing.

Forms and charts break behind HTTPS, or links point at http://. Upstream lists a missing TRUSTED_PROXIES=** as the usual cause, and it also explains Content Security Policy errors. Set it in .env and recreate the app with docker compose up -d.

Recurring transactions never appear. The cron job isn't reaching the app. Check docker compose logs cron, and check that STATIC_CRON_TOKEN is exactly 32 characters.

Verification + next steps

You're done when you can load https://money.example.com over a valid certificate, register the owner account, and enable 2FA. A private window should then show registration closed, docker compose ps should list app, db and cron as running, and you should have an off-box backup that you have restored once.

From there: set up rules so imported transactions file themselves, then add the Data Importer for your bank. For investments, which Firefly III does not track as a portfolio, see Deploy Ghostfolio on a VPS and Firefly III vs Ghostfolio.

Next steps

How to self-host Firefly III →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.