Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cryguy/hashboard/llms.txt

Use this file to discover all available pages before exploring further.

Hashboard ships a multi-stage Dockerfile that compiles better-sqlite3 from source on Alpine, prunes dev dependencies in place, and produces a minimal runtime image. A single named volume at /data holds both the SQLite database and the attachments directory, so one backup of that directory captures everything. Migrations are applied automatically each time the container starts — no separate migration step is needed.
Never scale this service past one replica. SQLite is a single-writer database and Hashboard is designed as a single process. Running two replicas will cause write contention and data corruption. This is the same constraint that forbids pm2 cluster mode.

Before you start

Open compose.yaml and set ORIGIN to the public URL where Hashboard will be reachable. Without it, adapter-node cannot build absolute URLs and OIDC redirect_uri values will be wrong. If you are placing Hashboard behind a reverse proxy (the recommended setup), also uncomment ADDRESS_HEADER and XFF_DEPTH — see the Reverse Proxy guide for why these are required in production.

Deployment

1

Clone the repository

git clone https://github.com/cryguy/hashboard.git
cd hashboard
2

Configure compose.yaml

Edit compose.yaml and fill in the environment variables for your deployment. At a minimum, uncomment and set ORIGIN:
# ORIGIN: https://hashboard.example.com
# ADDRESS_HEADER: X-Forwarded-For
# XFF_DEPTH: '1'
For OIDC sign-in, also fill in OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET. Local username/password accounts work without OIDC.
3

Build and start the container

docker compose up -d --build
The image is built locally. On first start the container creates /data/hashboard.db, applies all pending migrations, and begins serving on 127.0.0.1:3000. Subsequent starts only apply new migrations.
4

Verify the container is healthy

The image includes a built-in HEALTHCHECK that polls /api/v1/health. Check its status with:
docker compose ps
The STATUS column will show healthy once the application is ready. You can also check directly:
curl http://127.0.0.1:3000/api/v1/health

compose.yaml reference

The full compose.yaml is shown below. Most environment variables are pinned with safe defaults; only ORIGIN (and the proxy vars) need to be filled in for production.
# docker compose up -d --build
#
# Never scale this service past one replica: SQLite has one writer and the
# app is designed as a single process — the same rule the pm2 config states.
services:
  hashboard:
    build: .
    container_name: hashboard
    restart: unless-stopped
    # Published on localhost only: TLS termination and the public hostname
    # belong to the reverse proxy in front (the session cookie is Secure, so
    # plain HTTP on a non-localhost address silently breaks sign-in — see
    # README). Widen to "3000:3000" only if something else terminates TLS.
    ports:
      - '127.0.0.1:3000:3000'
    environment:
      # Pinned here as well as in the image so an env_file can never point
      # the database outside the volume (a copied dev .env would try).
      DATABASE_URL: /data/hashboard.db
      # Pinned alongside the database for the same reason: both live on the
      # volume below, so one backup of /data is a complete one.
      ATTACHMENTS_DIR: /data/attachments
      # Raise together — BODY_SIZE_LIMIT is adapter-node's transport cap (512K
      # by default, which would reject most uploads), MAX_UPLOAD_MB is the
      # per-file limit the service enforces.
      BODY_SIZE_LIMIT: 32M
      # MAX_UPLOAD_MB: '25'
      # The public origin — required in production, OIDC redirect_uri and
      # absolute URLs are built from it.
      # ORIGIN: https://hashboard.example.com
      # Behind a reverse proxy, required for per-IP rate limiting (README):
      # ADDRESS_HEADER: X-Forwarded-For
      # XFF_DEPTH: '1'
      # OIDC sign-in (optional — local accounts work without it):
      # OIDC_ISSUER: https://authentik.example.com/application/o/hashboard/
      # OIDC_CLIENT_ID: ''
      # OIDC_CLIENT_SECRET: ''
    volumes:
      - hashboard-data:/data

volumes:
  hashboard-data:

Networking

The port binding 127.0.0.1:3000:3000 intentionally limits exposure to localhost. TLS termination and the public hostname belong to the reverse proxy in front of Hashboard — see the Reverse Proxy guide for nginx, NPMplus, and Caddy examples. If you widen the binding to 0.0.0.0:3000:3000 you are responsible for terminating TLS elsewhere. The session cookie carries the Secure flag; over plain HTTP on any non-localhost address the browser accepts the cookie on login and then never sends it back, making sign-in appear to do nothing with no visible error.

Live backup

Both the database and attachments live under /data in the container, so a single backup covers both. To take a consistent database snapshot from a running container without stopping it:
docker compose exec hashboard node -e "new (require('better-sqlite3'))(process.env.DATABASE_URL).exec(\"VACUUM INTO '/data/backup.db'\")"
This creates /data/backup.db inside the named volume. Copy it out with docker cp or a volume-aware backup tool. For full backup procedures — including the order in which to snapshot the database and attachments — see the Backups guide.

Build docs developers (and LLMs) love