docs: add legal storage policy, DocuSeal deploy template, and app4 migration plan

This commit is contained in:
root
2026-08-15 15:45:36 -04:00
parent 4d43623db6
commit 26b985f6a4
5 changed files with 619 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
# app4 Scoping and Migration Plan
**Owner:** IT Pro Partner (Germaine Brown)
**Created:** 2026-08-15
**Status:** Draft (for review)
**Objective:** Move every customer-facing app off Core onto a new `app4` host. Core becomes the Hermes AI assistant home only, with no customer-facing apps long term.
---
## 1. End State
| Host | Role |
| --- | --- |
| **Core** (RS 2000, 152.53.192.33) | Hermes + its direct dependencies + internal monitoring + Caddy for Core-local routes only |
| **app4** (new, netcup) | All customer-facing apps, their databases, and all customer-facing Caddy routes |
Core keeps: browserless, camofox-browser, Super Search + SearXNG, the Prometheus/Telegraf/Grafana monitoring stack, mikrotik-exporter, Caddy itself, and core.itpropartner.com. Everything else moves.
---
## 2. Classification
### 2.1 STAYS ON CORE (Hermes and its direct dependencies)
| Item | Type | Current | Rationale |
| --- | --- | --- | --- |
| Caddy reverse proxy | systemd (80/443) | Core | Edge proxy; customer site blocks removed after cutover, `default_bind 152.53.192.33` retained |
| browserless | Docker (:3000) | Core | Hermes headless browser dependency |
| camofox-browser | Docker (:9377) | Core | Hermes stealth browser dependency |
| SearXNG | Docker (127.0.0.1:8888) | Core | Super Search search backend |
| Super Search MCP | systemd (:8899) | Core | Hermes `web_search` / `web_extract` MCP |
| Prometheus | Docker | Core | Internal fleet monitoring (scrapes node_exporter) |
| Telegraf | Docker | Core | Internal metrics collection |
| Grafana | Docker | Core | Internal monitoring dashboards |
| mikrotik-exporter | Docker (127.0.0.1:9436) | Core | MikroTik router metrics for Prometheus |
| core.itpropartner.com | Caddy site | Core | Hermes / Core admin endpoint |
### 2.2 MOVES TO APP4 (customer-facing apps and routes)
| Item | Type | Current | Notes |
| --- | --- | --- | --- |
| DocuSeal | Docker (127.0.0.1:8091->3000) | Core | e-sign platform; SQLite (bind-mounted ./data) + internal Redis/Sidekiq |
| TimeTrex | Docker (127.0.0.1:8085) | Core | Time tracking; Postgres backed |
| microbin | Docker (127.0.0.1:8260) | Core | Paste bin; lightweight, low blast radius |
| Uptime Kuma | Docker (:3001) | Core | Public status monitor |
| Ops Portal backend | systemd / uvicorn (:8090) | Core | FastAPI; SQLite (ops.db) |
| Postgres | host service (:5432) | Core | Shared; customer schemas move to app4. Core keeps a minimal instance only if a STAYS service still needs it, else decommission |
| Redis | host service (:6379) | Core | Shared cache; enumerate consumers in Phase 0 |
| sign.itpropartner.com | Caddy site | Core | DocuSeal frontend |
| ops.itpropartner.com | Caddy site | Core | Ops Portal frontend |
| uptimekuma.itpropartner.com | Caddy site | Core | Uptime Kuma frontend |
| my.itpropartner.com | Caddy site | Core | Customer hub |
| status.itpropartner.com | Caddy site | Core | Public status page |
| auth.itpropartner.com | Caddy site | Core | Centralized auth |
| voice.itpropartner.com | Caddy site | Core | Voice agent |
| voice-open.itpropartner.com | Caddy site | Core | Voice agent (open) |
| *.iamgmb.com | Caddy sites | Core | Customer sites |
| *.intelsight.io | Caddy sites | Core | Customer sites |
| *.fleettracker360.com | Caddy sites | Core | Customer sites |
| *.debtrecoveryexperts.com | Caddy sites | Core | Customer sites |
**Postgres / Redis split note:** both are shared instances today. They move per-app, not wholesale. Customer databases and caches are provisioned fresh on app4 and populated from dumps. Core keeps a Postgres/Redis instance only if a STAYS service (none currently identified) depends on it, otherwise they are decommissioned on Core after cutover.
**Inventory caveat (resolve in Phase 0):** the architecture plan (`server-architecture-plan` skill) records DocuSeal and SearXNG as having moved off Core in 2024/Aug 2026. This plan treats the supplied Core inventory as authoritative and classifies both on Core. Phase 0 must reconcile with `docker ps` and the live Caddyfile before any move.
---
## 3. app4 Sizing Recommendation
**Recommendation: netcup RS 4000 G12 (12 vCPU / 32 GB DDR5 ECC / 1 TB NVMe), ~$44/mo, Manassas VA.**
Justification:
- Matches the ITPP standard app tier. app1, app2, and app3 are all RS 4000 G12. Consistency simplifies provisioning, monitoring, DR, and cost accounting.
- The moved workload is app-tier, not hub-tier. app4 will host a dedicated Postgres + Redis, DocuSeal (Ruby on Rails), TimeTrex (PHP), the Ops Portal backend (FastAPI/uvicorn), the voice stack, and a dozen-plus customer Caddy sites. That is comparable to app1, which already runs an RS 4000.
- RS 2000 (8 vCPU / 16 GB) is too small. Core today runs everything on an RS 2000 and is being relieved precisely because it is overloaded. Squeezing the entire customer tier back onto a single RS 2000 would recreate the problem.
- 1 TB NVMe provides headroom for Postgres growth, Docker volumes, voice/audio assets, and backup retention without immediate pressure.
- Fault isolation: a customer-app outage on app4 no longer competes with Hermes on Core.
**Upsize trigger:** if the voice stack or customer site count grows materially, or Postgres usage exceeds ~40% of 32 GB, re-evaluate for RS 8000 (16 vCPU / 64 GB / 2 TB). Start at RS 4000.
---
## 4. Phased Migration Plan
### Phase 0: Inventory and Recon (Core, read-only)
- Confirm live inventory: `docker ps`, `docker volume ls`, `ss -tlnp`, `systemctl list-units --type=service`.
- Extract every moving site block from `/etc/caddy/Caddyfile` (site name, backend, TLS, redirects).
- Catalog data locations: Docker named volumes + bind mounts for DocuSeal, TimeTrex, microbin, Uptime Kuma, Ops Portal.
- Enumerate Postgres databases (`psql -l`) and map each to its app; enumerate Redis keyspace consumers.
- Record env files and secret references (Hudu / Vaultwarden) for each moving app.
- Record cron entries that touch the moving apps or their backups.
- Verify DNS authority per domain with `dig NS <domain>` (itpropartner.com is SiteGround manual; fleettracker360.com and voipsimplicity.com are Cloudflare; check iamgmb.com, intelsight.io, debtrecoveryexperts.com individually).
- Reconcile the DocuSeal / SearXNG inventory caveat from section 2.
- Produce the runbook: per-app data migration command, per-domain DNS record, and an acceptance checklist.
### Phase 1: Provision app4 + Monitoring First
- Order RS 4000 G12 per `server-provisioning-standard`: Debian 13, ippadmin user + sudo, itpp-infra SSH key, UFW (open 22, 80, 443), Fail2Ban, unattended-upgrades, Docker + compose plugin, Python, AWS CLI with the cron PATH fix, node_exporter on :9100, Tailscale.
- Install Caddy on app4 with `default_bind <app4-ipv4>` to avoid the Tailscale port 443 conflict.
- Enroll app4 in root-essentials-backup; run one manual backup and verify it lands in S3 (do not rely on the cron entry alone).
- Add app4:9100 to Core Prometheus targets and Grafana dashboards. Observability exists before any app moves.
- Do not move any customer app in this phase.
### Phase 2: Low-Risk Apps (prove the pattern)
- Move microbin and Uptime Kuma first. Small, self-contained, low blast radius.
- microbin: rsync volume, start on app4, verify via `curl --resolve`.
- Uptime Kuma: move after microbin; its monitors continue running and its own cutover is the first DNS flip of the whole project.
- Validate the rsync + healthcheck + rollback playbook on these two before touching customer apps.
- Confirm app4 S3 backups for these two are working.
### Phase 3: Customer Apps + Data
- Foundation first: provision Postgres and Redis on app4 (least-privilege, internal-only networks).
- Move Ops Portal backend (:8090), then DocuSeal, then TimeTrex.
- Bring each up on app4 on internal ports and test side-by-side with Core using `curl --resolve <domain>:443:<app4-ip>`.
- Move the voice stack (voice.itpropartner.com, voice-open.itpropartner.com), including any audio assets and external webhook/Twilio endpoint updates.
- Move static customer sites (*.iamgmb.com, *.intelsight.io, *.fleettracker360.com, *.debtrecoveryexperts.com) by rsyncing web roots.
- Verify Postgres row counts and Redis state after each app move (see section 5).
### Phase 4: DNS Cutover + Decommission on Core
- Lower TTL on all moving records to 300 (or 60) at least 24h before cutover.
- Flip DNS per domain, one at a time, low-traffic first, verifying each before the next.
- Keep critical Core Caddy blocks as a temporary 301 redirect to app4 during a 24 to 72h soak window; remove after verification.
- After soak: remove customer site blocks from Core Caddyfile (use targeted edits + caddy-audit hook, never rewrite the whole file), stop and remove moved containers on Core, retain volumes and images for 30 days as rollback.
- Decommission customer schemas in Core Postgres/Redis (or the whole instance if unused by Core).
- Update Prometheus targets, Uptime Kuma, docs, `app-inventory.csv`, and the recovery manual.
---
## 5. Data Migration Steps
Docker volumes (rsync, app stopped):
```bash
# On Core, stop the app, then delta-sync the volume data to app4
docker compose -f /root/docker/<app>/docker-compose.yml stop
rsync -az --delete \
/var/lib/docker/volumes/<volume>/_data/ \
ippadmin@app4:/var/lib/docker/volumes/<volume>/_data/
# On app4
docker compose -f /root/docker/<app>/docker-compose.yml up -d
```
SQLite (online-safe backup, never `cp` a live DB):
```bash
sqlite3 /path/app.db ".backup '/tmp/app-backup.db'"
rsync -az /tmp/app-backup.db ippadmin@app4:/path/app.db
```
Postgres (per-database custom-format dump):
```bash
# On Core
pg_dump -Fc -d <dbname> -f /tmp/<dbname>.dump
rsync -az /tmp/<dbname>.dump ippadmin@app4:/tmp/
# On app4
pg_restore -d <dbname> /tmp/<dbname>.dump
```
For a full-instance move, use `pg_dumpall` instead of per-database dumps.
Redis:
```bash
# If cache only: rebuild empty on app4. If state matters:
redis-cli BGSAVE # then rsync dump.rdb with Redis stopped, or configure replication during cutover
```
Post-migration verification (mandatory, per the Hudu lesson):
- Compare Postgres row counts for every major table between Core and app4, not just a spot check.
- Compare Docker volume sizes and file counts after rsync.
- Hit each domain through app4 with `curl --resolve` and compare responses against Core side-by-side.
- Do not declare a migration done on container health alone.
---
## 6. Caddy / DNS Change Checklist
- [ ] Verify authoritative nameservers per domain (`dig NS`). itpropartner.com is SiteGround manual; do not create records via Cloudflare for it.
- [ ] Lower TTL to 300 (or 60) on every moving record at least 24h before cutover.
- [ ] Pre-write the app4 Caddyfile with all moving site blocks; `caddy validate` it.
- [ ] Pre-issue TLS certs on app4 (on-demand or staging) before DNS flip.
- [ ] Open UFW 80/443 on app4 (netcup blocks them by default) and verify from an external network.
- [ ] Set `default_bind <app4-ipv4>` in app4 Caddy global block to avoid Tailscale :443 conflict.
- [ ] Flip each A/AAAA record to app4 in the domain's authoritative panel (SiteGround manual or Cloudflare API, per domain).
- [ ] Verify propagation: `dig +short @1.1.1.1 <domain>`.
- [ ] Verify service and cert on app4: `curl -sI https://<domain>`.
- [ ] Apply the caddy-audit hook before any Core Caddyfile edit; use targeted edits or `patch`, never a full rewrite.
- [ ] After soak, remove stale Core blocks and reload Caddy.
---
## 7. DR / Backup Implications
- Repoint backup scripts and cron from Core to app4 for every moved app (docker-volume-sync, any per-app backup jobs, root-essentials-backup).
- app4 gets its own S3 backup path under the existing Wasabi bucket, keyed by hostname, with least-privilege credentials.
- The daily Hermes backup on Core stops backing up customer app volumes once they move; confirm the app4 cron owns them before removing Core entries.
- Add app4 to the DR plan (`server-dr-plans.md`) and the recovery manual; document what to restore in what order.
- Decide standby scope: app1-bu is a warm standby for Core, not for customer apps. app4 relies on S3 backups unless a customer-app standby is separately approved.
- Use `/opt/awscli-venv/bin/aws` (full path) in every app4 backup script to avoid the silent cron PATH failure.
- After the first real backup on app4, perform a test restore of one app to prove the backups work, not just the cron entry.
---
## 8. Risks and Rollback
### Risks
| Risk | Impact | Mitigation |
| --- | --- | --- |
| DNS authority confusion (SiteGround vs Cloudflare) | Silent no-op record changes, outage | Verify `dig NS` per domain first; route changes through the correct panel |
| Shared Postgres/Redis partial migration | Core app breaks mid-move | Move per-app dumps, verify row counts, keep Core DB intact until cutover |
| Live-file copy corruption (cp on running SQLite/Postgres) | Data loss | Always stop app or use `.backup` / `pg_dump` |
| Voice stack hidden dependencies (Twilio webhooks, TTS/STT endpoints) | Voice breaks after cutover | Enumerate external webhooks in Phase 0, update endpoints before DNS flip |
| TLS issuance failure on app4 | Site unreachable | Pre-issue certs, confirm UFW 80/443 open, test externally |
| Caddyfile fragility (whole-file rewrite drops sites) | Silent domain loss | Targeted edits + caddy-audit hook, never full rewrite |
| Backup silently failing on app4 (aws not in PATH) | No restorable backup | Full-path AWS, manual test restore after first backup |
### Rollback
- Before each phase, snapshot: current DNS records, Core Caddyfile, and Core Docker state.
- Phase 2/3 rollback: stop the app on app4, flip DNS back to Core, restart the Core container. Core volumes are untouched and the app returns to its pre-move state.
- Phase 4 rollback: with low TTL, flipping the A record back to Core propagates in minutes; Core Caddy blocks are retained during the soak window for exactly this purpose.
- Data rollback: Core volumes and images are retained for 30 days after cutover, so any container can be restarted on Core instantly.
- After 30 days: restore from app4 S3 backups (this is why a test restore is mandatory in Phase 1).
+33
View File
@@ -0,0 +1,33 @@
# DocuSeal environment template. Replace every <...> placeholder.
# Never commit real values. chmod 600 after filling.
# Public base URL
HOST=https://sign.<entity>.example.com
FORCE_SSL=true
# Encryption root. Generate with: openssl rand -hex 64
# NEVER rotate on a live instance (ActiveRecord encryption root).
SECRET_KEY_BASE=<openssl rand -hex 64>
# SMTP (netcup smarthost: only port 2525 works; 25/465/587 are blocked)
SMTP_ADDRESS=<smtp-relay-hostname>
SMTP_PORT=2525
SMTP_USERNAME=<noreply@entity-domain>
SMTP_PASSWORD=<smtp-relay-password>
SMTP_AUTHENTICATION=login
SMTP_ENABLE_STARTTLS=true
SMTP_DOMAIN=<entity-domain>
SMTP_SSL_VERIFY=true
# SMTP_FROM is inert: DocuSeal hardcodes the mailer from as
# "DocuSeal <info@docuseal.com>". Do not expect it to change the sender.
# S3/Wasabi attachments. Uncomment and fill once the <entity>-legal bucket exists.
# Presence of S3_ATTACHMENTS_BUCKET activates S3 storage. Left unset, DocuSeal
# uses local disk storage under ./data/attachments.
#S3_ATTACHMENTS_BUCKET=<entity>-legal
#S3_ENDPOINT=s3.us-east-1.wasabisys.com
#AWS_ACCESS_KEY_ID=<wasabi-access-key-id>
#AWS_SECRET_ACCESS_KEY=<wasabi-secret-access-key>
#AWS_REGION=us-east-1
#ACTIVE_STORAGE_PUBLIC=false
@@ -0,0 +1,176 @@
# DocuSeal Deployment Template and Runbook
Spin up one self-hosted DocuSeal instance per legal entity. Reference live deployment: Core `sign.itpropartner.com` (container `docuseal`, image `docuseal/docuseal:latest`, bound `127.0.0.1:8091:3000`, volume `./data:/data`, `env_file .env`).
## Hard isolation rule
One instance per legal entity. Do NOT multi-tenant a single DocuSeal across entities. Distinct legal entities require hard isolation: signatures, templates, and audit data must never mix. DocuSeal has a multitenant mode but it is not approved for cross-entity use here. Deploy a separate container, data volume, subdomain, and S3 bucket for each entity.
First target: Model Ortho. Future entities follow the same steps with a new slug, port, subdomain, and bucket.
## Port remap
Core host port 3000 is occupied by browserless. Bind each instance to a unique loopback port, mapping to container port 3000:
- Core `sign.itpropartner.com`: `127.0.0.1:8091`
- Model Ortho: next free port, e.g. `127.0.0.1:8092`
List loopback listeners and pick an unused port:
```
ss -tln | grep 127.0.0.1
```
## Prerequisites
- Docker and Compose on the host (Core today, app4 in future)
- DNS record for the sign subdomain
- Wasabi S3 bucket named `<entity>-legal` (e.g. `modelortho-legal`) for attachments
- SMTP relay credentials (netcup smarthost: only port 2525 works; 25/465/587 are blocked)
- Vaultwarden (`bw` CLI) for credential storage
## Steps
1. Create the instance directory:
```
mkdir -p /root/docker/docuseal-<entity> && cd /root/docker/docuseal-<entity>
```
2. Copy the templates and rename:
```
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/docker-compose.yml.template docker-compose.yml
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/.env.example .env
chmod 600 .env
```
3. Edit `docker-compose.yml`. Replace `__ENTITY_SLUG__` with the entity slug and `__HOST_PORT__` with the unique loopback port.
4. Fill `.env`. Set HOST, the SMTP_* vars, SECRET_KEY_BASE, and (once the bucket exists) the S3_/AWS_ vars. Generate the secret:
```
openssl rand -hex 64
```
Record it in Vaultwarden (step 10). NEVER rotate it once the instance is live.
5. Start the container:
```
docker compose up -d
```
6. Verify boot. A clean instance returns HTTP 302 to /setup:
```
curl -sI http://127.0.0.1:<port> | head -1
```
A 500 means a stale encrypted DB under a different SECRET_KEY_BASE. Move data/ aside and boot clean:
```
mv data data.old-$(date +%s) && docker compose up -d
```
7. Complete setup at `https://<host>/setup` (owner email and password). If the submit button does not advance, run in the browser console:
```
document.querySelector('form').requestSubmit()
```
8. Add the Caddy block for the sign subdomain:
```
<host> {
reverse_proxy 127.0.0.1:<port>
}
```
Then validate and reload:
```
caddy validate && systemctl reload caddy
```
9. Point DNS (A record to origin; grey-cloud for `*.iamgmb.com`). Verify:
```
curl -sI https://<host> | head -1
```
Expected: HTTP/2 200.
10. Store admin credentials and the secret in Vaultwarden:
```
bw create item '{"type":1,"name":"DocuSeal <entity> admin","login":{"username":"owner@<entity>.com","password":"<admin password>","uris":[{"uri":"https://<host>"}]},"notes":"SECRET_KEY_BASE never rotate"}'
bw create item '{"type":2,"name":"DocuSeal <entity> SECRET_KEY_BASE","notes":"<secret key base>\nNEVER rotate on a live instance. ActiveRecord encryption root."}'
```
11. Wire backup. Copy the Core script pattern (see Backup section) to `/root/.hermes/scripts/docuseal-<entity>-backup.sh` and add a Hermes cron job (daily).
## SMTP env vars (exact names, verified against running container)
- `SMTP_ADDRESS`: mail.itpropartner.com (or entity relay). NOT SMTP_HOST.
- `SMTP_PORT`: 2525 (netcup smarthost; 25/465/587 blocked).
- `SMTP_USERNAME`: noreply@<domain>. NOT SMTP_USER_NAME.
- `SMTP_PASSWORD`: relay password.
- `SMTP_AUTHENTICATION`: login (honored only when SMTP_PASSWORD present).
- `SMTP_ENABLE_STARTTLS`: true.
- `SMTP_DOMAIN`: <domain>.
- `SMTP_SSL_VERIFY`: true (false = VERIFY_NONE).
SMTP_FROM is INERT. DocuSeal does not read it. The mailer default from is hardcoded as `DocuSeal <info@docuseal.com>` in `app/mailers/application_mailer.rb`. Branding the From address requires a fork, which conflicts with the AGPL rule below.
## S3/Wasabi attachment vars
S3 storage activates when S3_ATTACHMENTS_BUCKET is present. Exact vars read from `config/storage.yml`:
- `S3_ATTACHMENTS_BUCKET`: <entity>-legal (presence triggers S3 storage).
- `S3_ENDPOINT`: s3.us-east-1.wasabisys.com (sets force_path_style true).
- `AWS_ACCESS_KEY_ID`: Wasabi access key.
- `AWS_SECRET_ACCESS_KEY`: Wasabi secret key.
- `AWS_REGION`: us-east-1 (default).
- `ACTIVE_STORAGE_PUBLIC`: false (optional).
Leave these unset to use local disk storage under `./data/attachments`.
## SECRET_KEY_BASE rule
SECRET_KEY_BASE is the encryption root for ActiveRecord encrypted columns in the SQLite DB. NEVER rotate it on a live instance. Rotating it breaks decryption (AEAD authentication tag verification failed, HTTP 500). Back it up with the instance and record it in Vaultwarden with a never-rotate note.
## Backup script pattern
Runs locally on the host (no SSH). Archive the data dir, compose file, and .env so a restore is fully self-contained. Upload to the entity legal/ops bucket:
```
#!/bin/bash
set -euo pipefail
if [ -f /opt/awscli-venv/bin/activate ]; then source /opt/awscli-venv/bin/activate; fi
S3_BUCKET="s3://<entity>-legal/docuseal"
S3_ENDPOINT="--endpoint-url https://s3.us-east-1.wasabisys.com"
DOCUSEAL_DIR="/root/docker/docuseal-<entity>"
TSTAMP=$(date +%Y%m%d-%H%M%S)
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
tar czf "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" -C "$DOCUSEAL_DIR" data docker-compose.yml .env
aws s3 cp $S3_ENDPOINT "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" "$S3_BUCKET/docuseal-backup-$TSTAMP.tar.gz"
```
Reference implementation: `/root/.hermes/scripts/docuseal-backup.sh`. Drive with a Hermes cron job.
## AGPL-3.0 constraint
DocuSeal is AGPL-3.0. Run it UNMODIFIED and integrate via API only. Do not fork or patch the source. Consequences: no branded From address on email (see SMTP_FROM above), and all automation goes through DocuSeal's API rather than code changes.
## docuseal:latest image note
Newer `docuseal:latest` bundles Redis and Sidekiq inside the single container. It may log a harmless memory overcommit warning (`vm.overcommit_memory`). This is cosmetic; the container runs fine. No separate Redis or Sidekiq services are needed.
## Teardown
1. Remove the Caddy block, then `caddy validate` and `systemctl reload caddy`.
2. `docker compose down` (NOT `stop`: `restart: always` resurrects stopped containers on reboot; `down` removes the container while the bind-mounted data/ is preserved).
3. Delete the DNS record.
4. Never delete an old encrypted DB. Preserve as `data.old-<epoch>`.
@@ -0,0 +1,22 @@
# DocuSeal compose template. Replace __ENTITY_SLUG__ and __HOST_PORT__.
# One instance per legal entity. Do not reuse a container_name or host port.
# Newer docuseal:latest bundles Redis + Sidekiq internally, so no separate
# redis/sidekiq services are needed. It may log a harmless memory overcommit
# warning.
services:
docuseal:
image: docuseal/docuseal:latest
container_name: docuseal-__ENTITY_SLUG__
restart: always
ports:
# 3000 inside the container. Host 3000 is browserless on Core.
- "127.0.0.1:__HOST_PORT__:3000"
volumes:
- ./data:/data
env_file:
- .env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"