Reference

Docker Configuration

Docker Configuration Reference

Complete reference for environment variables and volume mounts used in eIOU Docker containers.

Table of Contents

  1. Environment Variables
  2. Volume Mounts
  3. Network Configuration
  4. Resource Limits
  5. SSL Certificate Configuration
  6. Wallet Restoration
  7. Timeout Configuration
  8. Healthcheck Configuration
  9. Security Configuration
  10. Backup and Restore
  11. 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 alice resolves 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, set EIOU_HOST to a real IP address or domain name, EIOU_PORT to 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=true only 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 set P2P_SSL_VERIFY=false in all nodes (dev/testing only), use a shared CA via P2P_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 RESTORE environment variable, the container will display a warning recommending RESTORE_FILE instead. The RESTORE variable 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 both RESTORE (or RESTORE_FILE) and EIOU_HOST are set, the wallet is first restored from the seed phrase (restoring keys and Tor address), and then the EIOU_HOST hostname 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 names
  • IP: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 in and eiou out for 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 balances table) with balance calculated from existing transactions; if they match, net_missing = 0 and 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 false if 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 true during the alpha/development phase
  • Set to false in 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 certificate
  • ca.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:9050 inside 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:

  1. External certificates (/ssl-certs/server.crt) - Mounted externally-obtained certs
  2. Let’s Encrypt (automatic) - When LETSENCRYPT_EMAIL is set with a valid FQDN
  3. CA-signed (/ssl-ca/ca.crt) - Self-generated, signed by mounted CA
  4. 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_VERIFY controls 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 unless P2P_SSL_VERIFY=false is set or a shared CA is configured via P2P_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_HOST to the public domain (e.g., node.example.com) so the node advertises the correct externally reachable address to contacts.

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:

  1. Request a certificate from Let’s Encrypt on first boot
  2. Install a daily cron job for automatic renewal
  3. Fall back to self-signed if the request fails

Requirements:

  • LETSENCRYPT_EMAIL must 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)

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):

  1. Open chrome://settings/security (or edge://settings/privacy)
  2. Click Manage certificates (opens Windows certificate manager)
  3. Go to the Trusted Root Certification Authorities tab
  4. Click Import… → select ssl-ca/ca.crt
  5. Place in Trusted Root Certification Authorities → Finish
  6. Restart Chrome

Chrome / Edge (macOS):

  1. Double-click ssl-ca/ca.crt — this opens Keychain Access
  2. Add to the System keychain
  3. Find eIOU Root CA in the list, double-click it
  4. Expand Trust → set When using this certificate to Always Trust
  5. Close and enter your password to confirm

Chrome (Linux):

  1. Open chrome://settings/certificates
  2. Go to the Authorities tab
  3. Click Import → select ssl-ca/ca.crt
  4. Check Trust this certificate for identifying websites → OK

Firefox (all platforms):

Firefox uses its own certificate store, separate from the OS.

  1. Open about:preferences#privacy
  2. Scroll to Certificates → click View Certificates…
  3. Go to the Authorities tab
  4. Click Import… → select ssl-ca/ca.crt
  5. 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

  1. Prefer RESTORE_FILE over RESTORE - File-based restoration is more secure
  2. Use read-only mounts - Mount seed files as :ro
  3. Delete seed files after restoration - Don’t leave them mounted permanently
  4. Use secrets management - In production, consider Docker secrets or vault

Restoration Process

  1. Create seed file:

    echo "word1 word2 ... word24" > /secure/location/seed.txt
    chmod 600 /secure/location/seed.txt
  2. Start container with restoration:

    environment:
      - RESTORE_FILE=/restore/seed
    volumes:
      - /secure/location/seed.txt:/restore/seed:ro
  3. 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:

  1. Always use RESTORE_FILE instead of RESTORE environment variable
  2. Mount seed files as read-only (:ro)
  3. Delete seed files after successful restoration
  4. 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}-backups volume)
  • 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:

  1. Restore wallet first (required for decryption):

    environment:
      - RESTORE_FILE=/restore/seed
    volumes:
      - /path/to/seed.txt:/restore/seed:ro
  2. 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
  3. Start and validate that candidate:

    docker compose up -d
    docker logs <candidate>
    docker exec <candidate> eiou verify-chain

    Keep 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_BACKUP after 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:

  1. Use Let’s Encrypt for automatic trusted certificates (see SSL Certificate Configuration)
  2. Use ./scripts/create-ssl-ca.sh to create local CA for development
  3. 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:

  1. Disable verification for development/testing (easiest):
environment:
  - P2P_SSL_VERIFY=false
  1. 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
  1. 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=false is 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