Skip to content

Installation

Carbure is installed entirely from the browser: on first launch, the portal shows a wizard that checks the database, creates (or updates) its tables and the administrator account. No command is needed.

  • Docker (recommended): everything is included (nginx, PHP, woob) and the database is prepared by docker/docker-compose.yml.
  • Synology NAS: see the step-by-step tutorial.
  • YunoHost: a package installs Carbure on a YunoHost server.
  • Existing web server (PHP + MariaDB/MySQL), without Docker.

Before installing, discover Carbure's features and its comparison with other self-hosted budget managers.

Trying Carbure (demo)

The simplest way: the online demo at demo.carbure.org, with the demo / carbure account and fictitious data.

To run it at home, with a single command, no database to prepare and no installation:

docker run --rm -p 8080:80 ltoinel/carbure:demo

Open http://localhost:8080/portal/ and sign in with demo / carbure (user), shown again on the login screen. The admin administrator account gets a random password, new on every start, displayed in the container output (docker logs <container> if it runs in the background):

 User:          demo / carbure
 Administrator: admin / 3f9c1a7e5b2d8046   (new password at each start)

A fictitious household has been living there for a year: two salaries, about twenty categories with their budgets and rules, seasonal expenses, savings.

The demo image is the usual image with MariaDB and the demo data inside: on every start, the database is recreated, the configuration is written with random secrets and the data is reloaded. Ctrl+C (or docker stop) stops it.

For trying only

All data is erased on every start and the demo account password is public: do not enter any real data in it, and only publish it on the Internet with the precautions below. Bank sync and notifications are not configured. For your own budget, install Carbure with the ltoinel/carbure image (below). For safety, the demo image refuses to start on a /data volume from a real installation.

The demo users are in French; -e LANGUAGE=en (or es, it, de, pt) picks another language. Each version also has its own demo image: ltoinel/carbure:<version>-demo (for example 1.6.0-demo).

Publishing a demo

For an online demo, docker/demo/docker-compose.yml runs the hardened image: read-only file system (everything written goes to memory, erased on every start), no privilege escalation, only the capabilities needed by MariaDB, PHP-FPM and nginx, port published on 127.0.0.1 only:

docker compose -f docker/demo/docker-compose.yml up -d
docker compose -f docker/demo/docker-compose.yml logs carbure | grep Administrator

CARBURE_DEMO_PORT (8080 by default) and CARBURE_DEMO_LANGUAGE (fr by default) are set in the environment or in a .env file next to it. Put an HTTPS reverse proxy in front that forwards everything to the container, without rewriting its security headers (they depend on the path: portal, Swagger, API), and without buffering for the API (live progress):

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 https;
}
location /api/ {
    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 https;
    proxy_buffering off;
    proxy_read_timeout 3600s;
}

Visitors can change the data: a regular restart resets it and changes the admin password. The docker/demo/run.sh script (to be copied next to the compose file on the server) starts the demo, updates it when a new image is published (the container is only recreated if the image has changed), with --reset recreates it with fresh data, and displays the admin password. For example in the crontab:

0 * * * * /srv/carbure-demo/run.sh >> /var/log/carbure-demo.log 2>&1
0 4 * * * /srv/carbure-demo/run.sh --reset >> /var/log/carbure-demo.log 2>&1

The log contains the admin password: keep it readable by you only.

With Docker (recommended)

git clone https://github.com/ltoinel/Carbure.git && cd Carbure/docker
docker compose pull && docker compose up -d

docker compose pull downloads the published image of the latest release: without it, Compose would build the image from the code of the repository (docker/docker-compose.yml is also used by the tests).

Open http://<host>:8080/: the setup wizard appears, already filled in with the database from docker/docker-compose.yml. Click Continue, choose the administrator password, then Install. That's it.

Port 8080 already taken (NAS, another service)? Choose another one in docker/.env, for example CARBURE_PORT=8090, then run docker compose up -d again.

On a new database, the wizard also offers, to get you started, common categories with their categorization rules (stores, energy suppliers, telecom operators…) and insights (spending, income, rest of the month, savings, budget overrun…). These options are checked by default; everything can still be changed afterwards. Their content is in sql/starter.json.

Then add your banks from the portal's Accounts tab (+ button): choose the bank, enter the requested credentials, then the accounts to track. The credentials are handed to woob, which keeps them on your server; Carbure does not store them.

The instance settings (log level and retention, woob, mobile push notifications, email SMTP server…) can then be viewed and changed in the portal, under Administration → Settings: see Configuration.

To choose the database password yourself, create a docker/.env file next to docker/docker-compose.yml before the first start:

