This guide walks through running BillRun in Docker. The recommended setup runs the BillRun application from the billrun/billrun Docker Hub image (Nginx + PHP-FPM behind Supervisord, with an entrypoint that bootstraps the database and persistent data layout on first start) alongside a standalone MongoDB container.
If you don’t have docker, install it first.
sudo apt-get update
sudo apt -y install docker.io
The fastest path - run these commands in order to bring up MongoDB and BillRun with sensible defaults (no MongoDB auth, persistent data volume, automatic DB init). Each step is explained in detail in the sections that follow.
# 1. Network for the two containers to talk
docker network create billrun-net
# 2. MongoDB (skip if you already have one - see the MongoDB section below)
docker run -d \
--name billrun-mongodb \
--network billrun-net \
-v billrun-db-data:/data/db \
mongo:7.0
# 3. Persistent volume for runtime data (uploads, invoices, exports, ...)
docker volume create billrun-data
# 4. Pull the BillRun image
docker pull billrun/billrun:5.25.0
# 5. Ask for the first admin user's credentials; required only for first initial run (password input is hidden)
read -p "BillRun admin username: " BILLRUN_ADMIN_USER
read -sp "BillRun admin password: " BILLRUN_ADMIN_PASSWORD
echo
# 6. Start BillRun
docker run -d \
--name billrun-app \
--network billrun-net \
-p 8074:8074 \
-v billrun-data:/data \
-e DB_INIT=1 \
-e BR_MDB_HOST=billrun-mongodb \
-e BR_MDB_PORT=27017 \
-e BR_MDB_DBNAME=billrun \
-e BILLRUN_ADMIN_USER="$BILLRUN_ADMIN_USER" \
-e BILLRUN_ADMIN_PASSWORD="$BILLRUN_ADMIN_PASSWORD" \
billrun/billrun:5.25.0
That’s it. Once the billrun-app container settles, the UI is reachable on http://localhost:8074/ and you can log in with the credentials you just typed.
Both containers need to talk to each other by name, so create a user-defined network up front:
docker network create billrun-net
If you already have a MongoDB instance you want BillRun to use, skip this section and point BillRun at it via the BR_MDB_* env vars (see Configuration via environment variables).
Otherwise, start a standalone MongoDB 7.0 container on the network created above, with a named volume for persistence:
docker run -d \
--name billrun-mongodb \
--network billrun-net \
-v billrun-db-data:/data/db \
mongo:7.0
Use MongoDB 7.0, not 8.0. MongoDB 8.0 is incompatible with kernel 6.19; Ubuntu 26.04 ships with kernel 7.0.0, which is the environment we standardize on. Version 7.0 works across both.
BillRun keeps runtime data (uploaded files, generated invoices, exports, the shared filesystem, and per-job workspaces) under four directories inside /billrun/:
| Directory | Purpose |
|---|---|
/billrun/shared |
Shared filesystem (per-tenant workspace, generated assets) |
/billrun/files |
Uploaded files (CDR batches, attachments, etc.) |
/billrun/export |
Generated invoices, reports, and other exports |
/billrun/workspace |
Scratch space used by long-running batch jobs |
These need to survive container rebuilds and restarts. Rather than mount each one separately, mount a single host volume (or a single bind-mount path) at /data inside the container:
docker volume create billrun-data
When the container starts, the entrypoint:
/data/shared, /data/files, /data/export, /data/workspace if they don’t exist./billrun/<dir> into /data/<dir> on first boot only./billrun/<dir> with a symlink to /data/<dir>.chowns the persistent tree to www-data so PHP-FPM can write to it.The behaviour is idempotent - on subsequent boots, the directories already exist and the symlinks are already in place, so the entrypoint is effectively a no-op for this step.
Customizing the mount path inside the container. The persistent location defaults to
/data. To use a different path, set theBILLRUN_DATA_DIRenv var and mount the volume there:-e BILLRUN_DATA_DIR=/var/lib/billrun -v billrun-data:/var/lib/billrun.
Using a bind mount instead of a named volume. Replace
billrun-datawith a host path, e.g.-v /srv/billrun-data:/data. The directory structure created inside the container is identical.
Pull the BillRun image from Docker Hub:
docker pull billrun/billrun:5.25.0
The Docker Hub image is available from version 5.25.0 onward. New tags are pushed automatically by the GitLab CI/CD pipeline on each git tag, so any later release is also available at
billrun/billrun:<version>. Pin to a specific version in production rather than using:latest.
Start the container on the same network as MongoDB, mounting the persistent data volume, and supplying first-boot credentials:
docker run -d \
--name billrun-app \
--network billrun-net \
-p 8074:8074 \
-v billrun-data:/data \
-e DB_INIT=1 \
-e BR_MDB_HOST=billrun-mongodb \
-e BR_MDB_PORT=27017 \
-e BR_MDB_DBNAME=billrun \
-e BR_MDB_USER=billrun \
-e BR_MDB_PASS=change-me \
-e BR_MDB_AUTHDB=billrun \
-e BILLRUN_ADMIN_USER=admin \
-e BILLRUN_ADMIN_PASSWORD=change-me \
billrun/billrun:5.25.0
On first start, the entrypoint:
/data (see Persistent data volume)./etc/nginx/sites-available/default from a baked-in template, substituting APPLICATION_MULTITENANT (default 0) from the environment.DB_INIT=1 and runs php public/index.php --env container --dbinit, which:
config collection and seeds the base config record.BILLRUN_ADMIN_USER / BILLRUN_ADMIN_PASSWORD).application/migrations/.DB_INIT=1 is safe to leave permanently set - every step is idempotent and re-runs are near no-ops.
| Command | Effect |
|---|---|
php public/index.php --env <env> --dbinit |
Full bootstrap: schema, indexes, base data, sharding, migrations. Idempotent. |
php public/index.php --env <env> --dbmigrate |
Apply only the data-migrations step (no schema creation). |
To run manually inside a running container:
docker exec -it billrun-app php public/index.php --env container --dbmigrate
BillRun can be configured entirely through environment variables, which override anything set in host.ini (or the per-env ini files). Env values always win over *.ini config, including when the matching key is already set in host.ini.
BR_MDB_* - default database connectionApplied at config bootstrap. Maps to the standard db.* config block.
| Variable | Maps to | Notes |
|---|---|---|
BR_MDB_HOST |
db.host |
Hostname or host:port |
BR_MDB_PORT |
db.port |
|
BR_MDB_DBNAME |
db.name |
Database name |
BR_MDB_USER |
db.user |
|
BR_MDB_PASS |
db.password |
|
BR_MDB_AUTHDB |
db.options.authSource |
Database to authenticate against (typically equals BR_MDB_DBNAME) |
BR_MDB_TLS |
db.options.tls |
Boolean - true / false |
BR_MDB_TLSKEYFILE |
db.options.tlsCertificateKeyFile |
Path inside the container |
BR_MDB_TLSPASSWORD |
db.options.tlsCertificateKeyFilePassword |
|
BR_MDB_TLSCAFILE |
db.options.tlsCAFile |
|
BR_MDB_TLSINSECURE |
db.options.tlsInsecure |
|
BR_MDB_TLSINVALIDCERT |
db.options.tlsAllowInvalidCertificates |
|
BR_MDB_TLSINVALIDHOST |
db.options.tlsAllowInvalidHostnames |
BR_ADMDB_* - admin database connectionApplied at runtime when Billrun_Factory::admindb() is called. Used for cluster-admin operations: tenant DB user creation (createUser) and sharding (enableSharding, shardCollection, reshardCollection).
Resolution order:
BR_ADMDB_* env vars (always override when set)admindb config block in *.ini (e.g. admindb.user="root"; admindb.password="...")db connection - the default db user must then have sufficient cluster privileges (clusterAdmin for sharding, userAdminAnyDatabase for tenant user creation)| Variable | Maps to |
|---|---|
BR_ADMDB_HOST |
admindb.host |
BR_ADMDB_PORT |
admindb.port |
BR_ADMDB_DBNAME |
admindb.name |
BR_ADMDB_USER |
admindb.user |
BR_ADMDB_PASS |
admindb.password |
BR_ADMDB_AUTHDB |
admindb.options.authSource |
BR_ADMDB_TLS |
admindb.options.tls |
BR_ADMDB_TLSKEYFILE |
admindb.options.tlsCertificateKeyFile |
BR_ADMDB_TLSPASSWORD |
admindb.options.tlsCertificateKeyFilePassword |
BR_ADMDB_TLSCAFILE |
admindb.options.tlsCAFile |
BR_ADMDB_TLSINSECURE |
admindb.options.tlsInsecure |
BR_ADMDB_TLSINVALIDCERT |
admindb.options.tlsAllowInvalidCertificates |
BR_ADMDB_TLSINVALIDHOST |
admindb.options.tlsAllowInvalidHostnames |
Host, port, and TLS settings fall through to the default db block when not specified, so a minimal admin setup needs only BR_ADMDB_USER, BR_ADMDB_PASS, and BR_ADMDB_AUTHDB.
BILLRUN_ADMIN_* - first admin userUsed by --dbinit when seeding the first record into the users collection from mongo/first_users.json. When either variable is set and non-empty, it overrides the corresponding field; the password is hashed with password_hash(PASSWORD_DEFAULT) before insert.
| Variable | Effect |
|---|---|
BILLRUN_ADMIN_USER |
Overrides username in the first-user record |
BILLRUN_ADMIN_PASSWORD |
Overrides password (stored hashed) |
These variables only apply during the first --dbinit against a database where the users collection does not yet exist. Once users exists, the first-user step is skipped - changing these vars on a running deployment does not rotate the admin password.
DB_INIT - auto-bootstrap on container start| Variable | Effect |
|---|---|
DB_INIT=1 |
Container entrypoint runs --dbinit before starting the web server |
| unset or any other value | No automatic bootstrap |
Safe to leave permanently set: every step inside --dbinit is idempotent.
BILLRUN_DATA_DIR - persistent data mount path| Variable | Effect |
|---|---|
BILLRUN_DATA_DIR=<path> |
Path inside the container where the persistent volume is mounted. Defaults to /data. |
The four runtime directories (shared, files, export, workspace) are created and symlinked under this path by the entrypoint. See Persistent data volume.
APPLICATION_MULTITENANT - single- vs multi-tenant nginx routingThe cicd image ships nginx.conf as a template; the entrypoint renders it at container start, substituting this variable into the fastcgi_param APPLICATION_MULTITENANT line passed to PHP-FPM.
| Variable | Effect |
|---|---|
APPLICATION_MULTITENANT=0 (default) |
Single-tenant mode - the container serves one tenant whose config comes from host.ini and the configured db.* connection. |
APPLICATION_MULTITENANT=1 |
Multi-tenant mode - nginx tells PHP to resolve the tenant ini at request time. Required when the container hosts multiple tenants, e.g. when the create-tenant API is in use. |
In multi-tenant mode, configure BR_ADMDB_* (or the admindb ini block) so the create-tenant flow can create per-tenant DB users and apply sharding when applicable.
See more info about multi-tenancy here.
The recommended production setup gives sharding and tenant-user creation their own privileged credentials, while the application runs as a low-privilege user.
docker run -d \
--name billrun-app \
--network billrun-net \
-p 8074:8074 \
-v billrun-data:/data \
-e DB_INIT=1 \
-e APPLICATION_MULTITENANT=1 \
\
`# Application connection (readWrite on billrun DB)` \
-e BR_MDB_HOST=billrun-mongodb \
-e BR_MDB_DBNAME=billrun \
-e BR_MDB_USER=billrun \
-e BR_MDB_PASS="$BILLRUN_DB_PASS" \
-e BR_MDB_AUTHDB=billrun \
\
`# Admin connection (clusterAdmin + userAdminAnyDatabase)` \
-e BR_ADMDB_USER=root \
-e BR_ADMDB_PASS="$ADMIN_DB_PASS" \
-e BR_ADMDB_AUTHDB=admin \
\
`# First boot only` \
-e BILLRUN_ADMIN_USER=admin \
-e BILLRUN_ADMIN_PASSWORD="$BILLRUN_ADMIN_PASS" \
\
billrun/billrun:5.25.0
When BillRun runs against a mongos router, --dbinit and the create-tenant API automatically:
enableSharding on the target databaselines, archive, rates, billrun, balances, audit, queue) plus bills on MongoDB >= 6 and jobs_messages >= 8Sharding requires admin credentials - supply them via BR_ADMDB_* env vars or the admindb config block. On standalone or replica-set deployments, the sharding step is a transparent no-op, so no BR_ADMDB_* setup is needed.
Creating a new tenant via the API requires BR_ADMDB_* (or the admindb config) to be configured with a user that has userAdminAnyDatabase on admin - otherwise the tenant DB-user creation step fails with a permission error. The container must also be started with APPLICATION_MULTITENANT=1 so nginx routes each request to the right tenant ini at runtime.
Custom data migrations are PHP files placed under application/migrations/. Each file represents one task and is applied at most once per database (tracked via past_migration_tasks in the config collection).
The migration runner only loads files whose names match the documented convention YYYYMMDD_NNN_<JIRA_REF>.php - any other .php file dropped into the directory is skipped with a log line. This is a defense-in-depth guard against arbitrary files landing in the migrations folder via a misconfigured deploy.
See application/migrations/README.md for the full file-naming convention, the migration class skeleton, available helpers, and a cookbook of sample migrations under application/migrations/samples/.
docker stop billrun-app billrun-mongodb
To remove the containers and wipe the database:
docker rm billrun-app billrun-mongodb
docker volume rm billrun-db-data
To also wipe runtime data (uploaded files, generated invoices, exports, shared filesystem, workspaces), remove the data volume:
docker volume rm billrun-data
Removing
billrun-datais destructive - all uploaded files and generated invoices are lost. Skip this step unless you intentionally want a clean slate.