You can self-host Firefly III on a VPS in under 20 minutes using Docker Compose. The result is a fully private personal finance manager running on your own server, with transaction data stored in a MariaDB container, accessible over HTTPS behind Nginx, and connected to the Data Importer for CSV bank exports. This guide covers the complete setup and the one configuration mistake that silently corrupts your encrypted data on every version upgrade.

What Firefly III actually does

Firefly III is an open-source personal finance manager built for people who want a real double-entry transaction ledger, not a bank app's filtered view of their spending. You define accounts (checking, savings, credit cards, cash wallets, investment accounts), record transactions manually or import them from CSV files or bank connections, and build budgets, categories, and reports on top. The application runs entirely on your own infrastructure. No third-party service touches your financial data.

The practical workflow on a VPS is straightforward: export a CSV from your bank's online portal once a week, feed it to the Data Importer container, and Firefly III categorizes and stores the transactions. Over time you get accurate spending history, multi-currency support, recurring transaction automation, and financial reports that would otherwise require a paid subscription to a cloud app that stores your data on someone else's server. On a ct.Steady plan (4 cores, 4GB RAM, 50GB SSD), Firefly III runs alongside other services without noticeable resource pressure.

What you need before you start

You need a VPS running Debian 12 or Ubuntu 22.04/24.04 with Docker and the Compose plugin installed. Firefly III's stated minimum is 512MB RAM, but with MariaDB and the cron container also running, a 1GB instance becomes tight under normal operation. Plan for 2GB to give yourself comfortable headroom. You also need a domain or subdomain pointed at your server's IP address before setting up the Nginx reverse proxy. The default SMTP block on new VPS instances does not affect Firefly III since email is optional configuration.

If Docker is not yet installed, run:

curl -fsSL https://get.docker.com | sh
systemctl enable --now docker

Verify that the Compose plugin is available:

docker compose version

If the command returns a v2.x version number, proceed. If it fails, install the plugin manually:

apt install docker-compose-plugin -y

Directory setup and file downloads

Create a dedicated directory and move into it:

mkdir -p /opt/firefly-iii && cd /opt/firefly-iii

Download the three required files directly from the official Firefly III repositories:

curl -O https://raw.githubusercontent.com/firefly-iii/docker/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/firefly-iii/firefly-iii/main/.env.example
curl -o .db.env https://raw.githubusercontent.com/firefly-iii/docker/main/database.env

The filenames are not arbitrary. The compose file contains env_file: .env and env_file: .db.env as literal references. Renaming the files requires editing the compose file to match. Download the raw files with curl rather than copy-pasting from a browser, since YAML is whitespace-sensitive and rendered pages may introduce invisible formatting changes.

The Docker Compose file

The default compose file from the Firefly III Docker repository defines three core services: the main application (app), the database (db running MariaDB LTS), and a lightweight Alpine cron container that triggers recurring transaction processing each night. A minimal compose file configured for a production VPS deployment looks like this:

services:
  app:
    image: fireflyiii/core:latest
    hostname: app
    container_name: firefly_iii_core
    restart: always
    volumes:
      - firefly_iii_upload:/var/www/html/storage/upload
    env_file: .env
    networks:
      - firefly_iii
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      - db

  db:
    image: mariadb:lts
    hostname: db
    container_name: firefly_iii_db
    restart: always
    env_file: .db.env
    networks:
      - firefly_iii
    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/REPLACE_WITH_CRON_TOKEN' | crontab - && crond -f -L /dev/stdout"
    networks:
      - firefly_iii

volumes:
  firefly_iii_upload:
  firefly_iii_db:

networks:
  firefly_iii:
    driver: bridge

The port binding uses 127.0.0.1:8080:8080 instead of 0.0.0.0:8080:8080. This restricts direct container access to the loopback interface. The only path into Firefly III from outside the server is through your Nginx reverse proxy. Binding to 0.0.0.0 is a routine mistake on new VPS deployments that bypasses any firewall rules you have in place for port 8080.

