Skip to main content

Docker Installation (Recommended)

The fastest way to run Invoicerr is using Docker Compose. A prebuilt image is available at ghcr.io/invoicerr-app/invoicerr.

Supported architectures​

  • linux/amd64 (x86_64)
  • linux/arm64/v8 (ARMv8)
32-bit ARM is no longer supported

linux/arm/v7 was dropped from the image's multi-arch manifest. Node.js stopped publishing linux-armv7l binaries starting with Node 24, which both this image and its base image (server-image) now run on. If you were running Invoicerr on a 32-bit ARM board (Raspberry Pi 2/3/Zero on the 32-bit OS), install a 64-bit OS on the same board and pull the image again: any board capable of 64-bit Debian (Pi 3 and newer) runs the linux/arm64/v8 image unchanged.

Quick start​

  1. Create a docker-compose.yml file with the following content, then adjust the environment variables to your setup:

    docker-compose.yml
    services:
    invoicerr:
    image: ghcr.io/invoicerr-app/invoicerr:latest
    ports:
    - "80:80"
    environment:
    - DATABASE_URL=postgresql://invoicerr:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in a .env file next to this one}@invoicerr_db:5432/invoicerr_db
    - APP_URL=https://invoicerr.example.com
    - CORS_ORIGINS=http://localhost:5173,https://invoicerr.example.com

    # Redis — required (BullMQ queues the document-send jobs). No password on a
    # single-host Docker network by default; set REDIS_PASSWORD on both services to add one.
    - REDIS_HOST=redis
    - REDIS_PORT=6379

    # Email delivery - see "Email delivery" below for the Resend alternative (Brevo's own SMTP
    # relay also works with the SMTP settings below)
    - SMTP_HOST=smtp-relay.example.com
    - SMTP_USER="username@example.com"
    - SMTP_FROM="user-from@example.com"
    - SMTP_PASSWORD="your_smtp_password"
    - SMTP_PORT=587
    - SMTP_SECURE=false

    - BETTER_AUTH_SECRET="your_better_auth_secret"
    depends_on:
    - invoicerr_db
    - redis

    invoicerr_db:
    image: postgres:15
    environment:
    POSTGRES_USER: invoicerr
    # Postgres applies this only on first init of an empty volume.
    POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in a .env file next to this one}
    POSTGRES_DB: invoicerr_db
    volumes:
    - db_data:/var/lib/postgresql/data

    redis:
    image: bitnami/redis:latest
    environment:
    ALLOW_EMPTY_PASSWORD: "yes"
    volumes:
    - redis_data:/bitnami

    volumes:
    db_data:
    driver: local
    redis_data:
    driver: local

    The app will not boot at all without a reachable Redis — it is not optional, and there is no degraded fallback if it is missing.

  2. Run the app:

    docker compose up -d
  3. Open your browser at http://localhost.

tip

The repository's docker-compose.yml also includes an OIDC example (placeholder values, live rather than commented out — set OIDC_CLIENT_ID to actually register the provider, or remove the whole block if you don't want single sign-on).

Updating​

docker compose pull && docker compose up -d picks up a new image and re-applies pending database migrations automatically on the container's next boot — same as any other restart.

Upgrading from a version with no POSTGRES_PASSWORD

Versions before this change hardcoded the database password to invoicerr. Postgres only applies POSTGRES_PASSWORD the first time it initialises its volume, so an existing deployment's database still has the old password regardless of what you now set. Pick one:

  • Set POSTGRES_PASSWORD=invoicerr in your .env to match what the database already has, and move on. You can change it later with the option below.
  • Pick a new password, connect to the running database (docker compose exec invoicerr_db psql -U invoicerr) and run ALTER USER invoicerr PASSWORD 'your-new-password';, then set that same value as POSTGRES_PASSWORD in your .env before the next docker compose up -d.

Setting a POSTGRES_PASSWORD that does not match what is actually inside the database makes the app fail to connect; it does not reset anything inside Postgres by itself.

One thing that is not automatic: if a release actually drops a country from one of the document-action policy / identifier-requirements / B2G-routing catalogs, removing that country's database rows is a deliberate, separate step — every automatic boot path only ever adds/updates rows, on purpose (so a rolling multi-container setup, or simply restarting on a half-pulled image, can never delete a country a newer container already seeded). Run this once, after the new container is up, whenever a release note says a country was removed:

docker compose exec invoicerr sh -c "cd /usr/share/nginx/backend && npm run catalogs:release"

(swap invoicerr for your own service name if you renamed it in docker-compose.yml). It is idempotent — safe to run on every update regardless of whether that particular release actually removed a country.

Environment variables​

These are set under the invoicerr service's environment key.

