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:
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):
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:
| 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):
| 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¶
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) and2026-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 answers429. Behind a reverse proxy, the address of the client is read fromX-Forwarded-Forwhen the proxy has a private address (by default): if your proxy has another address, setTRUSTED_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(indocker/.env) chooses the port ofdocker/docker-compose.yml,8080by 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-Changesheader); 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_thresholdmigration 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,0to 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=falseturns it off. - First installation with Docker Compose:
docker compose pull && docker compose up -duses the published image (withoutpull, Compose built the image from the clone). - Scripted installation:
tools/install.phpinstalls 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,EURby 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_browseris 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_urlsetting is gone: the notification service is always Carbure Push (https://api.carbure.org), a value left in/data/conf/prod.iniis 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:
- Back up the database, as before any update.
- 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 setpush_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 alwayshttps://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.p8key from/data/conf. - Update the image (
docker compose pull && docker compose up -d). Three migrations are applied at startup:2026-10-23_device_platform: device platform (iosfor 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).
- Check:
- Administration → API shows Swagger UI: if a message says the frame is
blocked, the DSM reverse proxy adds its own
X-Frame-Options: DENYheader (to be removed for/swagger/); - My profile → Notifications: the large expense threshold is now there, along with the watched budget and its threshold.
- Administration → API shows Swagger UI: if a message says the frame is
blocked, the DSM reverse proxy adds its own
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=falsein/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:
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,opensslandjson; 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.htaccessfile: 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 tosrc/api.php(the/apiprefix 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/andtools/must not be exposed.- Use HTTPS (also required to install the portal as an app).
Serve
.webmanifestwith theapplication/manifest+jsontype (seedocker/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).