Reference
Docker Configuration
Docker Configuration Reference
Complete reference for environment variables and volume mounts used in eIOU Docker containers.
Table of Contents
- Environment Variables
- Volume Mounts
- Network Configuration
- Resource Limits
- SSL Certificate Configuration
- Wallet Restoration
- Timeout Configuration
- Healthcheck Configuration
- Security Configuration
- Backup and Restore
- Troubleshooting
Environment Variables
Quick Reference
| Variable | Default | Required | Description |
|---|---|---|---|
EIOU_HOST |
(none) | No* | Hostname / address for HTTP/HTTPS addressing (IP, FQDN, or bare hostname; optional :port). Setting this enables HTTP/HTTPS mode |
EIOU_NAME |
$EIOU_HOST |
No | Display name for the node (shown in local UI) |
EIOU_PORT |
(none) | No | Port for HTTP/HTTPS URLs (appended to addresses) |
RESTORE |
(none) | No | 24-word seed phrase for wallet restoration |
RESTORE_FILE |
(none) | No | Path to file containing seed phrase |
SSL_DOMAIN |
$EIOU_HOST |
No | Primary domain for SSL certificate CN |
SSL_EXTRA_SANS |
(none) | No | Additional Subject Alternative Names |
LETSENCRYPT_EMAIL |
(none) | No | Email for Let’s Encrypt — enables automatic trusted certs |
LETSENCRYPT_DOMAIN |
$SSL_DOMAIN |
No | Domain for Let’s Encrypt certificate |
LETSENCRYPT_STAGING |
false |
No | Use Let’s Encrypt staging server for testing |
EIOU_HS_TIMEOUT |
60 |
No | Tor hidden service wait timeout (seconds) |
EIOU_TOR_TIMEOUT |
120 |
No | Tor connectivity timeout (seconds) |
EIOU_TOR_STRICT_READINESS |
false |
No | Fail startup instead of serving after the Tor SOCKS5 readiness timeout |
EIOU_TEST_MODE |
false |
No | Enable manual message processing |
EIOU_CONTACT_STATUS_ENABLED |
true |
No | Enable contact status pinging |
EIOU_BACKUP_AUTO_ENABLED |
true |
No | Enable/disable automatic daily backups |
EIOU_AUTO_CHAIN_DROP_PROPOSE |
true |
No | Auto-propose tx drops when mutual gaps detected |
EIOU_AUTO_CHAIN_DROP_ACCEPT |
false |
No | Auto-accept incoming tx drop proposals (with balance guard) |
EIOU_AUTO_CHAIN_DROP_ACCEPT_GUARD |
true |
No | Balance guard for auto-accept: blocks if missing txs erase debt owed to us |
EIOU_AUTO_ACCEPT_RESTORED_CONTACT |
true |
No | Auto-accept restored contacts on wallet restore when transaction history proves prior relationship |
APP_DEBUG |
true |
No | Enable debug logging to database (visible in GUI Debug panel). Set to false for production |
EIOU_DEFAULT_TRANSPORT_MODE |
tor |
No | Default transport when sending to a contact name (http, https, tor) |
EIOU_TOR_FORCE_FAST |
true |
No | Auto-enable fast mode for Tor routes. Set to false for best-fee testing over Tor |
EIOU_HOP_BUDGET_RANDOMIZED |
true |
No | Randomize hop budget via geometric distribution. Set to false for deterministic routing depth |
EIOU_UPDATE_CHECK_ENABLED |
true |
No | Check Docker Hub daily for newer image versions. Set to false to disable all external API calls |
EIOU_ANALYTICS_ENABLED |
false |
No | Share anonymous usage statistics, batched daily by a cron job (opt-in, not real-time). Sends only aggregate transaction counts and volume per currency — no personal data, amounts per transaction, contacts, or addresses |
EIOU_VOLUME_KEY_FILE |
(none) | No | Path to file containing volume encryption passphrase (recommended) |
EIOU_VOLUME_KEY |
(none) | No | Volume encryption passphrase as environment variable (less secure) |
P2P_SSL_VERIFY |
true |
No | Verify SSL certificates on outbound P2P HTTPS connections. When true (default), self-signed certs are rejected — set to false for dev/testing with auto-generated certs, use P2P_CA_CERT for a shared CA, or place nodes behind a reverse proxy with valid certificates |
P2P_CA_CERT |
(none) | No | Path to a CA certificate file inside the container. When set, P2P SSL verification uses this CA instead of the system bundle — use with a volume mount (e.g., ./ssl-ca:/ssl-ca:ro) |
TRUSTED_PROXIES |
(none) | No | Comma-separated trusted reverse-proxy IPs or narrow CIDRs for forwarded-IP parsing. Prefer exact addresses; IPv4 /8 or broader and equivalent embedded-IPv4 ranges are ignored fail-closed |
EIOU_PUBLIC_PLUGIN_ROUTES |
allow |
No | Node-level ceiling for plugin public routes: allow (permitted, then enabled per-plugin), off (none, and the node’s admin cannot enable any), or on (force all enabled plugins’ routes live). The default exposes nothing on its own; the per-plugin toggle does. Set off to stop a tenant exposing public HTTP on a node you host for them. See /docs/reference/plugins |
EIOU_PLUGIN_MASTER_KEY |
(auto-generated) | No | 32-byte hex master key (64 lowercase hex chars) that sandboxed plugins derive their per-plugin at-rest encryption keys from. Persisted to /etc/eiou/config/plugin-master.key (0640 root:www-data) if unset. See SECURITY.md |
EIOU_PLUGIN_SUPERVISOR_TIMEOUT_SECONDS |
15 |
No | Deadline (clamped 5–120s) the wallet waits on the supervisor credentials poller when applying a sandboxed plugin’s DB user. Raise on slow hosts (constrained CI / WSL / Docker Desktop) where the poller starts late and the first cold-boot export races it |
EIOU_IDENTITY_MONITOR |
off |
No | Best-effort self-check that warns if this seed’s .onion appears to be served by a second node at once. Off by default (self-detection is unreliable); eiou retire is the robust mitigation |
EIOU_CONFIRM_SINGLE_NODE |
(none) | No | Set to 1 to acknowledge the restore-time single-node warning (confirms no other node is running the same seed) |
| Service Tuning | |||
NGINX_WORKER_PROCESSES |
2 |
No | Nginx worker thread count (match to CPU allocation) |
NGINX_WORKER_CONNECTIONS |
768 |
No | Max connections per nginx worker |
NGINX_CLIENT_MAX_BODY |
10m |
No | Max request body size |
NGINX_RATE_LIMIT_GENERAL |
30r/s |
No | General endpoint rate limit |
NGINX_RATE_LIMIT_API |
10r/s |
No | API endpoint rate limit |
NGINX_RATE_LIMIT_P2P |
20r/s |
No | P2P endpoint rate limit |
NGINX_CONN_LIMIT |
50 |
No | Max concurrent connections per IP |
EIOU_ALLOW_PRIVATE_DELIVERY |
false |
No | Opt in to HTTP(S) delivery to private addresses on the node’s permitted mesh subnet(s) |
PHP_FPM_PM |
ondemand |
No | Process manager mode: ondemand, dynamic, static |
PHP_FPM_MAX_CHILDREN |
5 |
No | Max PHP worker processes (~20-30MB RAM each) |
PHP_FPM_START_SERVERS |
2 |
No | Initial workers on startup (dynamic mode only) |
PHP_FPM_MIN_SPARE |
1 |
No | Min idle workers (dynamic mode only) |
PHP_FPM_MAX_SPARE |
3 |
No | Max idle workers (dynamic mode only) |
PHP_FPM_IDLE_TIMEOUT |
10s |
No | Kill idle workers after this timeout (ondemand mode only) |
PHP_FPM_MAX_REQUESTS |
0 |
No | Recycle worker after N requests (0 = unlimited) |
EIOU_WATCHDOG_MAX_RESTARTS |
10 |
No | Max crash-restarts the watchdog allows per background message processor within the stability window before it stops respawning it. Raise for long, restart-heavy runs (e.g. full test suites) |
EIOU_WATCHDOG_RESET_COOLDOWN |
300 |
No | Seconds a processor must run without a restart before the watchdog resets its crash-restart counter to zero, so the budget counts crashes within a window rather than over the node’s entire uptime |
*Required unless using Tor-only mode
Detailed Descriptions
EIOU_HOST / EIOU_NAME / EIOU_PORT
These variables configure the node’s externally reachable identity. Setting EIOU_HOST enables HTTP/HTTPS mode; omitting it leaves the node Tor-only.
EIOU_NAME is purely local — it is never broadcast to contacts or other nodes. It appears in the GUI wallet header, Docker startup logs, and any integration that reads the node’s display name.
| Variable | Purpose | Default |
|---|---|---|
EIOU_HOST |
Hostname / address (IP, FQDN, or bare hostname for Docker-network-only access; optional :port) |
None — without it the node runs Tor-only |
EIOU_NAME |
Display name (shown in local UI) | Falls back to EIOU_HOST |
EIOU_PORT |
Externally-published HTTPS port — drives nginx HTTP→HTTPS redirect target, SSL cert CN, and the eiou info browser-display URL. Does NOT fold into the locator peers receive (see “EIOU_PORT vs the locator port” below) |
Not used as a redirect target if omitted |
EIOU_PORT vs the locator port
EIOU_PORT and the port that goes into your wallet’s stored locator (what peers receive when adding you as a contact) are two separate concerns. They are conflated in many ops mental models because the same variable used to drive both — that wiring is gone now.
| Concern | Driven by | Why |
|---|---|---|
| nginx HTTP→HTTPS redirect target | EIOU_PORT |
Browser hitting http://host:8080/ needs a redirect Location with the externally-published HTTPS port; nginx’s $host strips the port off the Host header, so the port is substituted in via this env. |
eiou info browser-display URL |
EIOU_PORT |
The operator looks at the URL the local host is reachable on for browser access. |
| SSL cert CN / SANs | EIOU_HOST (port-stripped) |
Ports are not part of certificate CNs. |
| Wallet-stored locator (what peers see) | EIOU_HOST’s embedded port, OR explicit eiou changesettings hostname https://…:port, OR omit for default 443 |
Peers reach you on whatever port the locator advertises. In containerized deployments the externally-published port (e.g. -p 9443:443) is not bound on the internal docker network where cross-container P2P actually flows — only port 443 is — so folding EIOU_PORT into the locator produces addresses that don’t connect over docker. |
If you want peers to see a non-default port in your locator, embed it in EIOU_HOST (e.g., EIOU_HOST=example.com:9443) or run eiou changesettings hostname https://example.com:9443 after first boot. Both paths route through CliSettingsService::splitHostnameForms() which preserves the explicit port on the form you provided and strips it from the derived complementary form (so https://example.com:9443 produces hostname_secure=https://example.com:9443 plus hostname=http://example.com — the HTTP port defaults to 80 unless you set it explicitly via the HTTP form, which gets the symmetric treatment).
If EIOU_HOST carries an embedded port, that port also flows into EIOU_PORT for the redirect / display concerns above (priority: explicit EIOU_PORT > embedded port from EIOU_HOST).
Example: Docker-internal (testing)
environment:
- EIOU_HOST=alice # Reachable as http://alice / https://alice on the Docker network
- EIOU_ALLOW_PRIVATE_DELIVERY=true # Required for delivery to private Docker-network peers
The node will be accessible at:
http://alice(within Docker network)https://alice(within Docker network, self-signed certificate)
Important: A bare hostname like
aliceresolves only via Docker’s internal DNS — other containers on the same Docker network can reach it, but the host machine and the internet cannot. For external access, setEIOU_HOSTto a real IP address or domain name,EIOU_PORTto the mapped port, and use a trusted SSL certificate (Let’s Encrypt or CA-signed) instead of the auto-generated self-signed one.Private-address delivery is blocked by default. Set
EIOU_ALLOW_PRIVATE_DELIVERY=trueonly on nodes intentionally participating in a private Docker mesh; loopback, link-local, and metadata targets remain blocked.HTTPS with self-signed certs: The auto-generated self-signed certificate allows HTTPS connections, but other nodes will reject these certificates by default because
P2P_SSL_VERIFY=true. To send transactions over HTTPS between auto-cert nodes, either setP2P_SSL_VERIFY=falsein all nodes (dev/testing only), use a shared CA viaP2P_CA_CERT, place nodes behind a reverse proxy with valid certificates, or use proper trusted certificates. See P2P SSL troubleshooting.
Example: Production node on the default HTTPS port (443)
environment:
- EIOU_HOST=88.99.69.172 # External IP address
- EIOU_NAME=Dave # Local display name
This generates the locator:
https://88.99.69.172(peers connect on the default port 443)http://88.99.69.172(HTTP form; peers’ HTTPS-redirect-on-receive lands them on 443)- Display name: “Dave”
Example: Production node on a non-default port that peers need to dial
When peers actually need to reach you on a non-default port, the port has to be in the locator. Embed it in EIOU_HOST:
environment:
- EIOU_HOST=88.99.69.172:1133 # Port embedded in host → flows into locator
- EIOU_NAME=Dave
This generates:
https://88.99.69.172:1133(locator; peers dial 1133)http://88.99.69.172(HTTP form, port-less so the default 80 applies — the HTTPS port is not the HTTP port)- Display name: “Dave”
The embedded port also flows into EIOU_PORT for the nginx-redirect / browser-display concerns, so a setup with EIOU_HOST=…:1133 is equivalent to setting both EIOU_HOST and EIOU_PORT for those purposes.
Example: docker-network testbed where EIOU_PORT is browser-only
When EIOU_PORT is set as a standalone (not embedded in EIOU_HOST), it’s interpreted as a browser-convenience setting and does NOT enter the locator. This is the right shape for multi-node docker testbeds:
environment:
- EIOU_HOST=alice
- EIOU_PORT=8443 # nginx redirect target only; not in locator
- EIOU_ALLOW_PRIVATE_DELIVERY=true # Opt in to private Docker-network peers
ports:
- "8443:443"
- "8080:80"
The browser opens https://localhost:8443 via the published port; nginx’s HTTP→HTTPS redirect uses EIOU_PORT to land on the right target; the wallet’s locator stays https://alice (port-less) so sibling containers reaching each other over the docker network land on the internally-bound port 443 rather than the unreachable externally-mapped port 8443. Every node that must deliver to those private peers needs the explicit opt-in shown above.
Priority:
- Name:
EIOU_NAME>EIOU_HOST - SSL CN:
SSL_DOMAIN>EIOU_HOST
RESTORE / RESTORE_FILE
Restore a wallet from a BIP39 24-word seed phrase. Two methods are available:
Method 1: File-based (RECOMMENDED)
More secure as the seed phrase is not visible in process listings or environment variable dumps.
environment:
- RESTORE_FILE=/restore/seed
volumes:
- /path/to/seed.txt:/restore/seed:ro
Method 2: Environment variable
Convenient but less secure - the seed phrase may be visible in logs or process listings.
environment:
- "RESTORE=word1 word2 word3 ... word24"
Or using Docker CLI:
docker run -e "RESTORE=word1 word2 word3 ... word24" ...
Important: The seed phrase value must be quoted because it contains spaces. Without quotes, only the first word would be captured.
Security Note: When using the
RESTOREenvironment variable, the container will display a warning recommendingRESTORE_FILEinstead. TheRESTOREvariable is automatically unset after successful seed restoration to prevent the seed phrase from remaining in the environment during normal operation.
Combining with
EIOU_HOST: When bothRESTORE(orRESTORE_FILE) andEIOU_HOSTare set, the wallet is first restored from the seed phrase (restoring keys and Tor address), and then theEIOU_HOSThostname is automatically applied as the HTTP/HTTPS address. This allows a restored wallet to be reachable at both its original Tor address and the new HTTP/HTTPS hostname. Example:environment: - EIOU_HOST=alice - RESTORE_FILE=/restore/seed volumes: - /path/to/seed.txt:/restore/seed:ro
SSL_DOMAIN
Override the primary domain used in the SSL certificate’s Common Name (CN). Useful when the container hostname differs from the external domain.
environment:
- EIOU_HOST=internal-name
- SSL_DOMAIN=public.example.com
SSL_EXTRA_SANS
Add additional Subject Alternative Names (SANs) to the SSL certificate. Format: comma-separated list of TYPE:value pairs.
environment:
- SSL_EXTRA_SANS=DNS:alt.example.com,IP:192.168.1.100,DNS:node.local
Supported types:
DNS:hostname- Additional DNS namesIP:address- IP addresses
EIOU_VOLUME_KEY / EIOU_VOLUME_KEY_FILE
Optional passphrase that encrypts the master encryption key at rest on the Docker volume. When set, the host server cannot read the master key (or any data derived from it) without the passphrase. The master key is decrypted to /dev/shm (RAM-only) on every boot.
EIOU_VOLUME_KEY_FILE (recommended): Path to a file containing the passphrase. The file is read once at startup and the contents are stored in RAM. Not visible in docker inspect.
EIOU_VOLUME_KEY: Passphrase as an environment variable. Convenient for development but visible in docker inspect and process listings. The variable is cleared from the shell environment after reading, but remains in /proc/<pid>/environ.
# Recommended: file-based passphrase
services:
alice:
environment:
- EIOU_HOST=alice
- EIOU_VOLUME_KEY_FILE=/run/secrets/volume_key
volumes:
- ./volume-key.txt:/run/secrets/volume_key:ro
First boot with passphrase: The plaintext master key is encrypted and the plaintext copy is securely deleted (overwrite + unlink). The encrypted key is stored as .master.key.enc on the config volume.
Subsequent boots: The encrypted master key is decrypted to /dev/shm/.master.key before any services start.
Without passphrase: Data-at-rest encryption (MariaDB TDE) is still active — only the master key itself is unprotected on the volume. Adding a passphrase later will automatically migrate the existing plaintext key to encrypted format.
Warning: If you lose the passphrase and don’t have the 24-word seed phrase, the node data cannot be recovered.
EIOU_TEST_MODE
Enables test mode for manual message processing. When enabled:
- Unlocks CLI commands
eiou inandeiou outfor manual message queue processing - Bypasses rate limiting for automated testing
- Should only be used in development/testing environments
environment:
- EIOU_TEST_MODE=true
Warning: Never enable in production - bypasses security rate limiting.
EIOU_CONTACT_STATUS_ENABLED
Controls the contact status polling background processor. When enabled (default), the node periodically pings contacts to update their online/offline status.
environment:
- EIOU_CONTACT_STATUS_ENABLED=true # Enable (default)
- EIOU_CONTACT_STATUS_ENABLED=false # Disable
Use Cases:
- Disable during automated tests to prevent interference with sync operations
- Disable in low-bandwidth environments to reduce network traffic
EIOU_BACKUP_AUTO_ENABLED
Controls automatic daily database backups at midnight. Backups are encrypted using the master key derived from the seed phrase.
environment:
- EIOU_BACKUP_AUTO_ENABLED=true # Enable (default)
- EIOU_BACKUP_AUTO_ENABLED=false # Disable
Notes:
- Backups are stored in
/var/lib/eiou/backups/ - Only the 3 most recent backups are retained (configurable)
- Backups are encrypted with AES-256-GCM
- Restore requires wallet restoration first (the master key is derived from the seed phrase)
EIOU_AUTO_CHAIN_DROP_PROPOSE
Controls whether tx drops are automatically proposed when send or ping detects a mutual gap that sync and backup recovery cannot repair.
environment:
- EIOU_AUTO_CHAIN_DROP_PROPOSE=true # Enable (default)
- EIOU_AUTO_CHAIN_DROP_PROPOSE=false # Disable — require manual `eiou chaindrop propose`
Notes:
- When disabled, users must manually run
eiou chaindrop propose <contact>or use the GUI - Sync and backup recovery still run automatically regardless of this setting
- Only affects auto-proposal; incoming proposals are still received and stored
EIOU_AUTO_CHAIN_DROP_ACCEPT
Controls whether incoming tx drop proposals are automatically accepted. A balance guard compares stored balances against transaction-calculated balances to block proposals where missing transactions would erase debt owed to us.
environment:
- EIOU_AUTO_CHAIN_DROP_ACCEPT=false # Disable (default) — require manual accept
- EIOU_AUTO_CHAIN_DROP_ACCEPT=true # Enable — auto-accept with balance guard
Notes:
- Default is OFF for safety — all proposals require manual review via CLI or GUI
- When enabled, the balance guard blocks auto-accept if
net_missing > 0(missing transactions include net payments to us that would be erased) - Blocked proposals remain pending for manual review
- The guard compares stored balance (from
balancestable) with balance calculated from existing transactions; if they match,net_missing = 0and auto-accept proceeds
EIOU_AUTO_CHAIN_DROP_ACCEPT_GUARD
Controls whether the balance guard runs before auto-accepting tx drop proposals. When disabled, auto-accept proceeds unconditionally (no balance comparison).
environment:
- EIOU_AUTO_CHAIN_DROP_ACCEPT_GUARD=true # Enable (default) — check balances before accepting
- EIOU_AUTO_CHAIN_DROP_ACCEPT_GUARD=false # Disable — accept unconditionally
Notes:
- Only relevant when
EIOU_AUTO_CHAIN_DROP_ACCEPT=true - Default is ON — the balance guard runs before every auto-accept
- Set to
falseif you want truly unconditional auto-accept behavior - Can also be toggled per-node via CLI (
changesettings autoChainDropAcceptGuard), GUI, or API
EIOU_AUTO_ACCEPT_RESTORED_CONTACT
Controls whether contacts are auto-accepted on wallet restore when transaction history proves a prior relationship. When a node is restored from seed (empty database) and receives a ping from a former contact, the system syncs transaction history. If transactions are found:
- Enabled (default): the contact is auto-accepted with default fee (0%) and credit limit (1000). The contact name is set to
RestoredContact<N>. The user should manually reconfigure the name, fee percentages, and credit limits per currency to match the original terms. - Disabled: the contact stays pending for manual review. Transaction history is synced so it’s visible when reviewing.
environment:
- EIOU_AUTO_ACCEPT_RESTORED_CONTACT=true # Enable (default) — auto-accept with defaults
- EIOU_AUTO_ACCEPT_RESTORED_CONTACT=false # Disable — leave pending for manual review
Notes:
- Default is ON — restored contacts are auto-accepted when transaction history proves the relationship
- Original fee structures and credit limits are NOT restored — they reset to node defaults
- Contact is named
RestoredContact<N>and should be renamed by the user - Can also be toggled per-node via CLI (
changesettings autoAcceptRestoredContact), GUI, or API
APP_DEBUG
Controls debug logging to the database. When enabled, log entries are written to the debug table and viewable in the GUI Debug Information panel.
environment:
- APP_DEBUG=true # Enable (default during alpha) — debug entries visible in GUI
- APP_DEBUG=false # Disable — recommended for production deployments
Notes:
- Default is
trueduring the alpha/development phase - Set to
falsein production to disable debug database writes - File-based logging (
/var/log/eiou/app.log) is always active regardless of this setting
EIOU_DEFAULT_TRANSPORT_MODE
Sets the default transport type used when sending to a contact by name. In production, defaults to tor for maximum privacy. Test environments typically override to http.
environment:
- EIOU_DEFAULT_TRANSPORT_MODE=tor # Default — use Tor for privacy
- EIOU_DEFAULT_TRANSPORT_MODE=http # Use HTTP (testing)
- EIOU_DEFAULT_TRANSPORT_MODE=https # Use HTTPS
EIOU_TOR_FORCE_FAST
Controls whether Tor routes automatically use fast mode (single-route, first-response). When enabled (default), P2P transactions over Tor skip best-fee collection to avoid long Tor timeout delays.
environment:
- EIOU_TOR_FORCE_FAST=true # Default — fast mode for Tor routes
- EIOU_TOR_FORCE_FAST=false # Allow best-fee mode over Tor (slower)
EIOU_HOP_BUDGET_RANDOMIZED
Controls whether the P2P hop budget uses geometric distribution randomization (30% stop probability per hop) for traffic analysis resistance. Disable for deterministic routing depth in tests.
environment:
- EIOU_HOP_BUDGET_RANDOMIZED=true # Default — randomized hop budget
- EIOU_HOP_BUDGET_RANDOMIZED=false # Deterministic — uses maxP2pLevel directly
Volume Mounts
Required Volumes
All eIOU containers use named volumes for data persistence:
| Volume | Container Path | Purpose | Backup Priority |
|---|---|---|---|
{node}-mysql-data |
/var/lib/mysql |
Database: transactions, contacts, balances | CRITICAL |
{node}-config |
/etc/eiou/config |
Config: wallet keys, userconfig.json, encryption data | CRITICAL |
{node}-plugins |
/etc/eiou/plugins |
Installed plugin directories. Bundled plugins (e.g. hello-eiou) are baked into the image at /app/plugins/ and seeded into this volume on first boot via startup.sh with cp -rn, so operator-added plugins survive container rebuilds and removed bundled plugins stay removed across upgrades. |
IMPORTANT |
{node}-plugin-scratch |
/var/lib/eiou/plugin-scratch |
Each sandboxed plugin’s private writable directory (/var/lib/eiou/plugin-scratch/<plugin-system-user>/), where the plugin keeps the durable state it produces (keys, balances, ledgers, file-backed stores, locks, caches). The supervisor creates and chowns each plugin’s 0700 subdir; this volume keeps that state across a container recreate. A plugin that stores money or credentials here makes this CRITICAL; treat it as such unless you know no installed plugin keeps durable state on disk. |
CRITICAL |
{node}-backups |
/var/lib/eiou/backups |
Encrypted database backups | CRITICAL |
{node}-backup-locks |
/var/lib/eiou/backup-locks |
Backup lifecycle lock and durable pending archive-snapshot state | CRITICAL |
{node}-ssl-cert |
/var/lib/eiou/ssl |
All SSL state on one logical volume, organised into subdirectories. startup.sh creates ssl/letsencrypt/ and ssl/nginx/ on first boot and symlinks /etc/letsencrypt and /etc/nginx/ssl into them, so certbot and nginx see the canonical paths they expect while operators see one volume on disk. ssl/letsencrypt/ holds certbot accounts, renewal configs, and live cert symlinks (mostly empty on nodes that never enable Let’s Encrypt). ssl/nginx/ holds the cert nginx actually presents on :443 — persists self-signed certs across docker rm + docker run (e.g. image updates) so clients don’t see a new fingerprint on every rebuild, and also receives the copy of any Let’s Encrypt cert. Operators can add subdirectories under ssl/ for additional SSL providers without touching the volumes section. Safe to comment out — fresh cert regenerates on next start. |
Low |
Example:
volumes:
- alice-mysql-data:/var/lib/mysql # Transaction history, contacts
- alice-config:/etc/eiou/config # Wallet keys, configuration
- alice-plugins:/etc/eiou/plugins # Installed plugins
- alice-plugin-scratch:/var/lib/eiou/plugin-scratch # Plugin runtime state
- alice-backups:/var/lib/eiou/backups # Encrypted backups
- alice-backup-locks:/var/lib/eiou/backup-locks # Backup coordination
- alice-ssl-cert:/var/lib/eiou/ssl # TLS state (regenerable)
Optional Volumes
External SSL Certificates
Mount externally-obtained SSL certificates (Let’s Encrypt, commercial CA, etc.):
volumes:
- /path/to/certs:/ssl-certs:ro
Required files in the mounted directory:
server.crt- SSL certificate (PEM format)server.key- Private key (PEM format)ca.crt(optional) - CA certificate chain
CA-Signed Certificates
Mount a local Certificate Authority for signing certificates:
volumes:
- ./ssl-ca:/ssl-ca:ro
Required files:
ca.crt- CA certificateca.key- CA private key
Generate a CA using: ./scripts/create-ssl-ca.sh ./ssl-ca
Network Configuration
Docker Network
eIOU containers communicate over a Docker bridge network. All compose files use a shared network named eiou-network-compose.
networks:
eiou-network-compose:
driver: bridge
name: eiou-network-compose
Port Mappings
Single Node
| Port | Protocol | Purpose |
|---|---|---|
| 80 | TCP | HTTP web interface and API |
| 443 | TCP | HTTPS web interface and API (SSL) |
Multi-Node Topologies
All nodes in multi-node setups expose unique ports for external access:
| Topology | Nodes | HTTP Ports | HTTPS Ports |
|---|---|---|---|
| 4-line | alice, bob, carol, daniel | 8080-8083 | 8443-8446 |
| 10-line | node-a through node-j | 8080-8089 | 8443-8452 |
| cluster | cluster-a0 through cluster-a42 | 8080-8092 | 8443-8455 |
Example Access:
# Access alice (4-line topology)
curl http://localhost:8080/api/v1/system/status
curl -k https://localhost:8443/api/v1/system/status
# Access bob (4-line topology)
curl http://localhost:8081/api/v1/system/status
Containers also communicate internally via the Docker network using their hostnames (e.g., http://alice, https://bob).
Container Hostname Resolution
Containers use their service name or container_name as their hostname within the Docker network:
# From inside container 'alice', reach 'bob' via:
curl http://bob/api/status # Container name
curl https://bob/api/status # HTTPS with self-signed cert
Tor Network
eIOU containers automatically configure Tor hidden services:
- Hidden service directory:
/var/lib/tor/hidden_service/ - Hidden service port: Maps external port 80 to internal nginx
- Tor SOCKS proxy: Available at
127.0.0.1:9050inside the container
Resource Limits
All eIOU containers are configured with resource limits to prevent runaway resource consumption and ensure predictable performance.
Default Resource Configuration
deploy:
resources:
limits:
cpus: '2.0'
memory: 1024M
pids: 200
reservations:
memory: 512M
Resource Parameters
| Parameter | Value | Description |
|---|---|---|
limits.cpus |
2.0 |
Maximum CPU cores the container can use |
limits.memory |
1024M |
Maximum memory the container can use |
limits.pids |
200 |
Maximum number of processes/threads inside the container |
reservations.memory |
512M |
Guaranteed minimum memory allocation |
Memory Requirements by Topology
| Topology | Nodes | Per-Node Limit | Total Reserved | Total Limit |
|---|---|---|---|---|
| Single | 1 | 1024M | 512M | 1GB |
| 4-line | 4 | 1024M each | 2GB | 4GB |
| 10-line | 10 | 1024M each | 5GB | 10GB |
| Cluster | 13 | 1024M each | 6.5GB | 13GB |
Customizing Resource Limits
Override resource limits for specific environments:
services:
alice:
deploy:
resources:
limits:
cpus: '2.0' # Allow 2 CPU cores
memory: 1G # Allow 1GB memory
reservations:
memory: 512M # Reserve 512MB minimum
Note: Resource limits require Docker Compose v2 or Docker Swarm mode. In Docker Compose v1, the deploy section is ignored when not using docker stack deploy.
SSL Certificate Configuration
Certificate Priority
The container selects SSL certificates in this order:
- External certificates (
/ssl-certs/server.crt) - Mounted externally-obtained certs - Let’s Encrypt (automatic) - When
LETSENCRYPT_EMAILis set with a valid FQDN - CA-signed (
/ssl-ca/ca.crt) - Self-generated, signed by mounted CA - Self-signed - Generated automatically using SSL_DOMAIN or EIOU_HOST
Note on P2P verification: The certificate priority above determines what certificate the node presents to incoming connections. Separately,
P2P_SSL_VERIFYcontrols whether the node verifies certificates on outgoing connections to other nodes. With the default self-signed setup (option 4), other nodes will reject the certificate unlessP2P_SSL_VERIFY=falseis set or a shared CA is configured viaP2P_CA_CERT. Options 1-3 use trusted certificates that pass verification automatically.
Reverse Proxy or Cloudflare Tunnel
Instead of managing SSL inside the container, you can terminate SSL externally using a reverse proxy or a Cloudflare Tunnel. In both cases the container keeps its self-signed certificate (or plain HTTP internally) — the trusted SSL is handled by the external layer.
Reverse Proxy (nginx, Caddy, Traefik, etc.):
The proxy sits in front of the eIOU container, terminates SSL with a trusted certificate, and forwards traffic to the container’s HTTP port. This is the standard approach when you already run a web server or ingress controller on the host.
Internet → Reverse Proxy (trusted SSL) → eIOU container (HTTP or self-signed)
# Example: expose only HTTP internally, proxy handles SSL
services:
eiou:
ports:
- "127.0.0.1:8080:80" # Only reachable from localhost
environment:
- EIOU_HOST=eiou
- EIOU_HOST=node.example.com
- EIOU_PORT=443
Your reverse proxy (e.g., Caddy with automatic HTTPS, or nginx with a Let’s Encrypt certificate) listens on port 443 and proxies to 127.0.0.1:8080.
Cloudflare Tunnel (cloudflared):
A Cloudflare Tunnel creates an outbound-only connection from your server to Cloudflare’s edge — no inbound ports need to be opened. Cloudflare handles SSL termination and proxies requests to your container. This is useful when you cannot open ports (e.g., behind CGNAT or a firewall).
Internet → Cloudflare Edge (trusted SSL) ← cloudflared tunnel ← eIOU container
Install cloudflared on the host, create a tunnel via the Cloudflare dashboard, and point it to your container’s local HTTP address (e.g., http://localhost:8080).
Note: When using a reverse proxy or tunnel, set
EIOU_HOSTto the public domain (e.g.,node.example.com) so the node advertises the correct externally reachable address to contacts.
Let’s Encrypt (Automatic — Recommended for Direct Exposure)
Let’s Encrypt provides free, browser-trusted SSL certificates. Use this when the container is directly exposed to the internet (no reverse proxy). Two approaches are supported:
Single Node (In-Container Certbot)
For a single node with port 80 reachable from the internet:
services:
eiou:
ports:
- "80:80"
- "443:443"
environment:
- EIOU_HOST=node.example.com
- LETSENCRYPT_EMAIL=admin@example.com
# - LETSENCRYPT_STAGING=true # Uncomment to test first (avoids rate limits)
volumes:
- ssl-cert:/var/lib/eiou/ssl # Persist certs across restarts
volumes:
ssl-cert:
The container will:
- Request a certificate from Let’s Encrypt on first boot
- Install a daily cron job for automatic renewal
- Fall back to self-signed if the request fails
Requirements:
LETSENCRYPT_EMAILmust be set (also receives expiry warnings)- Domain must be a real FQDN (not IP, localhost, or container name)
- Port 80 must be reachable from the public internet for the ACME HTTP-01 challenge
- DNS must resolve the domain to the server’s public IP
| Variable | Default | Description |
|---|---|---|
LETSENCRYPT_EMAIL |
(none) | Email for Let’s Encrypt registration — enables LE when set |
LETSENCRYPT_DOMAIN |
$SSL_DOMAIN |
Domain for the certificate (falls back to SSL_DOMAIN → EIOU_HOST) |
LETSENCRYPT_STAGING |
false |
Use staging server for testing (certs won’t be browser-trusted) |
Multiple Nodes, Same Domain, Different Ports (Recommended)
SSL certificates validate the domain name, not the port. A single standard certificate for wallet.example.com is valid on every port — https://wallet.example.com:1153, https://wallet.example.com:1154, etc. This means 2–150+ nodes on one server can all share one regular (non-wildcard) certificate.
Step 1: Install certbot on the host server:
sudo apt install certbot
Step 2: Get a single cert (run once on the host):
# Option A: HTTP-01 (if port 80 is open on the server)
./scripts/create-ssl-letsencrypt.sh \
-d wallet.example.com \
-e admin@example.com
# Option B: DNS-01 (no port 80 needed — uses DNS provider API)
./scripts/create-ssl-letsencrypt.sh \
-d wallet.example.com \
-e admin@example.com \
--dns-plugin cloudflare \
--credentials ./cloudflare.ini
Step 3: Mount the cert in all containers:
services:
node-1:
ports: ["1153:443"]
environment:
- EIOU_HOST=wallet.example.com
- EIOU_PORT=1153
volumes:
- ./letsencrypt-certs:/ssl-certs:ro # Shared cert
node-2:
ports: ["1154:443"]
environment:
- EIOU_HOST=wallet.example.com
- EIOU_PORT=1154
volumes:
- ./letsencrypt-certs:/ssl-certs:ro # Same cert
# ... repeat for all nodes (only port number changes)
Every container receives the same certificate file. Each node’s nginx listens on 443 internally; Docker maps that to the unique external port. Only one DNS A record is needed — wallet.example.com → <server IP>.
Step 4: Set up automatic renewal (host crontab):
Let’s Encrypt certificates expire after 90 days. The renewal script checks whether the certificate is due for renewal (within 30 days of expiry) and only contacts Let’s Encrypt when needed — so running it daily is safe and won’t hit rate limits.
Running the script manually is a one-time check:
# One-time manual check (does NOT set up automatic renewal)
./scripts/renew-ssl-letsencrypt.sh -d wallet.example.com -o ./letsencrypt-certs
To automate renewal, add a cron job on the host server. Open the root crontab editor:
sudo crontab -e
This opens a text editor (usually nano or vi) showing the root user’s scheduled tasks. Add the following line at the end of the file, then save and exit:
0 3 * * * /path/to/scripts/renew-ssl-letsencrypt.sh \
-d wallet.example.com -o /path/to/letsencrypt-certs \
--restart "eiou-*" --graceful >> /var/log/eiou-ssl-renew.log 2>&1
Replace /path/to/ with the actual absolute paths on your server (e.g., /root/eiou-docker/scripts/...).
Flag reference:
| Flag | Required | Description |
|---|---|---|
-d wallet.example.com |
Yes | The domain name of the certificate to renew |
-o /path/to/letsencrypt-certs |
Yes | Directory where the renewed cert files are copied (the same directory mounted as /ssl-certs in your containers) |
--restart "eiou-*" |
No | After a successful renewal, restart Docker containers whose names match this pattern (e.g., eiou-* matches eiou-node-1, eiou-node-2, etc. — use the actual naming pattern of your containers) |
--graceful |
No | Used with --restart — sends a reload signal (SIGHUP) to containers instead of fully restarting them, avoiding downtime |
The >> /var/log/eiou-ssl-renew.log 2>&1 part at the end redirects all output to a log file so you can check what happened later.
Most days the cron job will do nothing. Certbot only renews when the certificate is within 30 days of expiry. When it does renew, the script copies the new files into the output directory and optionally reloads containers.
Multiple Nodes, Different Subdomains (Wildcard)
If each node needs its own subdomain (e.g., alice.example.com:1154, bob.example.com:1155), use a wildcard certificate. A wildcard cert for *.example.com covers any single subdomain.
Step 1: Get the wildcard cert (run once on the host):
# Wildcard certs require DNS-01 (no HTTP-01 support)
sudo apt install certbot python3-certbot-dns-cloudflare
echo "dns_cloudflare_api_token = YOUR_TOKEN" > cloudflare.ini
chmod 600 cloudflare.ini
./scripts/create-ssl-letsencrypt.sh \
-d example.com \
-e admin@example.com \
--wildcard \
--dns-plugin cloudflare \
--credentials ./cloudflare.ini
Step 2: Mount the cert in all containers:
services:
alice:
ports: ["1154:443"]
environment:
- EIOU_HOST=alice.example.com
- EIOU_PORT=1154
volumes:
- ./letsencrypt-certs:/ssl-certs:ro
bob:
ports: ["1155:443"]
environment:
- EIOU_HOST=bob.example.com
- EIOU_PORT=1155
volumes:
- ./letsencrypt-certs:/ssl-certs:ro
# ... each node gets a unique subdomain + port
Each subdomain needs a DNS A record pointing to the server IP (or use a wildcard DNS record: *.example.com → <server IP>).
Which approach to choose
| Setup | Cert Type | DNS Records | Best For |
|---|---|---|---|
wallet.example.com:1153, :1154, … |
1 standard cert | 1 A record | Simplest — all nodes share one domain |
alice.example.com, bob.example.com, … |
1 wildcard cert | 1 per subdomain (or wildcard DNS) | Each node has its own identity |
DNS-01 vs HTTP-01:
| Challenge | Port Required | Wildcard Support | Best For |
|---|---|---|---|
| HTTP-01 | Port 80 | No | Single domain, port 80 available |
| DNS-01 | None | Yes | Wildcard certs, no port 80 needed |
DNS-01 uses your DNS provider’s API to validate domain ownership — no port access needed. Supported providers include Cloudflare, Route53, DigitalOcean, Google Cloud DNS, and many more.
External Certificates (Manual)
Mount externally-obtained certificates from any source:
services:
eiou:
volumes:
- /path/to/certs:/ssl-certs:ro
Required files in the mounted directory:
server.crt- SSL certificate (PEM format)server.key- Private key (PEM format)ca-chain.crt(optional) - CA certificate chain
Local CA (Development/Testing)
# Generate CA once
./scripts/create-ssl-ca.sh ./ssl-ca
Then install ca.crt in your browser or operating system trust store so that certificates signed by this CA are recognized as trusted:
Chrome / Edge (Windows):
- Open
chrome://settings/security(oredge://settings/privacy) - Click Manage certificates (opens Windows certificate manager)
- Go to the Trusted Root Certification Authorities tab
- Click Import… → select
ssl-ca/ca.crt - Place in Trusted Root Certification Authorities → Finish
- Restart Chrome
Chrome / Edge (macOS):
- Double-click
ssl-ca/ca.crt— this opens Keychain Access - Add to the System keychain
- Find eIOU Root CA in the list, double-click it
- Expand Trust → set When using this certificate to Always Trust
- Close and enter your password to confirm
Chrome (Linux):
- Open
chrome://settings/certificates - Go to the Authorities tab
- Click Import → select
ssl-ca/ca.crt - Check Trust this certificate for identifying websites → OK
Firefox (all platforms):
Firefox uses its own certificate store, separate from the OS.
- Open
about:preferences#privacy - Scroll to Certificates → click View Certificates…
- Go to the Authorities tab
- Click Import… → select
ssl-ca/ca.crt - Check Trust this CA to identify websites → OK
Linux system-wide (for curl, wget, etc.):
sudo cp ssl-ca/ca.crt /usr/local/share/ca-certificates/eiou-ca.crt
sudo update-ca-certificates
After installing the CA, mount it in docker-compose so containers generate CA-signed certificates:
services:
eiou:
environment:
- EIOU_HOST=alice
volumes:
- ./ssl-ca:/ssl-ca:ro
Wallet Restoration
Security Recommendations
- Prefer RESTORE_FILE over RESTORE - File-based restoration is more secure
- Use read-only mounts - Mount seed files as
:ro - Delete seed files after restoration - Don’t leave them mounted permanently
- Use secrets management - In production, consider Docker secrets or vault
Restoration Process
-
Create seed file:
echo "word1 word2 ... word24" > /secure/location/seed.txt chmod 600 /secure/location/seed.txt -
Start container with restoration:
environment: - RESTORE_FILE=/restore/seed volumes: - /secure/location/seed.txt:/restore/seed:ro -
After successful restoration, remove the mount and restart
Timeout Configuration
Default Timeouts
| Variable | Default | Purpose |
|---|---|---|
EIOU_HS_TIMEOUT |
60s | Wait for Tor hidden service to become available |
EIOU_TOR_TIMEOUT |
120s | Wait for Tor network connectivity |
EIOU_INIT_TIMEOUT |
120s | Container initialization timeout (test runner) |
WSL2 / Slow Environments
Increase timeouts for slower environments:
environment:
- EIOU_HS_TIMEOUT=120
- EIOU_TOR_TIMEOUT=240
Disabling Tor Wait
For HTTP-only testing, you can reduce Tor timeouts:
environment:
- EIOU_HS_TIMEOUT=5
- EIOU_TOR_TIMEOUT=10
Healthcheck Configuration
Docker healthchecks monitor container readiness. Default configuration:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/gui/"]
interval: 30s # Check every 30 seconds
timeout: 20s # Timeout per check
retries: 5 # Mark unhealthy after 5 failures
start_period: 120s # Grace period for container startup
stop_grace_period: 45s # Time allowed for graceful shutdown
Healthcheck Parameters
| Parameter | Default | Description |
|---|---|---|
test |
curl -f http://localhost/gui/ |
Command to verify health |
interval |
30s |
Time between health checks |
timeout |
20s |
Maximum time for health check |
retries |
5 |
Failures before unhealthy |
start_period |
120s |
Startup grace period |
stop_grace_period |
45s |
Time for graceful shutdown before SIGKILL |
Graceful Shutdown
The stop_grace_period controls how long Docker waits for the container to stop gracefully before sending SIGKILL. The 45-second default allows:
- Background processors (P2P, Transaction, Cleanup) to finish current operations
- Database connections to close cleanly
- Pending message queue items to be saved
Custom Healthcheck for Slow Environments
For WSL2 or limited resources:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/gui/"]
interval: 60s
timeout: 30s
retries: 10
start_period: 180s
Security Configuration
Container Privilege Model
eIOU containers run as root during initialization, then services drop privileges:
| Service | Runtime User | Purpose |
|---|---|---|
| nginx | www-data |
Web server (reverse proxy) |
| PHP-FPM | www-data |
PHP application processing |
| MariaDB | mysql |
Database operations |
| Tor | debian-tor |
Tor hidden service |
| PHP processors | www-data |
Background message processing (via runuser) |
Critical File Permissions
| Path | Permissions | Owner | Purpose |
|---|---|---|---|
/etc/eiou/config/ |
755 (dir) / 644 (files) | www-data | Configuration |
/app/eiou/ |
755 (dir) / 644 (files) | www-data | Source code |
/var/lib/mysql/ |
700 | mysql | Database files |
/var/lib/tor/hidden_service/ |
700 | debian-tor | Tor keys |
/etc/nginx/ssl/server.key |
600 | root | SSL private key |
Container Security Hardening
The reference compose files include security hardening directives:
security_opt:
- no-new-privileges:true # Prevent privilege escalation
pids_limit: 200 # Limit process count per container
For docker run users, add equivalent flags:
docker run --security-opt no-new-privileges:true --pids-limit 200 ...
Log Rotation
Docker stdout/stderr log rotation is configured in the reference compose files:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
For docker run users or daemon-level configuration, add to /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Application logs (nginx, PHP) inside the container are rotated by logrotate (weekly, 4 rotations, compressed).
Base Image Pinning
The base image in eiou.dockerfile is pinned to a SHA256 digest to ensure reproducible builds and prevent supply chain attacks from upstream tag republishing.
To check whether the pinned digest is current, run ./scripts/check-base-image.sh. For full verification and update instructions, see SECURITY.md.
Secure Seed Phrase Handling
Best Practices:
- Always use
RESTORE_FILEinstead ofRESTOREenvironment variable - Mount seed files as read-only (
:ro) - Delete seed files after successful restoration
- Never commit seed phrases to version control
Backup and Restore
Automated Backup System
eIOU includes an automated backup system that creates encrypted database backups daily at midnight.
Configuration:
environment:
- EIOU_BACKUP_AUTO_ENABLED=true # Enable automatic backups (default)
- EIOU_BACKUP_AUTO_ENABLED=false # Disable automatic backups
Backup Details:
- Backups run automatically at midnight (container local time)
- Stored in
/var/lib/eiou/backups/(persisted via{node}-backupsvolume) - Encrypted with AES-256-GCM using the master key (derived from the seed phrase)
- Only the 3 most recent backups are retained by default
- Backup files are named with timestamps:
backup_YYYYMMDD_HHmmss.eiou.enc
CLI Backup Commands
The eiou CLI provides commands for manual backup and restore operations:
# Create a manual backup
docker exec <container> eiou backup create
# List available backups
docker exec <container> eiou backup list
# Restore by starting a new-volume candidate in pre-service boot recovery
EIOU_BOOT_RECOVERY_BACKUP=<backup-file> docker compose up
# Delete old backups (keeps most recent 3)
docker exec <container> eiou backup cleanup
Important: Restore requires the wallet seed-derived master key, a new empty
database volume, and EIOU_BOOT_RECOVERY_BACKUP=<filename> at container start.
Startup mints a root-only one-boot capability and completes validation/import
before cron, PHP-FPM, nginx, or background processors start. An ordinary CLI,
GUI, or API process cannot authorize import.
Critical Data
Must backup:
{node}-mysql-data- Contains all transaction history and contact relationships{node}-config- Contains wallet private keys and encryption keys{node}-backups- Contains encrypted database backups{node}-plugins- Contains the plugin binaries matching plugin database tables{node}-plugin-scratch- Contains durable plugin-owned state{node}-backup-locks- Contains the durable pending archive-snapshot obligation{node}-ssl-cert- Preserves the node’s persisted TLS identity
Manual Volume Backup
For complete disaster recovery, back up Docker volumes only while the node is stopped. A live copy or restore bypasses MariaDB and the backup lifecycle locks.
docker stop <container>
docker run --rm --volumes-from <container> -v "$PWD":/backup alpine sh -ec '
tar czf /backup/mysql-data.tar.gz -C /var/lib/mysql .
tar czf /backup/config.tar.gz -C /etc/eiou/config .
tar czf /backup/plugins.tar.gz -C /etc/eiou/plugins .
tar czf /backup/plugin-scratch.tar.gz -C /var/lib/eiou/plugin-scratch .
tar czf /backup/backups.tar.gz -C /var/lib/eiou/backups .
tar czf /backup/ssl-cert.tar.gz -C /var/lib/eiou/ssl .
mkdir /empty
if [ -f /var/lib/eiou/backup-locks/archive-backup-pending ]; then
tar czf /backup/backup-locks.tar.gz -C /var/lib/eiou/backup-locks archive-backup-pending
else
tar czf /backup/backup-locks.tar.gz -C /empty .
fi
'
Restore Commands
# Keep <container> stopped and its original volumes untouched for rollback.
# Restore only into newly-created, empty volumes.
docker volume create <restore-mysql-volume>
docker volume create <restore-config-volume>
docker volume create <restore-backups-volume>
docker volume create <restore-plugins-volume>
docker volume create <restore-plugin-scratch-volume>
docker volume create <restore-backup-locks-volume>
docker volume create <restore-ssl-cert-volume>
docker run --rm -v <restore-mysql-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/mysql-data.tar.gz -C /target'
docker run --rm -v <restore-config-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/config.tar.gz -C /target'
docker run --rm -v <restore-backups-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/backups.tar.gz -C /target'
docker run --rm -v <restore-plugins-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/plugins.tar.gz -C /target'
docker run --rm -v <restore-plugin-scratch-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/plugin-scratch.tar.gz -C /target'
docker run --rm -v <restore-backup-locks-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/backup-locks.tar.gz -C /target; rm -f /target/lifecycle.lock'
docker run --rm -v <restore-ssl-cert-volume>:/target -v "$PWD":/restore:ro alpine sh -ec \
'test -z "$(find /target -mindepth 1 -maxdepth 1 -print -quit)"; tar xzf /restore/ssl-cert.tar.gz -C /target'
Create a temporary candidate from the same image and environment as the stopped
node, mounting all seven new volumes. Keep the original stopped and never run both
copies of the wallet simultaneously. Validate startup logs, wallet access,
MariaDB health, and eiou verify-chain. After validation, recreate the original
container with the new volumes or promote the candidate. If validation fails,
delete the candidate and new volumes and restart the original container with its
untouched volumes. Startup repairs ownership and recreates lifecycle.lock; only
the durable archive pending marker is restored from the coordination archive.
Restore from Encrypted Backup
To restore from an encrypted backup file:
-
Restore wallet first (required for decryption):
environment: - RESTORE_FILE=/restore/seed volumes: - /path/to/seed.txt:/restore/seed:ro -
Create an isolated candidate container with new empty database and coordination volumes. Restore the matching config, plugins, plugin-scratch, backup, and SSL volumes from the same recovery point. Mixing plugin database tables with different plugin binaries/state is unsupported. Set the exact selected backup filename:
environment: - EIOU_BOOT_RECOVERY_BACKUP=backup_20260115_000000.eiou.enc -
Start and validate that candidate:
docker compose up -d docker logs <candidate> docker exec <candidate> eiou verify-chainKeep the serving node stopped throughout. Promote the candidate volumes only after validation; discard them on failure and restart the untouched original. Remove
EIOU_BOOT_RECOVERY_BACKUPafter the successful recovery boot so a later ordinary restart does not attempt recovery again.
Backups created before the v0.1.18-alpha v3 format cutoff are rejected. Restore them with the earlier image that created them and then create a v3 snapshot.
Complete Reset
Remove all data and start fresh:
docker compose down -v
Troubleshooting
Container Startup Issues
Container Exits Immediately
Symptoms: Container starts but exits with code 1
Solutions:
# Check container logs
docker logs <container_name>
# Verify volume permissions
docker exec <container_name> ls -la /etc/eiou/config
# Reset and rebuild
docker-compose -f <config>.yml down -v
docker-compose -f <config>.yml up -d --build
Container Stuck at “Waiting for MariaDB”
Cause: MariaDB taking too long to initialize (common on first startup)
Solutions:
- Wait longer (up to 2 minutes on first run)
- Check MariaDB logs:
docker logs <container> 2>&1 | grep -i maria - Ensure sufficient memory (minimum 275MB per container)
MariaDB Version Mismatch After Image Rebuild
Symptoms: MariaDB error log shows Reading log encryption info failed; the log was created with MariaDB X.Y.Z followed by InnoDB: Plugin registration as a STORAGE ENGINE failed.
Cause: Rebuilding the Docker image pulled a newer MariaDB patch version from Debian repos. The InnoDB redo logs on the persistent volume use the old version’s encryption metadata format, which the new binary cannot parse.
Solution: Starting with v0.1.8-alpha, this is handled automatically by startup.sh. The startup script detects version mismatches, uses innodb_force_recovery=1 to regenerate redo logs, and runs mariadb-upgrade. No manual action needed. See the Upgrade Guide for details.
Missing InnoDB Redo Log After Broken Container
Symptoms: MariaDB error log shows InnoDB: File ./ib_logfile0 was not found followed by Plugin 'InnoDB' registration as a STORAGE ENGINE failed. Even innodb_force_recovery fails with the same error.
Cause: The prior container crashed during initialization, had a partially restored volume, or never completed MariaDB setup. The persistent volume has ibdata1 (InnoDB data) but is missing ib_logfile0 (InnoDB redo log). MariaDB requires this file to exist before the InnoDB plugin can initialize — force-recovery cannot help if the file is absent entirely.
Solution: Starting with v0.1.8-alpha, this is handled automatically by startup.sh. The script detects missing ib_logfile0 when ibdata1 exists, moves broken data to /tmp/mysql-broken-<timestamp>/, reinitializes MariaDB with mysql_install_db, recreates the database and tables from the config volume, enables TDE encryption, and auto-restores from the latest backup. Wallet identity (keys, .onion address) is preserved — userconfig.json on the config volume is never modified. Recovery runs before serving/background processes start and fails the boot closed if no valid backup can be restored.
TDE Encryption Config Lost After Container Rebuild
Symptoms: MariaDB error log shows Obtaining redo log encryption key version 1 failed (4294967295). Maybe the key or the required encryption key management plugin was not found followed by Reading log encryption info failed.
Cause: encryption.cnf lives in the container filesystem (/etc/mysql/conf.d/), not on a volume. When the container is rebuilt, this file is lost, but the mysql-data volume still has TDE-encrypted redo logs and tablespace files. Without the encryption plugin config, MariaDB cannot decrypt its own data.
Solution: Starting with v0.1.8-alpha, this is handled automatically by startup.sh. The pre-MariaDB TDE key setup detects when the master key is available and database files exist on the volume but encryption.cnf is missing. It recreates the encryption config and TDE key file before MariaDB starts. No manual action needed. See the Upgrade Guide for details.
Tor Connectivity Issues
Hidden Service Not Ready
Symptoms: “Hidden service hostname file not ready” warning
Solution: Increase timeout:
environment:
- EIOU_HS_TIMEOUT=120 # Increase from default 60s
Tor Connection Timeout
Symptoms: “Tor connection could not be verified” warning
Solution:
environment:
- EIOU_TOR_TIMEOUT=240 # Increase from default 120s
SSL Certificate Issues
Browser Shows Certificate Warning
Cause: Self-signed certificate not trusted by browser
Solutions:
- Use Let’s Encrypt for automatic trusted certificates (see SSL Certificate Configuration)
- Use
./scripts/create-ssl-ca.shto create local CA for development - Add exception in browser for quick testing
P2P HTTPS Fails Between Docker Nodes (Self-Signed Certificates)
Cause: SSL peer verification is enabled by default. Docker nodes using auto-generated certs self-signed certificates that are not trusted by other nodes.
Error: HTTP request failed: SSL certificate problem: self-signed certificate
Solutions:
- Disable verification for development/testing (easiest):
environment:
- P2P_SSL_VERIFY=false
- Use a shared CA certificate (recommended for production):
# Generate a local CA
./scripts/create-ssl-ca.sh
environment:
- P2P_CA_CERT=/ssl-ca/ca.crt
volumes:
- ./ssl-ca:/ssl-ca:ro
- Use Let’s Encrypt for real trusted certificates (see SSL Certificate Configuration)
Note: Auto-generated self-signed certificates will always be rejected by other nodes unless
P2P_SSL_VERIFY=falseis set or a shared CA is configured. This is intentional — P2P SSL verification is enabled by default for security.
Let’s Encrypt Certificate Request Failed
Cause: ACME challenge could not be completed
Solutions:
- Ensure port 80 is reachable from the internet (for HTTP-01 challenge)
- Verify the domain resolves to the server’s public IP:
dig +short yourdomain.com - Test with staging first:
LETSENCRYPT_STAGING=true(avoids rate limits) - Check certbot logs:
docker exec <container> cat /var/log/letsencrypt/letsencrypt.log - For multi-node setups, use the host-level wildcard approach instead (see docs)
Performance Issues (WSL2)
Cause: WSL2 has slower I/O and network performance
Solutions:
environment:
- EIOU_HS_TIMEOUT=120 # Double default
- EIOU_TOR_TIMEOUT=240 # Double default
For tests:
EIOU_INIT_TIMEOUT=180 ./run-all-tests.sh http4
Slow GUI or API Under Load
Cause: PHP-FPM’s default pool size (pm.max_children = 5) limits how many PHP requests can execute simultaneously. If more than 5 requests arrive at the same time, the extras queue until a worker frees up.
This is separate from P2P worker processes — outbound P2P messages are handled by independent PHP CLI processes (up to 50 for HTTP, 50 for HTTPS, 5 for Tor), controlled by the EIOU_P2P_MAX_WORKERS setting. Inbound P2P requests from other nodes do go through PHP-FPM.
How the request pipeline works:
| Layer | Process | Concurrency | Scaling |
|---|---|---|---|
| nginx workers | Accept connections, rate limit, route | 2 workers handle thousands of connections each (event-driven) | NGINX_WORKER_PROCESSES (default: 2) |
| PHP-FPM workers | Execute PHP for GUI, API, and inbound P2P | PHP_FPM_MAX_CHILDREN (default: 5) |
Increase if GUI/API feels slow under load |
| P2P outbound workers | Send messages to other nodes | EIOU_P2P_MAX_WORKERS (default: 50 HTTP, 50 HTTPS, 5 Tor) |
Independent of PHP-FPM |
Solution: Add environment variables to docker-compose.yml (persists across restarts):
environment:
- PHP_FPM_MAX_CHILDREN=10 # Increase from default 5
- PHP_FPM_PM=dynamic # Keep spare workers warm for faster response
- PHP_FPM_START_SERVERS=3
- PHP_FPM_MIN_SPARE=2
- PHP_FPM_MAX_SPARE=5
Each PHP-FPM worker uses ~20-30MB of memory. With the default 1024MB container memory limit, 15-20 workers is a safe maximum. If you increase beyond that, also increase the memory limit:
deploy:
resources:
limits:
memory: 1536M # Increase from 1024M for more PHP-FPM workers
See .env.example for the full list of tunable PHP-FPM and Nginx variables.
Common Log Locations
| Log | Location | Purpose |
|---|---|---|
| Application | /var/log/eiou/app.log |
eIOU application log (always active) |
| Analytics | /var/log/eiou/analytics.log |
Daily analytics cron output (3 AM UTC) |
| Backup | /var/log/eiou/backup.log |
Daily backup cron output (midnight UTC) |
| Update check | /var/log/eiou/update-check.log |
Daily update check cron output (2 AM UTC) |
| nginx access | /var/log/nginx/access.log |
HTTP requests |
| nginx error | /var/log/nginx/error.log |
Web server errors |
| PHP errors | /var/log/php_errors.log |
PHP runtime errors |
| Tor | /var/log/tor/log |
Tor network status |
# View logs inside container
docker exec <container> tail -f /var/log/eiou/app.log
docker exec <container> cat /var/log/eiou/analytics.log
docker exec <container> cat /var/log/eiou/backup.log
docker exec <container> tail -f /var/log/nginx/error.log
Expected Warnings (Safe to Ignore)
These warnings appear during normal operation and do not indicate a problem:
| Warning | When | Why |
|---|---|---|
dbconfig.json encryption migration deferred — will complete during wallet setup |
First boot only | The Application constructor attempts to encrypt the database password before the wallet (master key) exists. This is harmless — wallet generation encrypts the password immediately after initializing the master key. Look for the follow-up message dbconfig.json: plaintext password encrypted successfully confirming completion. |
Contact status polling disabled, stopping processor |
When EIOU_CONTACT_STATUS_ENABLED=false |
Normal — the contact status processor exits cleanly when disabled. |
Hidden service hostname file not ready |
Boot (Tor mode) | Tor is still building circuits. Wait for EIOU_HS_TIMEOUT (default 60s) or increase if on slow networks. |
See Also
- CLI Reference - Command-line interface documentation
- API Reference - REST API documentation
- Error Codes - Complete error code reference
- docker-compose.yml - Reference template with inline documentation