VariableDescription
DATABASE_URLPostgreSQL connection string, built from POSTGRES_PASSWORD below, e.g. postgresql://invoicerr:${POSTGRES_PASSWORD}@invoicerr_db:5432/invoicerr_db
POSTGRES_PASSWORDRequired. The invoicerr_db service's own password, generated with e.g. openssl rand -hex 24. Postgres sets it only once, the first time the database volume is initialised; see the upgrade warning below before changing it on an existing deployment.
APP_URLFull public URL of the frontend (e.g. https://invoicerr.example.com). Required for email templates and links, and used as better-auth's own base URL.
REDIS_HOST / REDIS_PORT / REDIS_PASSWORDRequired — the app refuses to boot without a reachable Redis (BullMQ queues every document-send job). Defaults to localhost:6379, no password, if unset; REDIS_URL (e.g. redis://:pass@redis:6379) is accepted instead and wins when both are set.
BETTER_AUTH_SECRETThe one that's actually documented and recommended — any random string, e.g. openssl rand -hex 32. Leaving it (and JWT_SECRET below) unset falls back to a known default, which the app flags as insecure.
JWT_SECRETA legacy fallback only — better-auth reads BETTER_AUTH_SECRET || JWT_SECRET. Set BETTER_AUTH_SECRET instead for anything new; this one exists so an older deployment that only ever set JWT_SECRET keeps working unchanged.
DEFAULT_LOCALEOptional. Instance-wide fallback language (en, fr, it, pl, de, or pt) for any document or system email whose client/company/user never set one of their own — see Document Language for the full cascade. Left unset, everything renders in English exactly as before.
ADDRESS_AUTOCOMPLETE_URLOptional, empty by default. Turns on address suggestions (street, house number, postcode, city, country) while typing a client's or your company's address - see Address autocomplete below.

Make sure port 80 is available on your host machine, or change the port mapping.

Address autocomplete​

Off by default: leave ADDRESS_AUTOCOMPLETE_URL unset and the address fields work exactly as before, plain text, with zero outbound requests. Setting it points this instance at a Photon geocoder (the same OpenStreetMap data as Nominatim, but built for type-ahead - Nominatim's own usage policy explicitly forbids autocomplete). Your browser never talks to Photon directly: the frontend calls this backend, which proxies the request to whichever server you configured, so the choice below stays server-side and the request is authenticated and rate-limited like any other endpoint.

Two options:

  • https://photon.komoot.io - komoot's own free server. The quickest way to turn this on, but it is shared and fair-use only ("extensive usage will be throttled or completely banned", per its own terms) - fine for a small/personal instance, not appropriate for a large one.
  • Your own Photon container, e.g. http://photon:2322 on the same Docker network. The only appropriate choice once you have real traffic: Photon's own search index ranges from a few GB for one country to 100GB+ for the whole planet, which is why it is never bundled into the Invoicerr image itself. See the Photon repository for the container image and how to build or download an index.
- ADDRESS_AUTOCOMPLETE_URL=https://photon.komoot.io

Email delivery​

Invoicerr needs to send emails (quote/invoice notifications, signature links). This is configured on two levels:

  • Instance level — the MAIL_PROVIDER/SMTP_*/RESEND_API_KEY variables below, set once for the whole deployment. Choose one provider with MAIL_PROVIDER, or leave it unset to auto-detect: RESEND_API_KEY present selects Resend (it wins even if SMTP_HOST is also set), otherwise SMTP is used.
  • Company level — each company can set its own mail server in Settings → Mail (SMTP or Resend). A company's own server, when set, is used for everything that company sends, instead of the instance-level one below; a company that never configures one keeps using the instance level. A send is refused (never silently dropped) only if neither level is configured.
- MAIL_PROVIDER=smtp # can be omitted, this is the default
- SMTP_HOST=smtp-relay.example.com
- SMTP_USER="username@example.com"
- SMTP_FROM="user-from@example.com" # optional, defaults to SMTP_USER
- SMTP_PASSWORD="your_smtp_password"
- SMTP_PORT=587 # default SMTP port for TLS
- SMTP_SECURE=false # set to true if your SMTP server requires a secure connection
VariableDescription
SMTP_HOST, SMTP_USER, SMTP_PASSWORDCredentials and server used for sending emails
SMTP_FROMOptional — sender address. Defaults to SMTP_USER if omitted
SMTP_PORTSMTP port (default 587)
SMTP_SECURESet to true if your SMTP server requires a secure connection
info

Brevo (formerly Sendinblue) has no dedicated provider any more — use its own SMTP relay (smtp-relay.brevo.com) with this SMTP option instead.

Instance operators​

Most actions in Invoicerr are scoped to one company. A small number of actions are instance-wide instead — they act across every company on this deployment at once. Today there is exactly one: resetting the whole instance (Settings → Danger Zone → "Reset instance"), which permanently deletes every company, user and document, then signs everyone out.

Nobody can do this by default. Set INSTANCE_OPERATOR_EMAILS to a comma-separated (case-insensitive) list of e-mail addresses to name who can:

- INSTANCE_OPERATOR_EMAILS=you@example.com,co-admin@example.com

Leaving it unset means no instance operator exists at all — the reset screen never appears, and the underlying API routes refuse every caller.

The instance-reset feature itself is unavailable entirely on a hosted (SaaS) deployment, regardless of this variable — the screen and its routes are hidden as if they did not exist, not merely forbidden.

The SAME INSTANCE_OPERATOR_EMAILS list is also used by one other, unrelated route: GET /api/backup/status (instance file-backup status), which used to trust any company's OWNER and now requires a real instance operator instead — on self-hosted and on a hosted deployment alike (unlike the reset feature, backup status is not hidden on SaaS).

danger

An instance reset is irreversible and cannot be scoped to "just one company" — it takes down every company on the deployment. It still requires a fresh e-mailed confirmation code and typing "RESET INSTANCE" exactly, but there is no undo once it runs.