Configuring .env and why APP_KEY is the most important variable in the file

Open .env and set these values before the containers ever start. The database password must match exactly between .env and .db.env:

# .env
APP_KEY=                              # Generate this FIRST - see below
APP_URL=https://finance.yourdomain.com
SITE_OWNER=you@yourdomain.com
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=firefly
DB_USERNAME=firefly
DB_PASSWORD=a_strong_password_here
STATIC_CRON_TOKEN=a32characterrandomtokenstringhere
TZ=Europe/Bucharest
# .db.env
MYSQL_RANDOM_ROOT_PASSWORD=yes
MYSQL_USER=firefly
MYSQL_PASSWORD=a_strong_password_here
MYSQL_DATABASE=firefly

Set the database password in both files before the first docker compose up. MariaDB initializes its data directory on the first container start and stores the credentials in the volume. If you start the stack with one password and change it later in the env files, the database volume still holds the original credentials. You will get authentication failures on every subsequent start that require dropping the firefly_iii_db volume to fix.

Generating APP_KEY correctly

APP_KEY is Laravel's application encryption key. Firefly III uses it to encrypt all OAuth tokens, API keys, and sensitive fields written to the database. The key must be exactly 32 characters. Generate one before you start the containers:

head /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 32 && echo

Copy the output into .env:

APP_KEY=yourgenerated32characterstring

Store this value in a password manager immediately. You will need the original, unchanged key every time you update the container image, rebuild the stack, or migrate the installation to a different server. This key is permanently tied to the encrypted data in your MariaDB volume. It does not rotate. It does not change.

The failure mode that breaks installations on upgrades

In our experience managing Docker deployments, this is the most common Firefly III support pattern: a user wants to upgrade to a new version, re-downloads the .env.example from the repository to get clean settings, leaves APP_KEY blank or generates a new one, and starts the updated container against the existing database volume. The new key cannot decrypt anything written with the old one. The application throws a Illuminate\Contracts\Encryption\DecryptException in the log and all previously created OAuth clients, API tokens, and encrypted account fields are unusable.

Check the application log inside the running container:

docker exec -it firefly_iii_core tail -n 50 /var/www/html/storage/logs/laravel.log

If you see DecryptException entries, the cause is almost always APP_KEY mismatch. The recovery path is to restore the original key in .env and restart the stack. There is no automated re-encryption utility. Changing APP_KEY after data has been written to the database is not recoverable without the original key.

The STATIC_CRON_TOKEN value goes into both the cron container command and your .env. Generate it the same way as APP_KEY and keep it stable across upgrades for the same reason.

Starting the stack and verifying health

Start all containers:

docker compose up -d --pull=always

Watch the application logs until initialization completes:

docker compose logs -f app

On a first run, Firefly III executes database migrations automatically. On a vm.Entry or ct.Ready plan, this typically finishes within 60 to 90 seconds. The "Thank you for installing Firefly III" line in the log confirms the application is ready. Verify the APP_KEY and database connection are working correctly with:

docker exec firefly_iii_core php artisan firefly-iii:decrypt-all

If that command returns without error output, the stack is healthy.

Nginx reverse proxy configuration

Install Nginx on the host if it is not already running:

apt install nginx -y

Create a server block at /etc/nginx/sites-available/firefly:

