
A private budgeting stack you can operate and recover
Firefly III is a free, open-source personal finance manager that can run on infrastructure you control. This guide builds a reproducible Docker Compose deployment with PostgreSQL, scheduled processing, private loopback binding, backups, upgrades, rollback planning, and remote HTTPS access through a Localtonet HTTP tunnel. It also explains the operational work that self-hosting requires, including secret preservation, storage monitoring, proxy validation, and restore testing.
๐ What's in this guide
Why self-host a personal finance application?
A hosted budgeting service can be convenient, especially when it provides integrated bank synchronization. It also means account names, balances, transaction descriptions, categories, and spending history are stored on infrastructure governed by that provider's security, pricing, availability, and retention policies.
With Firefly III, the primary application database and uploaded files can remain on a server you operate. You choose when to update the application, where backups are stored, which integrations are enabled, and how remote users reach the service. Firefly III uses a double-entry transaction model and provides budgeting, categorization, rule, reporting, savings-goal, and API workflows for personal finance management.
Control is not the same as automatic security. A self-hosted server still needs operating-system updates, application maintenance, strong credentials, storage monitoring, tested backups, and a carefully designed public access path. Optional imports, email delivery, exchange-rate features, and other integrations can also communicate with external services when configured. Review every enabled integration rather than assuming that self-hosting means the server never makes outbound requests.
Architecture and trust boundaries