DB_PASSWORD=un-mot-de-passe-solide
Variable Purpose Default
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD Database offered by the wizard (the password does not need to be re-entered) db, 3306, carbure, carbure
LANGUAGE Language offered for the administrator (fr, en, es, it, de, pt) fr
SYNC_INTERVAL Automatic sync of all accounts every N seconds (86400 = once a day) disabled
TRUSTED_PROXIES Trusted reverse proxies (addresses or CIDR networks) whose X-Forwarded-For header gives the client's address, for the login rate limit and the logs; none: none (Carbure published without a proxy) private networks and loopback
CARBURE_WOOB_PATH woob command written into the configuration by the wizard; the image already sets it (env HOME=/data/woob woob) woob

The project's data/ folder, mounted on /data in the container, holds everything that is specific to your instance: the configuration written by the wizard (data/conf/prod.ini, with random secrets), the logs (data/logs) and the woob configuration with your banks (data/woob). It is the only folder to back up, along with the database. No administrator password is passed through the environment: it is chosen in the wizard.

The image contains nginx (portal, Swagger and API on port 80), PHP-FPM and woob. Its HEALTHCHECK queries GET /api/health every 5 minutes (every 10 seconds during the first 2 minutes, with Docker 25 or later): docker ps shows healthy when the database responds.

Access from the Internet

From your local network, the wizard is directly accessible. If Carbure is reached from a public address before being installed, the wizard asks for an installation code, displayed in the container logs (docker compose logs carbure) and written to /data/conf/setup.code.

Behind a reverse proxy (NAS proxy, Traefik, Caddy…), requests seem to come from the proxy, hence from the local network: install Carbure before exposing it on the Internet, or enforce the code by creating /data/conf/setup.code beforehand, containing a code of your choice.

Getting the image

The image is published on Docker Hub with every release, for amd64 and arm64 (Intel as well as ARM Synology NAS):

docker pull ltoinel/carbure:latest
Tag Contents
latest Latest release
1.2.0 A specific version (recommended in production, to control updates)
1.2 Latest patch version of 1.2

Without docker compose, with an existing MariaDB/MySQL database (the database and its user must already exist; the wizard asks for them on first launch):

docker run -d --name carbure --restart unless-stopped -p 8080:80 \
  -e SYNC_INTERVAL=86400 -v carbure-data:/data \
  ltoinel/carbure:latest

Updating

docker compose pull && docker compose up -d