server {
    listen 80;
    server_name finance.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name finance.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/finance.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/finance.yourdomain.com/privkey.pem;

    client_max_body_size 64M;

    location / {
        proxy_pass         http://127.0.0.1:8080;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

Enable the site and obtain a certificate from Let's Encrypt:

ln -s /etc/nginx/sites-available/firefly /etc/nginx/sites-enabled/
apt install certbot python3-certbot-nginx -y
certbot --nginx -d finance.yourdomain.com
nginx -t && systemctl reload nginx

The client_max_body_size 64M directive is not optional if you plan to import large CSV files. Nginx rejects uploads above its 1MB default with a 413 error. Firefly III does not surface a helpful message in the UI when this happens, so the request silently fails and the import never starts. If you are running a DirectAdmin or cPanel stack on the same server with an existing Nginx front-end, add the proxy location to the relevant virtual host configuration instead of creating a parallel Nginx instance.

Adding the Data Importer

The Data Importer is a separate container maintained by the Firefly III project. It reads CSV files or connects to supported bank APIs via GoCardless and pushes parsed transactions into Firefly III through the REST API. Running it as an additional service in the same compose file is the cleanest approach on a single VPS.

Add the importer service to docker-compose.yml:

  importer:
    image: fireflyiii/data-importer:latest
    hostname: importer
    container_name: firefly_iii_importer
    restart: always
    networks:
      - firefly_iii
    ports:
      - "127.0.0.1:8081:8080"
    env_file: .importer.env
    depends_on:
      - app

Create .importer.env. Two variables are non-negotiable:

# .importer.env
FIREFLY_III_URL=http://app:8080
VANITY_URL=https://finance.yourdomain.com
TZ=Europe/Bucharest

The FIREFLY_III_URL value is the internal Docker bridge network address. The importer communicates with the Firefly III container over the shared firefly_iii network, using the container hostname app and internal port 8080. Setting this to your public domain name will fail because the importer cannot resolve external DNS from inside the Docker network. The VANITY_URL is what the importer displays in its own interface to generate correct redirect links pointing at your public domain.

Creating the OAuth client

After bringing up the updated stack with docker compose up -d, open Firefly III in your browser and go to your profile page. Scroll to the OAuth section and create a new client:

  • Name: Data Importer (or anything descriptive)
  • Redirect URL: https://finance.yourdomain.com/callback
  • Uncheck "Confidential"

Note the client ID that is generated (it is a small integer, typically 1 or 2). You will enter this the first time you open the importer interface. Add an Nginx proxy block for the importer. The simplest configuration is a separate subdomain or a path-based location on the same server block as Firefly III:

server {
    listen 443 ssl;
    server_name importer.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/importer.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/importer.yourdomain.com/privkey.pem;

    client_max_body_size 64M;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Importing your first bank CSV

Most banks provide CSV export from their online portal under the transaction history section. The exact column structure varies by institution, but Firefly III's Data Importer handles column mapping during the first import from each bank. You match CSV columns to Firefly III fields (date, amount, description, account IBAN), save the configuration as a JSON file, and every subsequent import from that bank reuses it. The process reduces to selecting the CSV, selecting the saved JSON configuration, and clicking import.

The importer shows a preview of parsed transactions before committing them to the database. Verify that date parsing is correct and that debit/credit direction is handled accurately for your bank's format before confirming. Some banks export amounts with a separate debit/credit column rather than signed values, and the column mapper handles both patterns.

For teams running business expense tracking on a managed VPS, Firefly III's REST API also accepts transactions directly. You can build automated import pipelines with n8n or a cron script that posts transaction data to the API endpoint, eliminating the manual CSV step entirely for accounts that support programmatic export. If you need help setting up Docker workloads or configuring the reverse proxy on a new server, ServerSpan's Linux administration service covers this type of deployment work.

Running Firefly III alongside other self-hosted services on a single VPS requires careful memory allocation and Docker network management. If you would rather focus on the application than the infrastructure, ServerSpan's managed VPS hosting includes Docker pre-configured, root SSH access, NVMe SSD storage, and technical support for deployment questions. View available plans and data center locations across Germany, Canada, and the USA.

For practical guidance on managing Docker containers and resource limits on a VPS, see our guide on Docker container management best practices. If you are sizing a new VPS for multiple self-hosted applications running concurrently, our deep-dive on Linux swap versus RAM memory management explains how to configure swap as a safety net for workloads with variable memory demand.

Source & Attribution

This article is based on original data belonging to serverspan.com blog. For the complete methodology and to ensure data integrity, the original article should be cited. The canonical source is available at: Firefly III on a VPS: Own Your Financial Data Before Your Bank's App Owns You.