The deployment in this guide keeps Firefly III bound to 127.0.0.1:8080 on the Docker host. A Localtonet client on that host connects to the application through loopback and establishes an outbound connection to a Localtonet relay. Remote browsers use the assigned public HTTPS address. No inbound router port forwarding or public IP address is required.
Remote browser
|
| Public HTTPS
| TLS terminates at the tunnel edge
v
Localtonet public endpoint and relay
^
| Outbound Localtonet client connection
| Forwarded request metadata travels on this path
|
Localtonet client on Docker host
|
| http://127.0.0.1:8080
v
+---------------- Private Docker host ----------------+
| Firefly III container |
| APP_URL = public HTTPS URL |
| uploads -> firefly_iii_upload volume |
| |
| PostgreSQL container |
| database -> firefly_iii_db volume |
| |
| Cron container |
| calls Firefly III's protected cron endpoint |
+-----------------------------------------------------+
|
| Encrypted, access-controlled backup transfer
v
Off-host backup storage
- PostgreSQL dump
- uploaded files
- APP_KEY and other protected configuration
- Compose file and pinned image references
TLS protects the browser-facing connection to the public endpoint. Firefly III still needs the correct public APP_URL and carefully configured proxy trust so it understands that the effective external scheme is HTTPS. The origin remains private because Docker publishes the application only on the host's loopback interface.
Do not treat the absence of router port forwarding as an authorization control. Keep Firefly III authentication enabled, use a strong unique password, apply any current account-security options supported by your installed Firefly III version, and stop the tunnel when remote access is not needed. Never expose PostgreSQL through a public tunnel.
Host prerequisites and compatibility checks
Use a supported, maintained operating system on a host you administer. The host must have enough storage for the PostgreSQL database, uploaded receipts or documents, container images, temporary upgrade space, and local backup staging. Database growth varies with usage, so there is no universal minimum disk allocation.
You need:
- A currently supported Docker Engine installation.
- Docker Compose v2, invoked as
docker compose. - Administrative access to manage Docker, files, backups, and system startup.
opensslor another cryptographically secure method for generating secrets.curlfor local and public HTTP verification.- A second device or network for verifying the real public path.
- A protected off-host destination for encrypted backups.
Verify Docker and Compose
docker version
docker compose version
docker info
Resolve any daemon, permission, or storage-driver errors before continuing. Membership in the Docker group generally grants extensive control over the host, so limit it to trusted administrators.
Confirm the host architecture
uname -m
docker info --format '{{.Architecture}}'
Common values include x86_64 or amd64 and aarch64 or arm64. Before deployment, confirm that every chosen image tag publishes a manifest for the host architecture:
docker buildx imagetools inspect fireflyiii/core:latest
docker buildx imagetools inspect postgres:16
docker buildx imagetools inspect alpine:latest
Do not assume that an image available for ARM64 also supports an older 32-bit ARM device. If docker buildx is unavailable, inspect the image through your registry tooling before pulling it.
Read the current installation guidance
Firefly III can change its supported environment variables, release requirements, and container workflow. Compare this tutorial with the current official Firefly III Docker installation documentation before deploying a new version. Use release notes to determine whether a newer release changes the database, PHP, proxy, or scheduled-task requirements.
A moving tag is useful for discovering a current image, but it is not a reproducible deployment reference. The workflow below pulls candidate tags, records their immutable repository digests, and places those digest references in the protected environment file. Repeat that process deliberately when upgrading.
Install Firefly III with Docker Compose
This deployment includes Firefly III, PostgreSQL, and a lightweight cron service. PostgreSQL and uploads use named volumes. The names are explicit so backup commands do not depend on the directory-derived Compose project name.
Create a protected deployment directory
Choose a filesystem location included in your administrative backup policy. Restrict newly created files before generating secrets.
mkdir -p "$HOME/firefly-iii"
cd "$HOME/firefly-iii"
umask 077
Pull candidate images and capture immutable digests
Start with versions allowed by the current Firefly III documentation. PostgreSQL major-version upgrades require separate planning, so this example stays within PostgreSQL 16. Test the resulting stack before treating it as production.
docker pull fireflyiii/core:latest
docker pull postgres:16
docker pull alpine:latest
FIREFLY_IMAGE="$(docker image inspect \
--format '{{index .RepoDigests 0}}' fireflyiii/core:latest)"
POSTGRES_IMAGE="$(docker image inspect \
--format '{{index .RepoDigests 0}}' postgres:16)"
CRON_IMAGE="$(docker image inspect \
--format '{{index .RepoDigests 0}}' alpine:latest)"
printf '%s\n' "$FIREFLY_IMAGE" "$POSTGRES_IMAGE" "$CRON_IMAGE"
Each result should contain @sha256:. An immutable digest makes a later pull resolve to the same image content. Keep the human-readable release version in your change record as well, because a digest alone is difficult to audit.
Generate protected application, database, and cron secrets
The APP_KEY protects application data and must survive every backup, restore, upgrade, and migration. The cron token prevents arbitrary callers from invoking the scheduled endpoint. Hexadecimal output avoids Compose interpolation problems caused by dollar signs in manually entered passwords.
APP_KEY="$(openssl rand -hex 16)"
DB_PASSWORD="$(openssl rand -hex 32)"
STATIC_CRON_TOKEN="$(openssl rand -hex 32)"
cat > .env <<EOF
FIREFLY_IMAGE=${FIREFLY_IMAGE}
POSTGRES_IMAGE=${POSTGRES_IMAGE}
CRON_IMAGE=${CRON_IMAGE}
APP_KEY=${APP_KEY}
DB_PASSWORD=${DB_PASSWORD}
STATIC_CRON_TOKEN=${STATIC_CRON_TOKEN}
APP_URL=http://localhost:8080
EOF
chmod 600 .env
unset APP_KEY DB_PASSWORD STATIC_CRON_TOKEN
Do not commit .env to a public repository. Store an encrypted recovery copy separately because losing APP_KEY can make encrypted application data unusable.
Create the Compose definition
The health check prevents Firefly III from starting before PostgreSQL reports that it is ready. It does not replace application-level verification.
services:
db:
image: ${POSTGRES_IMAGE}
restart: unless-stopped
environment:
POSTGRES_DB: firefly
POSTGRES_USER: firefly
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U firefly -d firefly"]
interval: 10s
timeout: 5s
retries: 12
volumes:
- firefly_db:/var/lib/postgresql/data
app:
image: ${FIREFLY_IMAGE}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
APP_KEY: ${APP_KEY}
APP_URL: ${APP_URL}
DB_CONNECTION: pgsql
DB_HOST: db
DB_PORT: 5432
DB_DATABASE: firefly
DB_USERNAME: firefly
DB_PASSWORD: ${DB_PASSWORD}
STATIC_CRON_TOKEN: ${STATIC_CRON_TOKEN}
ports:
- "127.0.0.1:8080:8080"
volumes:
- firefly_upload:/var/www/html/storage/upload
cron:
image: ${CRON_IMAGE}
restart: unless-stopped
depends_on:
- app
environment:
STATIC_CRON_TOKEN: ${STATIC_CRON_TOKEN}
command: >-
sh -c "echo '0 3 * * * wget -qO-
http://app:8080/api/v1/cron/'\"$${STATIC_CRON_TOKEN}\"
| crontab - && crond -f -L /dev/stdout"
volumes:
firefly_db:
name: firefly_iii_db
firefly_upload:
name: firefly_iii_upload
The doubled dollar sign in $${STATIC_CRON_TOKEN} tells Compose not to substitute that value while parsing the file. The shell inside the cron container expands it at runtime.
Validate the rendered configuration
Have Compose check the file before starting anything. The rendered output includes resolved secrets, so do not paste it into a ticket, chat, screenshot, or public log.
docker compose config --quiet
If you need to inspect the full rendered configuration, do it only in a protected terminal. A dollar sign in a manually chosen secret can be interpreted as Compose interpolation unless it is escaped. The generated hexadecimal secrets avoid that specific failure mode.
Start PostgreSQL and confirm readiness
Start the database first so initialization and readiness are easy to diagnose.
docker compose up -d db
docker compose ps
docker compose logs --tail=100 db
docker compose exec db pg_isready -U firefly -d firefly
Continue only after pg_isready reports that PostgreSQL accepts connections and the logs show no repeated initialization, permission, or storage errors.
Start Firefly III and the cron service
docker compose up -d app cron
docker compose ps
docker compose logs --tail=150 app
docker compose logs --tail=50 cron
First startup can take longer while the application initializes and applies required database changes. Do not repeatedly restart the stack while initialization is still progressing.
Verify the local HTTP service
Test from the Docker host before introducing a public tunnel.
curl --fail --show-error --location \
--output /dev/null \
--write-out 'HTTP %{http_code}\n' \
http://127.0.0.1:8080/
A successful final HTTP response confirms that the web route is reachable. Also open http://127.0.0.1:8080 in a browser on the host or through a trusted administrative path.
Complete initial account setup
Use the Firefly III registration or initialization screen presented by your installed version to create the first account. Choose a strong, unique password. Complete the basic account and currency configuration, then add only a small test transaction or test account until backup and recovery have been verified.
After initialization, inspect the application logs once more:
docker compose logs --since=10m app
docker compose ps
Investigate database exceptions, repeated migrations, file-permission errors, or application-key warnings before importing real financial history.
The database and upload volumes contain the data you are trying to protect. Inspect logs, configuration, permissions, available disk space, and database readiness first. Remove a volume only when intentionally destroying a disposable installation or after completing and verifying a recovery plan.
Configure and verify scheduled processing
Firefly III uses scheduled processing for recurring transactions and other time-based application work. Starting only the web and database containers leaves that work incomplete. The cron service in this guide invokes Firefly III's protected cron endpoint every day at 03:00 according to the cron container's clock.
The application and cron service must receive the same STATIC_CRON_TOKEN. Keep it secret. Review the current Firefly III cron documentation for the schedule and invocation method required by your installed release.
Test the cron endpoint from inside the Docker network
docker compose exec cron sh -c \
'wget -qO- "http://app:8080/api/v1/cron/${STATIC_CRON_TOKEN}"'
Inspect both services after the test:
docker compose logs --since=5m cron
docker compose logs --since=5m app
A successful network request alone does not prove that every recurring item was due or created. Confirm the expected result in Firefly III and review the application response and logs. If scheduling must follow a specific local time, configure and verify the container time zone according to the base image and your operational policy. Do not assume the container clock matches the host's displayed local time.
Publish Firefly III with a Localtonet HTTP tunnel
Once local operation is stable, a Localtonet HTTP tunnel can provide a public HTTPS address. Our client establishes an outbound connection to a Localtonet relay, so this workflow does not require inbound router port forwarding, firewall changes, VPN setup, or a public IP address. The tunnel is available only while the selected client is connected and the tunnel is running.
Use the current Localtonet HTTP tunnel documentation alongside the sequence below for operating-system-specific client installation and the current dashboard interface.
Install and run the Localtonet client
Install the current client on the Docker host. Keeping the client on the same host allows it to reach Firefly III through 127.0.0.1. If you place it on another device, loopback will not reach the Docker host and you must design a restricted private-network path instead.
Create an HTTP tunnel and select the process type
Open the HTTP tunnel configuration and choose Random Sub Domain, Custom Sub Domain, or Custom Domain according to the options currently available to your account. These process types provide a public HTTPS address for the same local HTTP service. Check current documentation before changing DNS for a custom domain.
Select the correct device AuthToken
Select the device-specific token for the connected client on the Docker host. Treat the token as a secret. Never place it in the Compose file, article screenshots, a public repository, or shared command output.
Select an available relay server
Choose a currently available relay server or region from the dashboard. Available values can vary by account, plan, client version, region, or deployment, so do not copy a hardcoded server code from an older tutorial.
Set the local target
Enter 127.0.0.1 as the local IP address and 8080 as the local port when the Localtonet client runs on the Docker host. Confirm that curl http://127.0.0.1:8080/ succeeds from that same host.
Start the tunnel
Creating the tunnel does not mean it is running. Press Start and copy the exact assigned public HTTPS address. You can later stop or delete the tunnel when it is no longer required.
Update Firefly III's public application URL
Edit .env and replace the local value with the complete public HTTPS origin, without adding an unrelated path:
APP_URL=https://your-assigned-public-hostname.example
The hostname above is a placeholder. Use the exact URL shown for your running tunnel. Recreate the application container so it receives the changed environment value:
docker compose up -d --force-recreate app
docker compose logs --tail=100 app
The cron service communicates through Docker's private service name and does not need to call the public URL.
Configure and validate reverse-proxy behavior
Firefly III uses APP_URL when generating URLs and interpreting its external location. A mismatch can produce HTTP redirects from an HTTPS page, incorrect links, cookie problems, failed form submissions, or repeated login behavior.
A tunnel or reverse proxy may also send forwarded headers describing the original public scheme and host. Firefly III must trust only the proxy path that is permitted to provide those values. Review the current Firefly III reverse-proxy documentation for the syntax supported by your installed version.
A value such as TRUSTED_PROXIES=** trusts forwarded information from every source that can reach the application. Do not add it merely because an older example used it. Prefer the narrow proxy address or network supported by the current Firefly III documentation and observed in your deployment. If broad trust is temporarily unavoidable, keep the origin strictly loopback-bound, ensure no untrusted process can reach it, document the exception, and revisit it before treating the deployment as production.
The exact source address seen by the application can depend on Docker networking, the Localtonet client location, and any additional proxy layer. Do not guess a public relay address and place it in the trusted-proxy list. The relay does not connect directly to the container in this architecture. Determine the immediate proxy peer at the application boundary using current Firefly III guidance and your own protected logs, then configure the smallest supported address or network.
Verify the actual public HTTPS path
Run the first test from a device that is not the Docker host. A phone using cellular data is a practical option because it avoids accidentally testing through the host's local network path.
curl --fail --show-error --location \
--output /dev/null \
--write-out 'final_url=%{url_effective}\nhttp=%{http_code}\n' \
https://your-assigned-public-hostname.example/
The final URL should remain on the expected HTTPS hostname. Inspect response headers without automatically following redirects:
curl --silent --show-error --dump-header - \
--output /dev/null \
https://your-assigned-public-hostname.example/
Check for these conditions:
- No redirect points to
http://,localhost,127.0.0.1, a Docker service name, or an unexpected hostname. - Generated links and form destinations stay on the public HTTPS origin.
- Cookies that require HTTPS are issued with appropriate secure attributes for your Firefly III version and login flow.
- Login, logout, navigation, and form submission work through the public address.
- The browser reports a valid HTTPS connection for the hostname actually used.
- The application is not separately reachable through the host's LAN or public interfaces on port 8080.
Use browser developer tools to inspect the Network panel during login and form submission. Confirm that request URLs, redirect locations, and cookies consistently use the public HTTPS origin. This end-to-end check is more useful than assuming forwarded headers are correct because the first page loaded.
Confirm the origin is still private
docker compose port app 8080
ss -lnt | grep ':8080'
The published listener should remain associated with 127.0.0.1:8080, not 0.0.0.0:8080 or [::]:8080. From another LAN device, a direct request to the Docker host on port 8080 should not succeed under this design.
Import bank data deliberately
Firefly III's Data Importer is a separate component. It supports documented file-based workflows and can work with supported external financial-data providers when explicitly configured. Provider availability, authentication, supported institutions, and regional behavior can change, so consult the current Firefly III Data Importer Docker documentation before adding it.
Start with a small export from your bank rather than importing years of transactions immediately. Review date formats, decimal separators, account mappings, duplicate handling, and rule behavior. Keep the original source file until the imported result and a subsequent backup have been verified.
An external data provider creates a different privacy boundary from a manual file import. Review what financial and authentication data the provider receives, how credentials or tokens are stored, and whether the provider's terms meet your requirements. Do not expose importer credentials in Compose output, shell history, or screenshots.
Back up and restore the complete deployment

A usable Firefly III recovery set includes more than PostgreSQL. Preserve all of the following:
The following procedure creates a PostgreSQL custom-format dump, a compressed upload archive, a protected copy of the deployment files, and checksums. Review the current Firefly III backup documentation before adapting it to automation.
Create a backup
cd "$HOME/firefly-iii"
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
backup_dir="$PWD/backups/$stamp"
umask 077
mkdir -p "$backup_dir"
docker compose exec -T db \
pg_dump -U firefly -d firefly -Fc \
> "$backup_dir/firefly.pg_dump"
docker compose exec -T app \
tar -C /var/www/html/storage/upload -czf - . \
> "$backup_dir/firefly-upload.tgz"
cp --preserve=mode .env docker-compose.yml "$backup_dir/"
docker compose ps > "$backup_dir/compose-status.txt"
sha256sum \
"$backup_dir/firefly.pg_dump" \
"$backup_dir/firefly-upload.tgz" \
"$backup_dir/.env" \
"$backup_dir/docker-compose.yml" \
> "$backup_dir/SHA256SUMS"
chmod -R go-rwx "$backup_dir"
If the application image used by your release does not contain tar, use a trusted volume-aware backup tool instead of installing software into the running application container. Keep the resulting archive associated with the matching database dump.
Copy the backup to encrypted off-host storage. A backup kept only on the same disk as the application does not protect against disk failure, theft, filesystem corruption, or host loss. Apply access controls appropriate for financial data and secrets.
Verify backup integrity
cd "$backup_dir"
sha256sum -c SHA256SUMS
pg_restore --list firefly.pg_dump > /dev/null
tar -tzf firefly-upload.tgz > /dev/null
The local machine running the verification needs compatible PostgreSQL client tools for pg_restore. Checksums and archive listings detect some failures, but only a complete isolated restore proves that the recovery set is sufficient.
Restore into an isolated deployment
Never test recovery over the only production instance. Use an isolated host or a separate Compose project with no public tunnel. First restore the protected .env and Compose file. Confirm that the original APP_KEY is present before starting Firefly III.
After starting only PostgreSQL, replace the empty database and restore the dump:
docker compose up -d db
docker compose exec db pg_isready -U firefly -d firefly
docker compose exec -T db dropdb -U firefly firefly
docker compose exec -T db createdb -U firefly -O firefly firefly
docker compose exec -T db \
pg_restore -U firefly -d firefly --no-owner \
< firefly.pg_dump
Restore the upload volume before starting the application:
docker compose run --rm --no-deps \
--entrypoint sh app \
-c 'find /var/www/html/storage/upload -mindepth 1 -delete;
tar -xz -C /var/www/html/storage/upload' \
< firefly-upload.tgz
Then start the application and cron service:
docker compose up -d app cron
docker compose ps
docker compose logs --tail=150 app
Verify login, account balances, a sample of historical transactions, rules, scheduled processing, uploaded attachments, and generated links. Keep the isolated restore private. If a backup contains real financial data, the test environment needs the same confidentiality controls as production.
A database-only restore can leave attachments missing. Restoring the database and uploads without the original APP_KEY can leave encrypted values unreadable. Preserve PostgreSQL, uploads, APP_KEY, required secrets, deployment configuration, and tested image references as one coordinated recovery set.
Upgrade, roll back, and recover after reboots
Perform a controlled upgrade
Read the current Firefly III upgrade documentation and release notes before changing an image. Create and verify a complete backup first.
- Record the current Firefly III digest from
.env. - Create a PostgreSQL, upload, secret, and configuration backup.
- Pull the intended release tag.
- Inspect and record its immutable digest.
- Replace only
FIREFLY_IMAGEin.env. - Validate the Compose configuration.
- Recreate the application container.
- Inspect logs and run local, cron, and public HTTPS tests.
docker pull fireflyiii/core:RELEASE_TAG
docker image inspect \
--format '{{index .RepoDigests 0}}' \
fireflyiii/core:RELEASE_TAG
docker compose config --quiet
docker compose up -d --force-recreate app
docker compose logs --tail=200 app
RELEASE_TAG is intentionally a placeholder because the supported current release changes. Select it from current Firefly III release guidance, test it, and place the resulting digest in .env.
Plan rollback before upgrading
Reverting only the application image may be unsafe if the newer release changed the database schema or stored data. A dependable rollback restores the complete pre-upgrade recovery set:
- The previous pinned Firefly III image reference.
- The matching pre-upgrade PostgreSQL dump.
- The matching upload archive.
- The original APP_KEY and deployment configuration.
- The previous proxy and public URL settings.
If post-upgrade writes occurred, rolling back to the earlier database backup discards those writes. Announce a maintenance window or otherwise prevent user changes while performing a high-risk upgrade.
Understand PostgreSQL credential changes
POSTGRES_PASSWORD initializes the database user only when PostgreSQL creates a new data directory. Changing DB_PASSWORD in .env does not rotate the password inside an already initialized database volume.
To rotate it, change the database role password first using an interactive prompt, then update .env to the same value and recreate Firefly III:
docker compose exec db \
psql -U firefly -d firefly -c '\password firefly'
chmod 600 .env
docker compose up -d --force-recreate app
docker compose logs --tail=100 app
The interactive command avoids placing the new password directly in shell history. Keep the old recovery set until the new credential has been tested and a fresh backup has completed.
Verify recovery after a reboot
The Compose services use restart: unless-stopped. Docker must also start with the operating system. Localtonet availability separately depends on the selected client being connected and the tunnel running.
After a reboot, check every layer:
docker compose ps
docker compose logs --since=15m db
docker compose logs --since=15m app
docker compose logs --since=15m cron
curl --fail --show-error \
--output /dev/null \
http://127.0.0.1:8080/
Then verify the Localtonet device status, confirm the tunnel is running, and test the public HTTPS address from an external network. Do not assume that a running container means the database, application, scheduled processing, and public route are all healthy.
Monitor logs and disk use
docker compose logs --tail=200 db app cron
df -h
df -i
docker system df
docker volume inspect firefly_iii_db
docker volume inspect firefly_iii_upload
df -i detects inode exhaustion, which can prevent writes even when byte-based disk reports appear acceptable. Avoid indiscriminate cleanup commands. In particular, do not prune volumes that may contain the database or uploads. Maintain enough free space for PostgreSQL operations, image pulls, upgrades, temporary files, and backup creation.
Firefly III compared with hosted budgeting services
| Area | Hosted budgeting service | Self-hosted Firefly III |
|---|---|---|
| Software and infrastructure | The provider operates the application and underlying infrastructure. | Firefly III is open-source software under AGPLv3, while you provide and maintain the host, storage, and network access. |
| Primary data location | Provider-controlled infrastructure and any connected processors. | Your PostgreSQL database and upload storage, subject to external integrations you choose to enable. |
| Accounting model | Varies by service. | Double-entry transaction model with personal finance organization and reporting tools. |
| Bank-data workflow | Often integrated into the hosted product. | Separate Data Importer using supported files or explicitly configured providers. |
| Updates and recovery | Primarily handled by the provider. | You maintain images, operating-system updates, PostgreSQL, backups, restores, monitoring, and rollback plans. |
| Remote access | Usually included as part of the hosted service. | You design and secure the access path, such as a Localtonet HTTP tunnel to a private origin. |
The practical trade-off is control versus operational responsibility. Firefly III lets you control storage, retention, updates, and integrations, but it also makes you responsible for maintenance and recovery. A hosted service may be a better fit when someone does not want to administer a database-backed web application.
Troubleshooting common deployment problems
| Symptom | Likely cause | Safe diagnostic or fix |
|---|---|---|
| Application reports a missing encryption key | APP_KEY is absent, malformed, or not loaded from .env. |
Check file permissions and run docker compose config --quiet. Restore the original APP_KEY for an existing deployment rather than generating a replacement. |
| PostgreSQL is repeatedly unhealthy | Initialization is incomplete, credentials are inconsistent, storage is full, or the data directory has a permission or corruption problem. | Inspect docker compose logs db, run pg_isready, check disk and inode capacity, and preserve the volume while investigating. |
| Firefly III starts before the database is usable | The database container is running but not ready to accept connections. | Keep the PostgreSQL health check and conditional dependency, then inspect both service logs. A running status is not the same as readiness. |
| Changing DB_PASSWORD breaks application login to PostgreSQL | The environment file changed, but the existing PostgreSQL role password did not. | Use PostgreSQL's \password command to rotate the role credential, update .env to match, and recreate the application container. |
| A secret containing a dollar sign is altered | Compose interpreted part of the value as variable interpolation. | Use safely generated hexadecimal secrets or escape values according to Compose rules. Validate the configuration without publishing rendered secret output. |
| Uploads fail or attachments disappear | The upload volume is not mounted, is not writable, or was omitted during restore. | Inspect the volume mount and application logs. Restore the upload archive that belongs with the database backup. Do not replace the volume blindly. |
| Container exits with permission errors | A bind mount or restored file has ownership or mode incompatible with the application image. | Compare the current image's documented ownership requirements, inspect file metadata, and correct only the affected path. Avoid recursively changing permissions without understanding the expected owner. |
| No space left on device | The filesystem or inode table is full because of database growth, logs, images, uploads, or backup staging. | Run df -h, df -i, and docker system df. Move verified backups off-host and remove only identified disposable data. Never prune persistent volumes casually. |
| Recurring transactions do not run | The cron service is stopped, its token differs from Firefly III, the endpoint is unreachable, or the schedule uses an unexpected time zone. | Invoke the endpoint from the cron container, compare protected token configuration, inspect both logs, and verify the container clock and expected application result. |
| Public URL redirects to HTTP or localhost | APP_URL is wrong or the effective scheme and host are not being interpreted correctly. |
Set APP_URL to the exact public HTTPS origin, recreate the application, and validate redirects and cookies through the real tunnel. |
| Local access works but the public URL does not | The Localtonet client is disconnected, the tunnel is stopped, the wrong AuthToken is selected, or the local target is incorrect. | Confirm the client device is connected, verify 127.0.0.1:8080 from that device, check the selected token and relay, and press Start for the tunnel. |
| The image does not start on ARM hardware | The pinned image does not publish a manifest for the host architecture. | Inspect the image manifest and select a Firefly III-supported tag for the exact architecture. Do not force an incompatible image into production. |
Useful diagnostic sequence
docker compose config --quiet
docker compose ps
docker compose logs --tail=200 db
docker compose exec db pg_isready -U firefly -d firefly
docker compose logs --tail=200 app
docker compose logs --tail=100 cron
curl -I http://127.0.0.1:8080/
df -h
df -i
docker system df
Work from the innermost dependency outward: storage, PostgreSQL, Firefly III, scheduled processing, local HTTP, Localtonet client, running tunnel, and finally the public HTTPS path. This isolates failures without destroying data.
Frequently asked questions
Does Firefly III connect to my bank automatically?
The core application does not provide every bank connection by itself. The separate Firefly III Data Importer handles documented file imports and optional supported financial-data providers. Provider support, regions, credentials, and capabilities can change, so verify them in the current Data Importer documentation.
Why does the deployment need a cron service?
Firefly III relies on scheduled processing for recurring transactions and other time-based work. The cron service calls the protected endpoint with STATIC_CRON_TOKEN. Without a working scheduler, the web interface may load normally while scheduled work does not run as expected.
Is Firefly III free?
Firefly III is open-source software licensed under AGPLv3. Hosting still has potential costs for hardware, electricity, storage, domains, backups, network service, and optional third-party integrations.
Can I access Firefly III from a phone?
Yes. While the selected Localtonet client is connected and the HTTP tunnel is running, open the assigned public HTTPS address in a phone browser. Keep application authentication enabled and test the public route from a network outside the Docker host.
Does creating a Localtonet tunnel make it immediately available?
No. Creating the configuration and starting it are separate lifecycle actions. Select the correct connected device, configure the local target, select an available relay, and press Start. The tunnel remains available only while the selected client is connected and the tunnel is running.
Why is port 8080 bound to 127.0.0.1?
The loopback binding lets a Localtonet client on the same host reach Firefly III without publishing the origin on every LAN or public interface. It reduces the chance that someone can bypass the intended tunnel path and connect directly to the application port.
Should I set TRUSTED_PROXIES to two asterisks?
Not as a general default. Broad proxy trust accepts forwarded information from every source that can reach the application. Follow the current Firefly III reverse-proxy documentation and configure the narrowest immediate proxy address or network supported by your deployment. Keep the origin private even when proxy trust is scoped.
What exactly must I back up?
Back up PostgreSQL, the upload volume, the original APP_KEY, required secrets, the Compose file, exact image references, APP_URL, and relevant proxy configuration. Encrypt off-host copies and test a complete restore in an isolated environment.
Can I change the PostgreSQL password by editing .env?
Not for an already initialized database. PostgreSQL uses POSTGRES_PASSWORD during initial data-directory creation. Rotate the existing role password inside PostgreSQL, update .env to match, recreate the application container, and verify a fresh backup.
Does self-hosting guarantee that no data leaves my server?
No. The primary database and uploads can remain on your infrastructure, but enabled imports, email delivery, exchange-rate features, remote access, backup destinations, or other integrations may communicate with external systems. Review every configured integration and its privacy implications.
Publish your private Firefly III origin when you need it
Deploy and verify Firefly III locally, protect its database and recovery secrets, then use a Localtonet HTTP tunnel for remote HTTPS access without opening an inbound router port.
Get Started Free โ