The /data volume is kept. On every start, the container updates the database automatically (the new version's migrations are applied) and stops with an explicit message if one of them fails. Remember to back up the database before an update.

When docker compose pull brings a new major version of MariaDB (for example from 11 to 13), the db container upgrades the existing database when it starts (MARIADB_AUTO_UPGRADE). There is no going back: the database can no longer be opened by the old version, hence the importance of the backup. To stay on a given version, pin it in docker/docker-compose.yml (image: mariadb:11).

Upgrading to 1.11.0

  • Two migrations are applied at startup: 2026-10-28_device_without_token (a mobile app signed in without a notification token is listed in My profile → My devices) and 2026-10-29_data_change (change markers of the data). The code works before them, as before.
  • Login rate limit: nginx accepts 30 login attempts per minute and per client (/api/user/login, /api/user/login/mfa, /api/oauth/token), then answers 429. Behind a reverse proxy, the address of the client is read from X-Forwarded-For when the proxy has a private address (by default): if your proxy has another address, set TRUSTED_PROXIES, otherwise the whole household shares one limit. After the upgrade, check that the logs show the addresses of the clients. See Login rate limit.
  • Published port: CARBURE_PORT (in docker/.env) chooses the port of docker/docker-compose.yml, 8080 by default; the message of the first start gives the right port.
  • Data up to date without reloading: every answer of the API carries the versions of the data (X-Carbure-Changes header); the portal reloads the open tab when another member of the household or a synchronization changed what it shows. The mobile apps will use it in a next version.
  • Translated error messages: the portal shows the errors of the API in the language of the user. The API adds a stable code (error_code, params) to its errors, without changing the existing fields (see API).
  • Portal on a phone: the tabs of every day move to a bar at the bottom of the screen, the others, the profile, the theme and the logout to its More menu; the header fits on one line.
  • Banks: the banks asking a validation at the first connection (Crédit Mutuel…) connect from the portal; the modules that needed missing Python packages (Banque Populaire, Caisse d'Épargne, Société Générale…) load; the region of Banque Populaire is accepted. At the first bank, a warning tells that the synchronization depends on the connectors of woob.
  • Rules: the Keep the existing categories setting (off by default) limits the rules to the uncategorized transactions.
  • Calendar of the recurring transactions of the month in the Transactions tab.

Upgrading to 1.10.0

  • The 2026-10-27_budget_alert_threshold migration is applied at startup: budget alerts get a global threshold (all the budgets), which can be turned off, and thresholds per category (My profile → Notifications). The settings are kept: a user who watched one budget keeps that budget, at their threshold, without a global threshold. The code works before it, as before.
  • End-of-month salary: a received transfer dated on the 25th or later is attached to the next month (before: from the 26th). The day is set with next_month_day (Administration → Settings, 0 to never move a transfer); only the transactions imported or synchronized after the upgrade are concerned.
  • New release: a banner tells administrators when a release of Carbure is published. The server asks the public API of GitHub for the latest release once a day, sending nothing about the instance; update_check=false turns it off.
  • First installation with Docker Compose: docker compose pull && docker compose up -d uses the published image (without pull, Compose built the image from the clone).
  • Scripted installation: tools/install.php installs Carbure without the web wizard (see Run the wizard); the YunoHost package uses it.
  • The mobile apps are published: App Store and Google Play links in My profile → My devices.

Upgrading to 1.9.0

No database migration.

  • Synchronization: each account starts with the list of the accounts of its bank (woob bank list) before its transactions; some modules (Crédit Mutuel) did not find the account without it. An account the bank no longer lists ends in error, with a clear message, instead of "Account not found".
  • Currency: new optional setting currency (ISO 4217 code, EUR by default) in Administration → Settings, to show the amounts in another currency (nothing is converted).

Upgrading to 1.8.0

  • The migration 2026-10-26_user_browser is applied at startup: it records the web browsers of the portal logins, listed in My profile → My devices (the code works before it, with no browser listed).
  • The push_url setting is gone: the notification service is always Carbure Push (https://api.carbure.org), a value left in /data/conf/prod.ini is ignored. The registration of the instance, its key and the state of the service are in Administration → Mobile push.
  • The QR code of My devices gives the address of the API to the mobile apps: set public_url (Administration → Settings) if the portal is also opened through a local address.

Upgrading to 1.6.0

The 2026-10-25_device_model migration is applied at startup: model, system version and app version of each mobile device (unknown for existing devices until their next connection). It also fixes the platform of Android devices already registered, recorded as iOS until then: their push notifications start working again.

Upgrading to 1.5.0

To be done in this order:

  1. Back up the database, as before any update.
  2. Push notifications: 1.5.0 no longer sends notifications to Apple (APNs) itself but goes through the Carbure Push service. As long as the instance is not registered with it, no push notification is sent (everything else works normally). In 1.5.0, the default address of the service (https://push.carbure.org) is not the right one: first set push_url = https://api.carbure.org (the default address starting with 1.6.0; starting with 1.8.0, this setting is gone: the service is always https://api.carbure.org). Once the service is available, register the instance right after the update (see Push notifications), then remove the [apns] section and the .p8 key from /data/conf.
  3. Update the image (docker compose pull && docker compose up -d). Three migrations are applied at startup:
    • 2026-10-23_device_platform: device platform (ios for existing ones);
    • 2026-10-23_user_budget_alert_category: budget watched by budget alerts (all budgets for existing users);
    • 2026-10-24_user_mfa: two-factor authentication (empty tables).
  4. Check:
    • Administration → API shows Swagger UI: if a message says the frame is blocked, the DSM reverse proxy adds its own X-Frame-Options: DENY header (to be removed for /swagger/);
    • My profile → Notifications: the large expense threshold is now there, along with the watched budget and its threshold.

New features to enable if needed:

  • Two-factor authentication (disabled by default): before enforcing it (Administration → Settings, mfa_required), set it up on your own account (My profile) and keep your recovery codes. The current iOS app keeps signing in from an already known iPhone, but a new iPhone is rejected until the app can enter the code. If you lose the app and the codes: mfa_required=false in /data/conf/prod.ini.
  • Backup (Administration → Backup): export your data to a ZIP; importing replaces the data of each imported type.
  • Languages: Spanish, Italian, German and Portuguese. On an instance that is not in French, set the savings category for Trends in Administration → Settings (savings_category, "Epargne" by default).

Nothing changes for the label cleanup rules: the sample configuration no longer contains regex_label["/\s(CARTE.*|CARTE|CART|CAR|CA|C)$/"]="", but keep it in your prod.ini if it is there (removing it would create duplicate card transactions).

Push notifications (Carbure Push)

Notifications for the iOS and Android apps go through the external Carbure Push service: in Administration → Mobile push, check that the relay is operational, click Register the instance, read and accept the consent (device tokens and notification content go through this relay, which forwards them to Apple and Google; the API key obtained is saved in /data/conf/prod.ini), then Send a test notification (the key status changes to "Valid"). Without a key, no push notification is sent. See Configuration.

Updating from a version that sent notifications itself (APNs)

The [apns] section and the .p8 key are no longer used. Register the instance (or set push_api_key) before or right after the update, otherwise push notifications stop. The 2026-10-23_device_platform migration (device platform) is applied when the container starts.

Email notifications (SMTP)

To also send notifications by email, enter the SMTP server (host, port, encryption, credentials, sender) in Administration → Settings, section Emails (SMTP), then check it with Send a test email (see Configuration). Each user then chooses, in My profile → Notifications, which notifications they receive by email; their profile must have an email address.

Maintenance

docker compose logs -f carbure                                         # nginx, PHP and startup logs
docker compose exec carbure ls /data/logs                              # Carbure logs
docker compose exec -u www-data carbure php tools/migrate.php --status # database schema version

woob modules

woob downloads its modules (including your bank's) into /data/woob and updates them automatically. A module fix not yet published (for example for BNP) can be copied into /data/woob/.local/share/woob/modules/3.7/woob_modules/; woob never installs the Python packages a module needs (it refuses to load it): the image ships the ones of the bank modules, curl_cffi (BNP), python-jose and chompjs (Banque Populaire, Caisse d'Épargne, BTP Banque, Crédit Coopératif), schwifty (Société Générale) and pycryptodomex (HSBC employee savings, Esalia, La Nef). The modules driving a real browser (American Express, Sogecartenet, HSBC Hong Kong) are not supported: use the statement import.

With YunoHost

The carbure_ynh package installs Carbure on a YunoHost server:

  • on a domain of its own (for instance budget.example.org), not in a sub-path;
  • without the wizard: the chosen YunoHost user becomes the Carbure administrator, with the password typed at the installation; the database and the starter categories, rules and insights are created;
  • with woob and the Python packages of the bank modules in a virtualenv, and a daily synchronization of every account;
  • upgrades apply the migrations of the database and update the bank modules; the YunoHost backups include the database and the data folder (configuration, logs, woob).

Not in the YunoHost catalog

The package is not in the YunoHost catalog yet: it is installed from its repository. It was tested on a fresh installation; your feedback on an upgrade, a backup and its restoration is welcome in issue #45.

To install it, in the administration interface of YunoHost: Applications → Install, then at the bottom of the page Install a custom app with the address https://github.com/ltoinel/carbure_ynh. From the command line:

sudo yunohost app install https://github.com/ltoinel/carbure_ynh

The installation asks for the domain, who can open the web portal, the language, the YunoHost user who becomes administrator and their Carbure password (8 characters at least).

Carbure keeps its own accounts (no single sign-on with YunoHost), which the mobile apps need: the administrator password is not the YunoHost one. The API stays public for the mobile apps, the synchronization and the AI agents, even when the portal is restricted to some users. A bank that asks for a validation on the phone (strong authentication) is synchronized from the portal.

On an existing web server

Prerequisites

  • PHP 8.2 or later with mysqli, curl, openssl and json; APCu recommended (route cache).
  • MariaDB ≥ 10.3 or MySQL 8, with a database and a user for Carbure.
  • A web server able to forward /api/ to PHP-FPM, for example nginx (example below). Carbure does not ship an .htaccess file: protecting sensitive folders must be configured in the server.
  • woob installed for the web server user, with the Python packages of the bank modules, which woob does not install itself: pip install "curl_cffi>=0.7" python-jose chompjs schwifty pycryptodomex (BNP, BPCE group, Société Générale, employee savings).
  • For push notifications: outbound HTTPS access to the Carbure Push service (https://api.carbure.org; no Apple or Firebase key is needed). Optional: an SMTP server for email notifications.

1. Copy Carbure

Copy a release archive (carbure-vX.Y.Z.tar.gz: src, portal, swagger, sql, tools/migrate.php, conf/prod.sample.ini and the VERSION file shown in the portal footer) or the repository contents into the site folder, for example /var/www/carbure. The conf/ folder must be writable by the web server: the wizard writes the configuration there.

2. Configure the web server

  • /api/… URLs are sent to src/api.php (the /api prefix is stripped by the API).
  • portal/ is served as static files (for example under /portal/).
  • swagger/ can be served to browse the OpenAPI specification.
  • data/, conf/, sql/, tests/ and tools/ must not be exposed.
  • Use HTTPS (also required to install the portal as an app). Serve .webmanifest with the application/manifest+json type (see docker/nginx.conf).

nginx example (the Docker image's one is in docker/nginx.conf):

location = / { return 302 /portal/; }
location /portal/ { root /var/www/carbure; }
location /api/ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/carbure/src/api.php;
    fastcgi_pass unix:/run/php/php-fpm.sock;
    fastcgi_read_timeout 3600;   # sync can take a long time
    fastcgi_buffering off;       # SSE stream
}
location ~ ^/(data|conf|sql|tests|tools|docker)/ { deny all; }

3. Run the wizard

Open the portal (https://<your-server>/portal/): the wizard asks for the database, creates its tables (or updates an existing Carbure database, after confirming a backup), the administrator account and, on a new database, the optional starter pack (categories, rules, insights), then writes data/conf/prod.ini with random secrets (jwtsecret, sync_token…). The woob command there is woob: adjust woob_path if woob is installed elsewhere (see Configuration).

From a public address, the wizard asks for the installation code written to data/conf/setup.code (and to the PHP error log). See Security.

Install without the wizard (script)

tools/install.php does the same installation from the command line, for a script (package of a distribution, YunoHost…). The passwords go through the environment, never the command line (visible in the list of the processes):

CARBURE_DB_PASSWORD='…' CARBURE_ADMIN_PASSWORD='…' \
  php tools/install.php --db-name=carbure --db-user=carbure --admin=marie \
    --email=marie@example.org --language=en --starter

Run it as the user of the web server, which must be able to write in data/. Exit code: 0 installed, 1 failed (nothing is written), 2 already installed (data/conf/prod.ini exists: nothing changes). php tools/install.php --help lists the options (--db-host, --db-port, --woob-path, --public-url, --backup-confirmed to migrate an existing database…).

4. Add a bank

From the portal's Accounts tab (+ button): choose the bank among those supported by woob, enter the requested credentials (handed to woob, which keeps them; Carbure does not store them), then the accounts to track. woob must be installed for the web server user.

5. Schedule the sync

The sync_token key in data/conf/prod.ini lets you trigger the sync without an account. The portal's Administration → Sync tab gives the full command to copy (and creates the token if it is missing):

# crontab: every day at 7 a.m.
0 7 * * * curl -sN -H "X-Sync-Token: <sync_token>" https://exemple.fr/api/bank/sync > /dev/null
# or with wget
0 7 * * * wget -qO /dev/null --header="X-Sync-Token: <sync_token>" https://exemple.fr/api/bank/sync

Migration: the token in the URL is deprecated

A scheduled task that still calls https://exemple.fr/api/bank/sync?token=<sync_token> still works, but passing the token in the URL is deprecated (it ends up in proxy logs and shell history): a warning is logged on every call. Remove ?token=… from the URL and pass the token in the X-Sync-Token header, as above.

Updating

Replace the Carbure files while keeping the data/ folder (configuration, logs, banks). If the new version changes the database, a database update banner is shown to the administrator in the portal: back up the database, then click Update. Migrations already applied by hand (phpMyAdmin) are recognized.

From the command line, php tools/migrate.php --status shows the schema version and php tools/migrate.php applies the migrations.

After a deployment that changes the routes, the APCu cache is invalidated automatically (signature of the files in src/resources/, cache key version).

If the sync fails

In the Accounts tab, each failed step shows the cause returned by woob, including when woob returns no account or transaction but reports an error (for example Bug(bnp): 403 Client Error: Forbidden: the bank's website rejects woob): the account is then in error, not "0 new transactions". Bank websites change regularly: a woob module can stop working overnight. First of all, check whether the issue is known or being fixed in the woob issues, by searching for your bank's module name (the portal offers this link directly):

https://gitlab.com/search?group_id=11540390&project_id=25520182&scope=work_items&search=<module>&sort=created_desc

Replace <module> with the name of the woob module (for example bnp, creditmutuel, boursorama). Once the fix is published, update the bank modules from the Accounts tab (Update the bank modules button, administrator; the first time, it asks you to approve the woob repository key, see Security), or restart the Docker container: once the repository is approved, woob config update is run on every start and updates all bank modules. The woob_auto_update=true option is not enough: woob only offers a new version of a module after that module crashes, and Carbure cannot answer its prompt.

Need help?

A question about installation, configuration or usage? Check the FAQ first, then ask it on the Discord server (see Support). Keep issues for confirmed bugs, and report a security vulnerability privately (see Security).