docs(docker): improve NAS self-hosting compatibility (#1330)

* docs(docker): improve NAS self-hosting compatibility

Signed-off-by: Emil <emilmattsson14@gmail.com>

* docs(docker): retain optional resource controls

Signed-off-by: Emil <emilmattsson14@gmail.com>

* docs(docker): clarify compatibility prerequisites

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
This commit is contained in:
Mattsson
2026-08-01 18:54:46 +02:00
committed by GitHub
parent bfbd926950
commit 115147d304
5 changed files with 175 additions and 2 deletions
+2
View File
@@ -717,6 +717,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-30] Report-vocabulary synonyms live in ReportDescriptor.searchTerms, NOT in the command-palette keywords, when they are words another report already owns. "stäm av"/"avstämning" on the huvudbok palette entry hijacked Enter from Bankavstämning, because the palette auto-selects the first hit and huvudbok is listed above it. The library search shows a list and has no such failure mode, so broad task-vocabulary belongs there. [2026-07-30] Report-vocabulary synonyms live in ReportDescriptor.searchTerms, NOT in the command-palette keywords, when they are words another report already owns. "stäm av"/"avstämning" on the huvudbok palette entry hijacked Enter from Bankavstämning, because the palette auto-selects the first hit and huvudbok is listed above it. The library search shows a list and has no such failure mode, so broad task-vocabulary belongs there.
[2026-08-01] Palette is persisted as an independent root data attribute instead of new next-themes names: Light, Dark, and System values remain backward-compatible, and System can still follow the OS while each palette supplies both light and dark tokens. [2026-08-01] Palette is persisted as an independent root data attribute instead of new next-themes names: Light, Dark, and System values remain backward-compatible, and System can still follow the OS while each palette supplies both light and dark tokens.
[2026-08-01] Palette refinement increases hue in primary, frame, secondary, muted, and border tokens only: Indigo, Forest, and Sand read more clearly without recoloring success, warning, destructive, or chart semantics. [2026-08-01] Palette refinement increases hue in primary, frame, secondary, muted, and border tokens only: Indigo, Forest, and Sand read more clearly without recoloring success, warning, destructive, or chart semantics.
[2026-08-01] Accounted's base self-hosted Compose omits `cpus` and `healthcheck.start_interval`: both controls are optional, bundled NAS Compose parsers can reject them, and CPU policy belongs in host tooling or a local override. The version-controlled `docker-compose.resources.yml` overlay retains the former two-CPU cap and faster startup probing for command-line Compose 2.20.2+ without breaking the portable base. Existing operators are told to reapply the cap through either path. Supabase bind mounts, database ports, and JWKS syntax remain version-matched upstream concerns, with scoped NAS guidance in Accounted's integration docs rather than a forked Supabase Compose file; the guide loopback-binds both upstream Supavisor ports by default because changing 5432 alone resolves a collision but does not prevent LAN exposure.
[2026-07-30] A missing org number on either side of the Bokio connect probe does NOT block the connection; only a confident mismatch does. Accounted allows companies without an org number and a provider response can omit it, so blocking on absence would refuse legitimate connections to prevent a mismatch we have no evidence of. Absence instead falls through to labelling the consent with the company the credentials actually opened, which is what lets the user catch it. Same reasoning applied to keeping 429/5xx from the probe out of the invalid-credentials mapping: a provider outage must not read as "your token is wrong". [2026-07-30] A missing org number on either side of the Bokio connect probe does NOT block the connection; only a confident mismatch does. Accounted allows companies without an org number and a provider response can omit it, so blocking on absence would refuse legitimate connections to prevent a mismatch we have no evidence of. Absence instead falls through to labelling the consent with the company the credentials actually opened, which is what lets the user catch it. Same reasoning applied to keeping 429/5xx from the probe out of the invalid-credentials mapping: a provider outage must not read as "your token is wrong".
@@ -727,3 +728,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-31] Assistant product knowledge REVERTED before merge (founder call): the assistant gets no internal knowledge of Accounted's own features yet. The working product-tier implementation (agent_atom_registry CHECK widening, product/bokforingsmallar atom, discovery, MCP + panel wiring, all CI green at bb65224c) lives in fix/mall-line-type-clarity branch history and a follow-up issue for when it is wanted. Only the UI clarification ships for the radtyp confusion: unified "Kostnad/Intäkt" label and an InfoTooltip explaining the three radtyper at the point of choice. [2026-07-31] Assistant product knowledge REVERTED before merge (founder call): the assistant gets no internal knowledge of Accounted's own features yet. The working product-tier implementation (agent_atom_registry CHECK widening, product/bokforingsmallar atom, discovery, MCP + panel wiring, all CI green at bb65224c) lives in fix/mall-line-type-clarity branch history and a follow-up issue for when it is wanted. Only the UI clarification ships for the radtyp confusion: unified "Kostnad/Intäkt" label and an InfoTooltip explaining the three radtyper at the point of choice.
[2026-08-01] Cloud Backup OAuth redirect URIs resolve from NEXT_PUBLIC_APP_URL, with request origin only as a self-hosted fallback: Google and Dropbox require pre-registered callbacks, so deriving them from an old alias or preview host can reject the flow before consent; one resolver keeps authorization, exchange, revoke, and sync origins consistent. [2026-08-01] Cloud Backup OAuth redirect URIs resolve from NEXT_PUBLIC_APP_URL, with request origin only as a self-hosted fallback: Google and Dropbox require pre-registered callbacks, so deriving them from an old alias or preview host can reject the flow before consent; one resolver keeps authorization, exchange, revoke, and sync origins consistent.
[2026-08-01] Do not claim a fixed Supabase Docker revision: Accounted does not maintain a tested Supabase stack pin, so the self-hosting guide requires one complete immutable upstream tag or commit instead of naming an unverified version.
+8
View File
@@ -0,0 +1,8 @@
# Optional resource and startup-health controls.
# Requires Docker Compose 2.20.2+ and Docker Engine 25.0+.
# Usage: docker compose -f docker-compose.yml -f docker-compose.resources.yml up -d
services:
app:
cpus: 2
healthcheck:
start_interval: 5s
+4 -2
View File
@@ -26,8 +26,9 @@ services:
- /tmp - /tmp
- /app/.next:uid=1001,gid=1001,mode=750,size=400m - /app/.next:uid=1001,gid=1001,mode=750,size=400m
- /app/public:uid=1001,gid=1001,mode=750,size=200m - /app/public:uid=1001,gid=1001,mode=750,size=200m
# CPU limits are intentionally left to host tooling or a local override.
# Some Compose builds bundled with NAS platforms reject the `cpus` key.
mem_limit: 1g mem_limit: 1g
cpus: 2
pids_limit: 200 pids_limit: 200
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
@@ -38,7 +39,8 @@ services:
interval: 30s interval: 30s
timeout: 5s timeout: 5s
start_period: 30s start_period: 30s
start_interval: 5s # `start_interval` requires Docker Compose 2.20.2+ and Docker Engine 25.0+.
# Omitting it keeps the base file usable with older NAS container stacks.
retries: 3 retries: 3
logging: logging:
driver: json-file driver: json-file
+61
View File
@@ -67,6 +67,67 @@ curl http://localhost:3000/api/health
--- ---
## Synology DSM and Xpenology
Use a **Container Manager Project**, not the single-container wizard. The
project path is the working directory for every relative path in the Compose
file. Create that directory first and put all of these files in it before
deploying the project:
```text
docker-compose.yml
.env
docker/
cron.Dockerfile
crontab.self-hosted
```
Uploading only `docker-compose.yml` is not enough: the cron service is built
from `docker/cron.Dockerfile` and bind-mounts
`docker/crontab.self-hosted`. Keep `.env` readable only by the administrator
and Container Manager because it contains the Supabase service-role key.
Container Manager ships its own Compose build, and supported keys vary by DSM
release. Accounted's base Compose file avoids the optional `cpus` and
`healthcheck.start_interval` keys for compatibility. Set a CPU limit through
Container Manager's resource controls or a local override if needed. When
updating an existing deployment that relied on the previous two-CPU cap,
reapply that limit in the host controls before restarting the project.
If you run Docker Compose 2.20.2 or newer against Docker Engine 25.0 or newer,
the optional resource overlay restores the previous two-CPU cap and faster
startup health checks while keeping the base file compatible. Download the
overlay from the same Accounted tag or full commit as the base Compose file:
```bash
ACCOUNTED_REF=replace-with-the-same-tag-or-full-commit
curl -fsSLo docker-compose.resources.yml \
"https://raw.githubusercontent.com/erp-mafia/accounted/${ACCOUNTED_REF}/docker-compose.resources.yml"
docker compose -f docker-compose.yml -f docker-compose.resources.yml up -d
```
Compose only applies the files named in each invocation. Keep the resource
overlay in every later `up` command, after any other overlay. For example:
```bash
# HTTPS with Caddy
docker compose -f docker-compose.yml -f docker-compose.caddy.yml -f docker-compose.resources.yml up -d
# Local image build
docker compose -f docker-compose.yml -f docker-compose.build.yml -f docker-compose.resources.yml up --build -d
```
Do not use this overlay if Container Manager rejects either key or the Docker
Engine is older than 25.0. The memory and PID limits remain active in the base
file either way.
Accounted itself does not use PostgreSQL port 5432 and does not need a database
data folder when connected to Supabase Cloud. If Supabase is also running on
the NAS, follow the [fully self-hosted notes](SELF-HOSTING.md#synology-dsm-and-xpenology-notes)
for its separate project, bind mounts, ports, and JWKS configuration.
---
## Enable HTTPS (recommended) ## Enable HTTPS (recommended)
Ship a Caddy reverse proxy alongside the app: it auto-provisions Let's Encrypt certificates and renews them forever. Ship a Caddy reverse proxy alongside the app: it auto-provisions Let's Encrypt certificates and renews them forever.
+100
View File
@@ -350,6 +350,106 @@ flowchart LR
5. **Reverse proxy** in front of both hosts. The app container and the Supabase `kong` container must share an external Docker network so the proxy can route to them by name. 5. **Reverse proxy** in front of both hosts. The app container and the Supabase `kong` container must share an external Docker network so the proxy can route to them by name.
### Synology DSM and Xpenology notes
Run Accounted and Supabase as two separate Container Manager Projects with two
separate project directories. Accounted owns the Compose files in this
repository. Supabase owns its database, Auth, Realtime, Storage, and pooler
configuration. Choose one upstream release tag or full commit and copy the
complete `docker/` directory from that immutable revision, following the
[official Supabase Docker guide](https://supabase.com/docs/guides/self-hosting/docker).
Do not copy individual snippets into Accounted's Compose file or mix files from
different upstream revisions.
For the **Accounted project**, follow the
[Accounted Container Manager file layout](DOCKER.md#synology-dsm-and-xpenology).
For the **Supabase project**:
1. Copy the entire upstream `supabase/docker/` directory into the project
directory. Do not upload only its `docker-compose.yml`: it bind-mounts SQL,
gateway, function, pooler, and Storage files from the accompanying
`volumes/` tree.
2. Create the two runtime directories that upstream deliberately excludes from
Git before the first deployment. File Station is fine, or from the Supabase
project directory use:
```bash
mkdir -p volumes/db/data volumes/storage
```
Container Manager must be able to write to both directories. Use the
narrowest NAS ACL that works for the container runtime; do not make the
whole shared folder world-writable.
3. Supavisor publishes two host ports. Before starting the project, make sure
both `POSTGRES_PORT` and `POOLER_PROXY_PORT_TRANSACTION` in the **Supabase**
`.env` are unused on the NAS. If the defaults conflict, examples are
`POSTGRES_PORT=5433` for session mode and
`POOLER_PROXY_PORT_TRANSACTION=6544` for transaction mode. Changing only
`POSTGRES_PORT` does not resolve a conflict on the transaction port.
Accounted's `PORT` only changes the web app port and cannot resolve either
database conflict. Do not expose the Supabase `db` container directly just
to solve a conflict: the official stack exposes PostgreSQL through
Supavisor.
Supabase's default Supavisor port mappings listen on every host interface.
Accounted does not need either database port over the network, so on a
shared NAS bind both mappings to loopback in the version-matched Supabase
Compose file:
```yaml
services:
supavisor:
ports:
- "127.0.0.1:${POSTGRES_PORT}:5432"
- "127.0.0.1:${POOLER_PROXY_PORT_TRANSACTION}:6543"
```
If another trusted machine must connect, bind to a specific private NAS
address and restrict both ports to trusted source addresses in the DSM
firewall. Never forward either database port to the public internet.
4. The default Accounted integration uses Supabase's legacy `ANON_KEY` and
`SERVICE_ROLE_KEY`, so asymmetric keys and `JWT_JWKS` are optional. Leave
the upstream JWKS lines commented when using legacy-only mode. If you enable
Supabase's new asymmetric keys, generate them with the upstream
`utils/add-new-auth-keys.sh` script and follow the
[official authentication-key guide](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys).
Some older Compose parsers reject the inline JSON fallback in Supabase's
optional Realtime setting:
```yaml
API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
```
After `JWT_JWKS` has been generated and saved in the Supabase `.env`, use the
direct substitution documented by Supabase for limited Compose parsers:
```yaml
# Supabase PostgREST
PGRST_JWT_SECRET: ${JWT_JWKS}
# Supabase Realtime
API_JWT_JWKS: ${JWT_JWKS}
# Supabase Storage
JWT_JWKS: ${JWT_JWKS}
```
Do not invent an empty or placeholder JWKS for a production deployment. Either
keep asymmetric authentication disabled or configure the generated value
consistently for every Supabase service that verifies tokens.
Accounted's base Compose file intentionally omits the optional `cpus` and
`healthcheck.start_interval` settings because older Container Manager Compose
builds can reject them. Operators who need a CPU cap can set one through DSM's
resource controls or a local Compose override. Existing deployments that
relied on the previous two-CPU cap must reapply it before restarting with the
new base file. Command-line deployments on Docker Compose 2.20.2 or newer and
Docker Engine 25.0 or newer can use the version-controlled
`docker-compose.resources.yml` overlay to restore both the cap and faster
startup health checks; older NAS container stacks should keep using the
portable base file alone.
### What you give up vs. cloud Supabase ### What you give up vs. cloud Supabase
- **Backups** are entirely your responsibility: set up `pg_dump` (or a tool like restic) to off-host storage. As a portable, vendor-neutral *logical* backup on top of the raw dump, you can also export each fiscal period as a standard **SIE4** file via the API and archive it: any Swedish bookkeeping system can re-import it: - **Backups** are entirely your responsibility: set up `pg_dump` (or a tool like restic) to off-host storage. As a portable, vendor-neutral *logical* backup on top of the raw dump, you can also export each fiscal period as a standard **SIE4** file via the API and archive it: any Swedish bookkeeping system can re-import it: