Prerequisites
- Docker Engine installed and running
- Basic familiarity with the command line
Installation
Install Docker
- Follow the official Docker installation guide
- Start the Docker service on your machine
- Verify the installation:
Create your application directory
Create a directory where your app will run:Download the Docker Compose file
Download the sample compose file from the Sure repository:compose.yml file in your current directory with the default configuration.
Configuration
By default, thecompose.example.yml file runs without any configuration. For production deployments or if you’re running outside of a local network, follow these steps to add security.
Email configuration
To enable email notifications and password resets, configure SMTP settings in your.env file:
SSL/TLS options
For SMTP servers with custom SSL certificates or self-signed certificates: Skip TLS verification (not recommended for production):SSL_CA_FILE option allows you to specify a custom CA certificate file for SSL verification when connecting to SMTP servers with self-signed or internal certificates.
Create an environment file
Create a.env file where Docker will read environment variables:
Generate a secret key
Generate a secret key using one of these methods: With OpenSSL:Configure environment variables
Open the.env file in your text editor and add:
Optional: expose Sure with Cloudflare Tunnel
If you want to reach your self-hosted instance from the public internet without opening router ports, you can put a Cloudflare Tunnel in front of theweb container.
- Create a tunnel in Cloudflare Zero Trust and copy the tunnel token.
- Add the token to your
.envfile:
- Edit
compose.ymland set:
- Add a
cloudflaredservice to yourcompose.yml:
- In the Cloudflare tunnel dashboard, add a public hostname such as
sure.example.comand point it at the internal Docker service:
- Start the tunnel connector:
https://sure.example.com.
Cloudflare should connect to
http://web:3000, not https://web:3000. Sure speaks plain HTTP inside Docker and relies on RAILS_ASSUME_SSL to know the original request was HTTPS.Passkeys and security keys
If people will use passkeys, Touch ID, Windows Hello, or hardware security keys as MFA credentials, pin the WebAuthn relying party settings in.env:
WEBAUTHN_RP_ID should usually be the registrable domain, not a full URL. For example, use example.com for https://sure.example.com.
WEBAUTHN_ALLOWED_ORIGINS must include the full origin where people access Sure, including https://.
See Passkeys and security keys for user setup steps.
Passwordless passkey login is enabled by default:
"false" if you only want registered passkeys and security keys to work as a second factor after email/password sign-in.
Reverse proxy sub-path deployments
Sure can be served under a path prefix by a reverse proxy — for examplehttps://home.example.com/sure instead of the root. The web app works at that address with no extra configuration.
When using the macOS desktop app with a sub-path deployment, enter the full mounted URL (including the path prefix) when adding the server. The app probes the health endpoint at the address you enter and walks up to the origin to find the correct mount point, so pasting a deep link such as https://home.example.com/sure/sessions/new also works.
The
RAILS_RELATIVE_URL_ROOT environment variable tells Rails that it is mounted under a prefix. Set it to the path portion if your reverse proxy strips the prefix before forwarding, or configure the proxy to preserve the path and leave the variable unset.Import size variables
By default, Sure NDJSON imports are limited to a fixed maximum file size. Self-hosted admins can raise this limit with:Encryption keys for bank sync
When you configure a bank sync provider (such as Plaid, SimpleFIN, or Brex) on a self-hosted instance that has not set explicit Active Record encryption keys, Sure shows a warning before you save your credentials:Encryption keys missing — This self-hosted install has not configured explicit Active Record encryption keys. ConfigureThe trust statement on the provider form also changes to remind you that credentials will not be stored encrypted until you configure those keys. If encryption is not configured at all when you visit Settings > Bank Sync, Sure blocks access and redirects you with the same “Encryption keys missing” message. To resolve the warning, add these three variables to yourprimary_key,deterministic_key, andkey_derivation_saltbefore saving provider credentials in production.
.env file before saving any provider credentials in production:
openssl rand -hex 32 for each one. Once all three are present, the warning disappears and credentials are stored encrypted.
Sure can derive these values from
SECRET_KEY_BASE automatically, so the warning only appears when none of the three environment variables are explicitly set. If you already set SECRET_KEY_BASE, encryption is still active — you will only see the warning if you want to use dedicated keys instead. See Turning on Active Record encryption after first boot if you need to backfill existing data.Market data provider variables
Sure supports multiple securities pricing providers. You can configure them through environment variables or in the UI under Settings > Self-Hosting. See market data providers for details on each provider.Setting
SECURITIES_PROVIDERS as an environment variable takes precedence over the UI setting. Leave it unset to manage providers from the UI only.TINKOFF_INVEST_API_KEY serves two purposes: it enables T-Invest as a securities price provider when tinkoff_invest is included in SECURITIES_PROVIDERS, and it allows Sure to fetch brand logos for all securities (including MOEX-priced ones) via T-Invest’s CDN. The T-Invest settings block in the UI stays visible whenever a token is configured, even if T-Invest is not selected as a price provider.On-chain wallet variables
On-chain wallet tracking uses public block explorers by default. You can override any endpoint with your own instance, and tune history and token limits.binance_public in SECURITIES_PROVIDERS. If your family currency is not USD, also set EXCHANGE_RATE_PROVIDER to a provider that needs no API key (e.g. frankfurter). See on-chain wallets for details.
Running the application
Start the application
Start the app to verify everything is working:http://localhost:3000. You should see the Sure login screen.
Create your account
On first run, register a new account:- Click “Create your account” on the login page
- Enter your email
- Enter a password
super_admin role. If two registrations happen at the same time, Sure uses a database lock to ensure exactly one of them becomes super_admin and the other becomes admin.
Restrict future signups
After creating your initial admin account, you can control how other people join your self-hosted instance from Settings > Self-Hosting > Onboarding.- Open: Anyone can create an account from the registration page.
- Invite-only: Signups stay enabled, but a valid invite code is required unless a default family is configured for invite-only onboarding.
- Closed: The registration page is disabled for new signups.
Run in the background
To run Sure in the background:- Stop the current process with
Ctrl+C - Start in detached mode:
http://localhost:3000.
Updating
The Docker image in yourcompose.yml file controls which version of Sure you’re running:
Recommended images
ghcr.io/we-promise/sure:latest- Latest alpha releaseghcr.io/we-promise/sure:stable- Latest stable release
Update to the latest version
Your app does not automatically update. To update:Change update channel
To switch between update channels, edit thecompose.yml file:
Backup service
The Docker Compose configuration includes an optional backup service that automatically backs up your PostgreSQL database and uploads it to any cloud storage provider supported by rclone — including S3, Cloudflare R2, Google Drive, SFTP, and 70+ others.What to back up
For a complete recovery plan, make sure you know which of these apply to your deployment:- PostgreSQL database: accounts, transactions, settings, users, and metadata
- Local file storage: uploaded files stored on disk by the app
- External object storage: uploaded files stored in S3, R2, or another object store
- Environment and deployment config: your
.env,compose.yml, secrets, and any reverse proxy or DNS setup needed to bring the app back online
Enabling backups
The backup service uses Docker Compose profiles and is disabled by default. To enable it:Configure backup settings
Add the following variables to your.env file to configure the backup destination and schedule. BACKUP_DESTINATION is required; all other variables are optional.
RCLONE_CONFIG_S3_* variables with the equivalent variables for your provider. See the rclone documentation for the full list of supported providers and their configuration variables.
The compose.yml maps the S3 variables by default. If you use a different provider, add the corresponding variables to both your .env file and the backup service’s environment block in compose.yml.
Backup schedule options
BACKUP_SCHEDULE accepts standard cron syntax. Some examples:
0 2 * * *— every day at 2 AM (default)0 * * * *— every hour0 2 * * 0— once per week (Sunday at 2 AM)0 2 1 * *— once per month (1st of the month at 2 AM)
Restore from a PostgreSQL backup
Use this process when you have a SQL dump created by the backup service or withpg_dump.
[!NOTE] If you customized the PostgreSQL username, password, or database name in your.envorcompose.yml, replacesure_userandsure_productionin the commands below.
- Stop the application containers so they do not write to the database during the restore:
- Start or keep the database container running:
-
Locate the backup file in your backup directory, for example
/opt/sure-data/backups. - Restore the SQL backup into PostgreSQL:
- If your deployment uses local uploaded files, restore them before restarting the app. If you use external object storage, verify that provider’s backup or versioning recovery has completed.
Restore local uploaded files
If your Sure instance stores uploaded files on the local filesystem, restoring the database alone is not enough. You must also restore the app’s storage directory from the matching file backup. The exact host path depends on how you mapped volumes incompose.yml. Restore the same directory that Sure uses for local storage before starting the app containers.
If you are using external object storage instead of local disk, restore those files using that provider’s backup or versioning workflow instead.
When the database and any uploaded files are restored, restart the app:
Verify the restore
After restoring, check the following:- You can sign in successfully
- Your accounts and transactions appear as expected
- Uploaded files open correctly, if you use uploads
- The web and worker containers start cleanly without repeated errors
Verifying backups
Check that backups are running correctly:SSL certificate configuration
For self-hosted environments using self-signed certificates or custom certificate authorities, Sure provides SSL configuration options.Environment variables
Add these variables to your.env file:
- Your environment uses self-signed certificates
- You need to trust a custom certificate authority
- Corporate proxies inject their own certificates
true)
- Set to
falseto disable SSL verification (not recommended for production) - Only disable verification in development or testing environments
compose.yml:
Optional: SSL/TLS Configuration
Sure supports additional SSL/TLS configuration options for secure email delivery and API connections.Custom CA Certificate
If you’re using a custom Certificate Authority (CA) or self-signed certificates, you can specify a CA file:- Running Sure in a corporate environment with internal CAs
- Using self-signed certificates for development
- Connecting to services with custom certificate chains
Skip TLS Verification for Email
For development or testing environments, you can disable TLS verification for the email mailer:- Development environments with self-signed certificates
- Testing email functionality locally
- Internal mail servers with custom certificates
- Always use valid SSL/TLS certificates in production
- Use the
SSL_CA_FILEoption instead of disabling verification - Ensure your SMTP server supports STARTTLS
Mailer SSL configuration
For SMTP connections, additional SSL options are available:true)
SMTP_OPENSSL_VERIFY_MODE: SSL verification mode for SMTP
none: Skip SSL verificationpeer: Verify the server certificate (default)
Security considerations
For production deployments:- Use properly signed certificates from a trusted CA
- Keep
SSL_VERIFY=true - Use
SMTP_OPENSSL_VERIFY_MODE=peer - If you must use self-signed certificates, provide a CA bundle via
SSL_CA_FILE
Monitoring system health
When the Sidekiq worker container isn’t running, background jobs silently never execute. Balance calculations, net-worth updates, and account syncs stall, and the UI may show zeros or “No balance data available” without explaining why.Sidekiq health banner
If Sure detects that Sidekiq is not processing jobs, a warning banner appears at the top of every page for super-admins. The banner indicates that data may be stale and links to the system health page. The check is conservative by design:- A worker process is considered stale if its heartbeat is older than 2 minutes, which tolerates brief deploy restarts and Redis blips.
- A queue is considered backed up if its latency exceeds 5 minutes.
System health page
Super-admins can view live Sidekiq state at Settings → Advanced → System health. The page shows:- Worker process count and last heartbeat time
- Maximum queue latency
- Job counters (processed, failed, enqueued, retries)
- Per-queue depth breakdown
AI status tab
The system health page has an AI tab at/admin/system_health?tab=ai. Opening it runs bounded, non-destructive live checks against the effective AI configuration:
- LLM probe — Verifies the configured model is available. For standard OpenAI, it queries the models list. For OpenAI-compatible endpoints (Ollama, OpenRouter, etc.), it sends a short chat completion request instead, since many compatible providers do not implement the models list endpoint.
- Function calling (tools) probe — Asks the configured model for one trivial tool call using the same
toolspayload the assistant sends for its own function calls. A pass means tool calls work end-to-end. Two distinct failure outcomes are reported separately: if the tools request is rejected as a client error but the same request without tools succeeds, the probe marks tool support as unsupported (common with models on providers like OpenRouter that answer a bare 404); if the endpoint accepted the tools parameter but the model replied with text instead of calling the tool, the probe marks it as tools accepted, but the model called none. The probe uses the same API route the assistant takes — Responses API for hosted OpenAI whensupports_responses_endpoint?is true, Chat Completions otherwise — so it always reflects the path live chat follows. - PDF text extraction probe — Sends a synthetic PDF to the model and checks that the response correctly identifies the institution name embedded in the document. This probe only runs when the effective provider is OpenAI and the model reports PDF text extraction support. The synthetic document contains no customer data.
- PDF vision processing probe — Same synthetic PDF check using the vision processing path. Runs for any provider that reports vision-based PDF support, including Anthropic. The synthetic document contains no customer data. If the probe fails with “The pdftoppm renderer (poppler-utils) is not available in this container”, your image predates the fix that bundled
poppler-utils; pull the latest image and restart. - Vector store probe — For OpenAI vector stores, confirms a list request succeeds without creating or modifying a store. For pgvector, checks that the
vectorextension is enabled and the chunks table exists. - Embedding probe — For pgvector, sends a short test embedding request and verifies the returned vector matches
EMBEDDING_DIMENSIONS. The test vector is not stored.
The tab also shows the active provider, model, endpoint (credentials are always redacted), function calling status, LLM request timeout, health-check probe timeout, PDF text extraction and vision processing status, and pgvector extension and table status. The model settings field links super-admins to this tab and notes that the assistant requires a model with function-calling support. The LLM request timeout and health-check probe timeout are displayed as separate rows because they bound different operations: the LLM request timeout covers normal chat and PDF import calls, while the health-check probe timeout covers only the live checks run on this page.
When
OPENAI_URI_BASE points to a custom endpoint, the page identifies the provider where possible — Ollama, OpenRouter, Together AI, Kilo, Cloudflare Workers AI or AI Gateway — and labels unrecognized services as Custom endpoint.
Results are cached for 60 seconds by default. Click Run checks again to bypass the cache and get fresh results. Failures are written to the system debug log and to Rails.logger; credentials are never included.
You can tune the AI status checks with these environment variables:
Verifying worker configuration
The checks above all run from theweb process. Most AI work — assistant responses, PDF processing, embeddings, auto-categorization, and merchant detection — actually executes in a Sidekiq worker process, which can differ from web in network access, DNS, proxy rules, or the credentials it loaded. This is most common in Kubernetes deployments using workload-specific environment overrides or a Secret updated without a pod restart. A passing web check does not prove a worker can reach the same provider.
Click Verify worker configuration on the AI status tab to queue an asynchronous check (WorkerAiHealthCheckJob). It runs the same live probes from inside whichever worker process dequeues it, and the tab lists the result: that worker’s process identity, when it checked in, whether its effective configuration matches what web resolved, and probe outcomes.
A few things this does and does not prove:
- One check verifies one worker. Sidekiq does not broadcast a job to every process, so a passing result confirms the named process is healthy, not your whole fleet. With multiple worker replicas, queue the check again to sample another. Only the 5 most recently checked-in distinct processes are kept. A 6th check-in can evict an older entry before its own 15-minute retention window expires. A result older than
WorkerAiHealth::STALE_AFTERdisplays as Stale rather than pass or fail. - Worker checks never reuse a web-cached result, or vice versa. The web page’s probes are cached briefly so repeat page loads do not re-hit providers. The worker job bypasses that shared cache so its result always reflects a live call from its own network context, and never leaves an entry the web process could read back as its own.
- In local development, web and worker results will not show up together.
bin/devrunswebandworkeras separate OS processes, andconfig/environments/development.rbuses a process-local cache. A worker check queued locally writes to the worker’s own in-memory cache, so the Verify worker configuration button can appear to do nothing. This is expected behavior; production’s shared Redis store is unaffected. - Which settings need a restart depends on how they are set. Provider, model, and API-key settings changed in Settings → Self-Hosting are stored in the database and take effect automatically on both
webandworkerwithout a restart. Embedding configuration and anything set only through an environment variable is fixed when each container starts. Changing those requires restarting or recreating bothwebandworker. The AI status tab labels this distinction next to the worker results.
OpenAI request timeout
The LLM request timeout controls how long each OpenAI-compatible HTTP request is allowed to take before it is cancelled. It applies to auto-categorization, merchant detection, chat completions, PDF processing, and vector-store calls. The default is 60 seconds, which works well for hosted models. Raise it when using slow local models (for example, Ollama on modest hardware). You can configure the timeout in two ways:- Environment variable — set
OPENAI_REQUEST_TIMEOUTto a number of seconds. This takes precedence over the UI setting. - Settings UI — go to Settings > Self-Hosting and enter a value in the Request Timeout in Seconds field under the OpenAI section.
AI response timeout
The response timeout is a whole-turn watchdog for chat. It bounds how long Sure waits for a complete assistant turn — including all tool calls and their results — before giving up. The default is 90 seconds. Size this as a sum of the per-call request timeout across the maximum number of tool-call iterations, plus tool execution and queue wait time:(1 + ASSISTANT_MAX_TOOL_CALL_ITERATIONS) × OPENAI_REQUEST_TIMEOUT. The 90-second default is sized for typical cloud model latency. For local models where each request is slow, either raise this value or lower ASSISTANT_MAX_TOOL_CALL_ITERATIONS.
You can configure the timeout in two ways:
- Environment variable — set
AI_RESPONSE_TIMEOUTto a number of seconds. This takes precedence over the UI setting. - Settings UI — go to Settings > Self-Hosting and enter a value in the Response Timeout in Seconds field under the OpenAI section.
Extra HTTP headers for OpenAI-compatible requests
OPENAI_EXTRA_HEADERS lets you attach custom HTTP headers to every request Sure sends to the OpenAI-compatible endpoint. This is useful for AI gateways that require routing or authentication headers — for example, sending a session identifier to a proxy that tracks usage per conversation.
Set the variable to a JSON object of header names and values:
- The value must be a valid JSON object. If it is missing, not an object, or not valid JSON, Sure logs an error and falls back to no extra headers. Chat continues normally.
- String keys and values are used as-is. Blank values are dropped.
- If a header value contains the literal
{session_id}, Sure substitutes the current chat’s UUID at request time. This identifies requests per conversation rather than per installation. The substitution only happens for chat requests — batch flows such as auto-categorization, merchant detection, and PDF processing never receive session headers. - This variable is environment-only. There is no Settings UI equivalent.
The Sidekiq health banner, system health page, and AI status tab are only available in self-hosted mode. Managed deployments do not show these features.
Running Sure on small (512 MB) hosts
Sure runs comfortably on hosts or containers limited to 512 MB of RAM, which makes it a good fit for the smallest tiers on platforms like Render, Fly.io, or a cheap VPS.What fits in 512 MB
Measured on a production deploy of the official image with the tuning below:- Boot, first-run onboarding, and everyday use (dashboard, transactions, budgets, reports)
- Small bank syncs and CSV imports
- Scheduled cron jobs such as exchange-rate refreshes
What does not fit in 512 MB
- The demo-data generator (“Load sample/demo data”): this is the one operation that deterministically exceeds 512 MB. On a 512 MB container it climbs to the limit and gets OOM-killed mid-generation. Because the generation runs in the worker, the symptom is a sample-data load that never completes, sometimes with all rows silently rolled back.
- Very large first-time imports or historical syncs (tens of thousands of rows) can also exceed the limit. Import your history in smaller batches, or temporarily raise the memory limit for the initial import and lower it afterwards.
- AI features: the assistant needs extra headroom. If you enable AI, run at least 1 GB.
Tuning already in the image
The official image ships with the memory tuning that makes 512 MB viable, so no extra configuration is needed:- jemalloc preloaded to reduce memory fragmentation
- YJIT (Ruby’s JIT) enabled
- Puma constrained to 1 worker × 3 threads (
WEB_CONCURRENCY=1,RAILS_MAX_THREADS=3)
Loading sample data on a small host
If you want to load sample data on a 512 MB host:- Raise the worker’s memory limit. The generator runs in the worker process, not the web process. On Render, bump the worker service’s plan; on Docker Compose, raise the worker container’s memory limit.
- Apply the change with a fresh deploy or restart of the worker. On Render, plan changes only take effect on the next deploy and do not apply on a plain restart.
- Load the sample data, then optionally lower the worker back to the smaller plan with another fresh deploy.
Troubleshooting
Database connection errors
If you encounterActiveRecord::DatabaseConnectionError on first startup, Docker may have initialized the Postgres database with a different default role from a previous attempt.
Reset the database:
Turning on Active Record encryption after first boot
Self-hosted Sure can derive Active Record encryption keys fromSECRET_KEY_BASE automatically. If you prefer dedicated keys, you can add:
SECRET_KEY_BASE for an existing instance unless you are restoring from a backup that was created with the new value. Sure uses it to derive encryption keys when the dedicated ACTIVE_RECORD_ENCRYPTION_* variables are not set. If SECRET_KEY_BASE changes, deterministic encrypted fields such as user email addresses may no longer be searchable, and email/password login can fail with “invalid username or password” even when the password is correct.
If you add those keys after your instance already has data, the normal app startup path is not enough to rewrite older plaintext values:
db:preparestill runs automatically on startup for schema setup and migrations.- New writes use encryption after the app boots with the keys.
- Existing rows are not automatically re-encrypted.
SECRET_KEY_BASE or previous ACTIVE_RECORD_ENCRYPTION_* values first. After the app can read the existing data again, run the backfill task if you are intentionally moving to dedicated keys.
Sure NDJSON import file size limit
By default, Sure NDJSON imports are limited to a fixed maximum file size. Self-hosted admins can raise this limit with an environment variable:Slow CSV imports
If CSV imports are processing rows slower than expected, check your worker logs for errors:sure-worker container requires Redis to process CSV imports.
AI features, external assistant, and Pipelock
Sure ships a separate compose file for AI-related features:compose.example.ai.yml. It adds:
- Pipelock (always on): AI agent security proxy for outbound tunnel controls and inbound MCP scanning
- Ollama + Open WebUI (optional
--profile local-ai): local LLM inference
Using the AI compose file
Setting up the external AI assistant
The external assistant delegates chat to a remote AI agent instead of calling LLMs directly. The agent calls back to Sure’s/mcp endpoint for financial data (accounts, transactions, balance sheet).
- Set the MCP endpoint credentials in your
.env:
- Set the external assistant connection:
- Choose how to activate:
- Per-family (UI): Go to Settings > Self-Hosting > AI Assistant, select “External”
- Global (env): Set
ASSISTANT_TYPE=externalto force all families to use external
external-assistant profile. This profile starts OpenClaw without the local Ollama or Open WebUI services:
docs/hosting/ai.md file in the Sure repository for full configuration details including agent ID, session keys, and email allowlisting.
Pipelock security proxy
Pipelock sits between Sure and external services and provides:- MCP request and response scanning for DLP, prompt injection, and tool poisoning
- HTTPS tunnel controls for destination, SSRF, rate, budget, CONNECT headers, and optional signed receipts
compose.example.ai.yml, Pipelock is always running. External AI agents should connect to port 8889 (Pipelock’s MCP reverse proxy) instead of directly to Sure’s /mcp on port 3000.
The example compose file does not enable TLS interception, so Pipelock controls the tunnel destination but cannot read encrypted request or response bodies. Docker Compose also does not prevent a client from bypassing the proxy.
pipelock.example.yaml reference file in the Sure repository.
Local development bind
Forbin/dev on your own machine, the server defaults to localhost (127.0.0.1 and [::1]), reachable only from the same machine. If you need external access (phone on the same Wi-Fi, devcontainer port forwarding, LAN testing), set the BINDING environment variable:
.devcontainer/docker-compose.yml already sets BINDING: "0.0.0.0", so Docker port forwarding works without a manual override when using the devcontainer.
Getting help
If you find bugs or have feature requests:- Read the contributing guide
- Ask in the Discord
- Open an issue