Compare commits
11 Commits
logo-propo
...
v1.13.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009f3b1403 | ||
|
|
51f74eab42 | ||
|
|
4acf797128 | ||
|
|
d167da9d63 | ||
|
|
a5eac339db | ||
|
|
9f3813eb72 | ||
|
|
b9ae0ad4d5 | ||
|
|
ca2a4dd6d9 | ||
|
|
fafdce5736 | ||
|
|
6436becff9 | ||
|
|
3460d8a3ea |
@@ -59,18 +59,20 @@ Check:
|
||||
- Config file search order is accurate
|
||||
- Example YAML would actually work
|
||||
|
||||
### 4. Verify docs/architecture.md
|
||||
### 4. Verify docs/architecture.md and CLAUDE.md
|
||||
|
||||
```bash
|
||||
# What source files actually exist?
|
||||
git ls-files "src/**/*.py"
|
||||
```
|
||||
|
||||
Check:
|
||||
Check **both** `docs/architecture.md` and `CLAUDE.md` (Architecture section):
|
||||
- Listed files exist
|
||||
- No files are missing from the list
|
||||
- Descriptions match what the code does
|
||||
|
||||
Both files have architecture listings that can drift independently.
|
||||
|
||||
### 5. Check Examples
|
||||
|
||||
For examples in any doc:
|
||||
|
||||
10
CLAUDE.md
@@ -20,15 +20,17 @@ src/compose_farm/
|
||||
│ ├── monitoring.py # logs, ps, stats commands
|
||||
│ ├── ssh.py # SSH key management (setup, status, keygen)
|
||||
│ └── web.py # Web UI server command
|
||||
├── config.py # Pydantic models, YAML loading
|
||||
├── compose.py # Compose file parsing (.env, ports, volumes, networks)
|
||||
├── config.py # Pydantic models, YAML loading
|
||||
├── console.py # Shared Rich console instances
|
||||
├── executor.py # SSH/local command execution, streaming output
|
||||
├── operations.py # Business logic (up, migrate, discover, preflight checks)
|
||||
├── state.py # Deployment state tracking (which stack on which host)
|
||||
├── glances.py # Glances API integration for host resource stats
|
||||
├── logs.py # Image digest snapshots (dockerfarm-log.toml)
|
||||
├── operations.py # Business logic (up, migrate, discover, preflight checks)
|
||||
├── paths.py # Path utilities, config file discovery
|
||||
├── registry.py # Container registry client for update checking
|
||||
├── ssh_keys.py # SSH key path constants and utilities
|
||||
├── state.py # Deployment state tracking (which stack on which host)
|
||||
├── traefik.py # Traefik file-provider config generation from labels
|
||||
└── web/ # Web UI (FastAPI + HTMX)
|
||||
```
|
||||
@@ -137,7 +139,7 @@ CLI available as `cf` or `compose-farm`.
|
||||
| `down` | Stop stacks (`docker compose down`). Use `--orphaned` to stop stacks removed from config |
|
||||
| `stop` | Stop services without removing containers (`docker compose stop`) |
|
||||
| `pull` | Pull latest images |
|
||||
| `restart` | `down` + `up -d` |
|
||||
| `restart` | Restart running containers (`docker compose restart`) |
|
||||
| `update` | Pull, build, recreate only if changed (`up -d --pull always --build`) |
|
||||
| `apply` | Make reality match config: migrate stacks + stop orphans. Use `--dry-run` to preview |
|
||||
| `compose` | Run any docker compose command on a stack (passthrough) |
|
||||
|
||||
70
README.md
@@ -51,6 +51,9 @@ A minimal CLI tool to run Docker Compose commands across multiple hosts via SSH.
|
||||
- [Multi-Host Stacks](#multi-host-stacks)
|
||||
- [Config Command](#config-command)
|
||||
- [Usage](#usage)
|
||||
- [Docker Compose Commands](#docker-compose-commands)
|
||||
- [Compose Farm Commands](#compose-farm-commands)
|
||||
- [Aliases](#aliases)
|
||||
- [CLI `--help` Output](#cli---help-output)
|
||||
- [Auto-Migration](#auto-migration)
|
||||
- [Traefik Multihost Ingress (File Provider)](#traefik-multihost-ingress-file-provider)
|
||||
@@ -363,24 +366,47 @@ Use `cf config init` to get started with a fully documented template.
|
||||
|
||||
The CLI is available as both `compose-farm` and the shorter `cf` alias.
|
||||
|
||||
### Docker Compose Commands
|
||||
|
||||
These wrap `docker compose` with multi-host superpowers:
|
||||
|
||||
| Command | Wraps | Compose Farm Additions |
|
||||
|---------|-------|------------------------|
|
||||
| `cf up` | `up -d` | `--all`, `--host`, parallel execution, auto-migration |
|
||||
| `cf down` | `down` | `--all`, `--host`, `--orphaned`, state tracking |
|
||||
| `cf stop` | `stop` | `--all`, `--service` |
|
||||
| `cf restart` | `restart` | `--all`, `--service` |
|
||||
| `cf pull` | `pull` | `--all`, `--service`, parallel execution |
|
||||
| `cf logs` | `logs` | `--all`, `--host`, multi-stack output |
|
||||
| `cf ps` | `ps` | `--all`, `--host`, unified cross-host view |
|
||||
| `cf compose` | any | passthrough for commands not listed above |
|
||||
|
||||
### Compose Farm Commands
|
||||
|
||||
Multi-host orchestration that Docker Compose can't do:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **`cf apply`** | **Make reality match config (start + migrate + stop orphans)** |
|
||||
| `cf up <stack>` | Start stack (auto-migrates if host changed) |
|
||||
| `cf down <stack>` | Stop and remove stack containers |
|
||||
| `cf stop <stack>` | Stop stack without removing containers |
|
||||
| `cf restart <stack>` | down + up |
|
||||
| `cf update <stack>` | Pull, build, recreate only if changed |
|
||||
| `cf pull <stack>` | Pull latest images |
|
||||
| `cf logs -f <stack>` | Follow logs |
|
||||
| `cf ps` | Show status of all stacks |
|
||||
| `cf refresh` | Update state from running stacks |
|
||||
| **`cf apply`** | **Reconcile: start missing, migrate moved, stop orphans** |
|
||||
| `cf update` | Shorthand for `up --pull --build` |
|
||||
| `cf refresh` | Sync state from what's actually running |
|
||||
| `cf check` | Validate config, mounts, networks |
|
||||
| `cf init-network` | Create Docker network on hosts |
|
||||
| `cf init-network` | Create Docker network on all hosts |
|
||||
| `cf traefik-file` | Generate Traefik file-provider config |
|
||||
| `cf config <cmd>` | Manage config files (init, show, path, validate, edit, symlink) |
|
||||
| `cf config` | Manage config files (init, show, validate, edit, symlink) |
|
||||
| `cf ssh` | Manage SSH keys (setup, status, keygen) |
|
||||
|
||||
All commands support `--all` to operate on all stacks.
|
||||
### Aliases
|
||||
|
||||
Short aliases for frequently used commands:
|
||||
|
||||
| Alias | Command | Alias | Command |
|
||||
|-------|---------|-------|---------|
|
||||
| `cf a` | `apply` | `cf s` | `stats` |
|
||||
| `cf l` | `logs` | `cf c` | `compose` |
|
||||
| `cf r` | `restart` | `cf rf` | `refresh` |
|
||||
| `cf u` | `update` | `cf ck` | `check` |
|
||||
| `cf p` | `pull` | `cf tf` | `traefik-file` |
|
||||
|
||||
Each command replaces: look up host → SSH → find compose file → run `ssh host "cd /opt/compose/plex && docker compose up -d"`.
|
||||
|
||||
@@ -400,7 +426,7 @@ cf down --orphaned # stop stacks removed from config
|
||||
# Pull latest images
|
||||
cf pull --all
|
||||
|
||||
# Restart (down + up)
|
||||
# Restart running containers
|
||||
cf restart plex
|
||||
|
||||
# Update (pull + build, only recreates containers if images changed)
|
||||
@@ -473,9 +499,9 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
│ stop Stop services without removing containers (docker compose │
|
||||
│ stop). │
|
||||
│ pull Pull latest images (docker compose pull). │
|
||||
│ restart Restart stacks (down + up). With --service, restarts just │
|
||||
│ that service. │
|
||||
│ update Update stacks. Only recreates containers if images changed. │
|
||||
│ restart Restart running containers (docker compose restart). │
|
||||
│ update Update stacks (pull + build + up). Shorthand for 'up --pull │
|
||||
│ --build'. │
|
||||
│ apply Make reality match config (start, migrate, stop │
|
||||
│ strays/orphans as needed). │
|
||||
│ compose Run any docker compose command on a stack. │
|
||||
@@ -524,6 +550,8 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
│ --all -a Run on all stacks │
|
||||
│ --host -H TEXT Filter to stacks on this host │
|
||||
│ --service -s TEXT Target a specific service within the stack │
|
||||
│ --pull Pull images before starting (--pull always) │
|
||||
│ --build Build images before starting │
|
||||
│ --config -c PATH Path to config file │
|
||||
│ --help -h Show this message and exit. │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
@@ -658,7 +686,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
|
||||
Usage: cf restart [OPTIONS] [STACKS]...
|
||||
|
||||
Restart stacks (down + up). With --service, restarts just that service.
|
||||
Restart running containers (docker compose restart).
|
||||
|
||||
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
|
||||
│ stacks [STACKS]... Stacks to operate on │
|
||||
@@ -693,7 +721,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
|
||||
|
||||
Usage: cf update [OPTIONS] [STACKS]...
|
||||
|
||||
Update stacks. Only recreates containers if images changed.
|
||||
Update stacks (pull + build + up). Shorthand for 'up --pull --build'.
|
||||
|
||||
╭─ Arguments ──────────────────────────────────────────────────────────────────╮
|
||||
│ stacks [STACKS]... Stacks to operate on │
|
||||
@@ -1281,12 +1309,12 @@ published ports.
|
||||
|
||||
**Auto-regeneration**
|
||||
|
||||
To automatically regenerate the Traefik config after `up`, `down`, `restart`, or `update`,
|
||||
To automatically regenerate the Traefik config after `up`, `down`, or `update`,
|
||||
add `traefik_file` to your config:
|
||||
|
||||
```yaml
|
||||
compose_dir: /opt/compose
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml # auto-regenerate on up/down/restart/update
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml # auto-regenerate on up/down/update
|
||||
traefik_stack: traefik # skip stacks on same host (docker provider handles them)
|
||||
|
||||
hosts:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
compose_dir: /opt/compose
|
||||
|
||||
# Optional: Auto-regenerate Traefik file-provider config after up/down/restart/update
|
||||
# Optional: Auto-regenerate Traefik file-provider config after up/down/update
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik # Skip stacks on same host (docker provider handles them)
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ Typer-based CLI with subcommand modules:
|
||||
cli/
|
||||
├── app.py # Shared Typer app, version callback
|
||||
├── common.py # Shared helpers, options, progress utilities
|
||||
├── config.py # config subcommand (init, show, path, validate, edit, symlink)
|
||||
├── config.py # config subcommand (init, init-env, show, path, validate, edit, symlink)
|
||||
├── lifecycle.py # up, down, stop, pull, restart, update, apply, compose
|
||||
├── management.py # refresh, check, init-network, traefik-file
|
||||
├── monitoring.py # logs, ps, stats
|
||||
@@ -343,3 +343,19 @@ For repeated connections to the same host, SSH reuses connections.
|
||||
```
|
||||
|
||||
Icons use [Lucide](https://lucide.dev/). Add new icons as macros in `web/templates/partials/icons.html`.
|
||||
|
||||
### Host Resource Monitoring (`src/compose_farm/glances.py`)
|
||||
|
||||
Integration with [Glances](https://nicolargo.github.io/glances/) for real-time host stats:
|
||||
|
||||
- Fetches CPU, memory, and load from Glances REST API on each host
|
||||
- Used by web UI dashboard to display host resource usage
|
||||
- Requires `glances_stack` config option pointing to a Glances stack running on all hosts
|
||||
|
||||
### Container Registry Client (`src/compose_farm/registry.py`)
|
||||
|
||||
OCI Distribution API client for checking image updates:
|
||||
|
||||
- Parses image references (registry, namespace, name, tag, digest)
|
||||
- Fetches available tags from Docker Hub, GHCR, and other registries
|
||||
- Compares semantic versions to find newer releases
|
||||
|
||||
@@ -8,14 +8,16 @@ The Compose Farm CLI is available as both `compose-farm` and the shorter alias `
|
||||
|
||||
## Command Overview
|
||||
|
||||
Commands are either **Docker Compose wrappers** (`up`, `down`, `stop`, `restart`, `pull`, `logs`, `ps`, `compose`) with multi-host superpowers, or **Compose Farm originals** (`apply`, `update`, `refresh`, `check`) for orchestration Docker Compose can't do.
|
||||
|
||||
| Category | Command | Description |
|
||||
|----------|---------|-------------|
|
||||
| **Lifecycle** | `apply` | Make reality match config |
|
||||
| | `up` | Start stacks |
|
||||
| | `down` | Stop stacks |
|
||||
| | `stop` | Stop services without removing containers |
|
||||
| | `restart` | Restart stacks (down + up) |
|
||||
| | `update` | Update stacks (only recreates if images changed) |
|
||||
| | `restart` | Restart running containers |
|
||||
| | `update` | Shorthand for `up --pull --build` |
|
||||
| | `pull` | Pull latest images |
|
||||
| | `compose` | Run any docker compose command |
|
||||
| **Monitoring** | `ps` | Show stack status |
|
||||
@@ -36,6 +38,18 @@ cf --version, -v # Show version
|
||||
cf --help, -h # Show help
|
||||
```
|
||||
|
||||
## Command Aliases
|
||||
|
||||
Short aliases for frequently used commands:
|
||||
|
||||
| Alias | Command | Alias | Command |
|
||||
|-------|---------|-------|---------|
|
||||
| `cf a` | `apply` | `cf s` | `stats` |
|
||||
| `cf l` | `logs` | `cf c` | `compose` |
|
||||
| `cf r` | `restart` | `cf rf` | `refresh` |
|
||||
| `cf u` | `update` | `cf ck` | `check` |
|
||||
| `cf p` | `pull` | `cf tf` | `traefik-file` |
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Commands
|
||||
@@ -58,14 +72,16 @@ cf apply [OPTIONS]
|
||||
|--------|-------------|
|
||||
| `--dry-run, -n` | Preview changes without executing |
|
||||
| `--no-orphans` | Skip stopping orphaned stacks |
|
||||
| `--full, -f` | Also refresh running stacks |
|
||||
| `--no-strays` | Skip stopping stray stacks (running on wrong host) |
|
||||
| `--full, -f` | Also run up on all stacks (applies compose/env changes, triggers migrations) |
|
||||
| `--config, -c PATH` | Path to config file |
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. Stops orphaned stacks (in state but removed from config)
|
||||
2. Migrates stacks on wrong host
|
||||
3. Starts missing stacks (in config but not running)
|
||||
2. Stops stray stacks (running on unauthorized hosts)
|
||||
3. Migrates stacks on wrong host
|
||||
4. Starts missing stacks (in config but not running)
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -79,7 +95,10 @@ cf apply
|
||||
# Only start/migrate, don't stop orphans
|
||||
cf apply --no-orphans
|
||||
|
||||
# Also refresh all running stacks
|
||||
# Don't stop stray stacks
|
||||
cf apply --no-strays
|
||||
|
||||
# Also run up on all stacks (applies compose/env changes, triggers migrations)
|
||||
cf apply --full
|
||||
```
|
||||
|
||||
@@ -100,6 +119,8 @@ cf up [OPTIONS] [STACKS]...
|
||||
| `--all, -a` | Start all stacks |
|
||||
| `--host, -H TEXT` | Filter to stacks on this host |
|
||||
| `--service, -s TEXT` | Target a specific service within the stack |
|
||||
| `--pull` | Pull images before starting (`--pull always`) |
|
||||
| `--build` | Build images before starting |
|
||||
| `--config, -c PATH` | Path to config file |
|
||||
|
||||
**Examples:**
|
||||
@@ -197,7 +218,7 @@ cf stop immich --service database
|
||||
|
||||
### cf restart
|
||||
|
||||
Restart stacks (down + up). With `--service`, restarts just that service.
|
||||
Restart running containers (`docker compose restart`). With `--service`, restarts just that service.
|
||||
|
||||
```bash
|
||||
cf restart [OPTIONS] [STACKS]...
|
||||
@@ -225,7 +246,7 @@ cf restart immich --service database
|
||||
|
||||
### cf update
|
||||
|
||||
Update stacks. Only recreates containers if images changed. With `--service`, updates just that service.
|
||||
Update stacks (pull + build + up). Shorthand for `up --pull --build`. With `--service`, updates just that service.
|
||||
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="/assets/update.webm" type="video/webm">
|
||||
@@ -587,6 +608,7 @@ cf config COMMAND
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `init` | Create new config with examples |
|
||||
| `init-env` | Generate .env file for Docker deployment |
|
||||
| `show` | Display config with highlighting |
|
||||
| `path` | Print config file path |
|
||||
| `validate` | Validate syntax and schema |
|
||||
@@ -598,6 +620,7 @@ cf config COMMAND
|
||||
| Subcommand | Options |
|
||||
|------------|---------|
|
||||
| `init` | `--path/-p PATH`, `--force/-f` |
|
||||
| `init-env` | `--path/-p PATH`, `--output/-o PATH`, `--force/-f` |
|
||||
| `show` | `--path/-p PATH`, `--raw/-r` |
|
||||
| `edit` | `--path/-p PATH` |
|
||||
| `path` | `--path/-p PATH` |
|
||||
@@ -633,6 +656,12 @@ cf config symlink
|
||||
|
||||
# Create symlink to specific file
|
||||
cf config symlink /opt/compose-farm/config.yaml
|
||||
|
||||
# Generate .env file for Docker deployment
|
||||
cf config init-env
|
||||
|
||||
# Generate .env in current directory
|
||||
cf config init-env -o .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -107,7 +107,7 @@ Supported compose file names (checked in order):
|
||||
|
||||
### traefik_file
|
||||
|
||||
Path to auto-generated Traefik file-provider config. When set, Compose Farm regenerates this file after `up`, `down`, `restart`, and `update` commands.
|
||||
Path to auto-generated Traefik file-provider config. When set, Compose Farm regenerates this file after `up`, `down`, and `update` commands.
|
||||
|
||||
```yaml
|
||||
traefik_file: /opt/traefik/dynamic.d/compose-farm.yml
|
||||
@@ -121,6 +121,16 @@ Stack name running Traefik. Stacks on the same host are skipped in file-provider
|
||||
traefik_stack: traefik
|
||||
```
|
||||
|
||||
### glances_stack
|
||||
|
||||
Stack name running [Glances](https://nicolargo.github.io/glances/) for host resource monitoring. When set, the web UI displays CPU, memory, and load stats for all hosts.
|
||||
|
||||
```yaml
|
||||
glances_stack: glances
|
||||
```
|
||||
|
||||
The Glances stack should run on all hosts and expose port 61208. See the README for full setup instructions.
|
||||
|
||||
## Hosts Configuration
|
||||
|
||||
### Basic Host
|
||||
|
||||
@@ -139,7 +139,6 @@ stacks:
|
||||
With `traefik_file` set, these commands auto-regenerate the config:
|
||||
- `cf up`
|
||||
- `cf down`
|
||||
- `cf restart`
|
||||
- `cf update`
|
||||
- `cf apply`
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ Real-world examples demonstrating compose-farm patterns for multi-host Docker de
|
||||
| Stack | Type | Demonstrates |
|
||||
|---------|------|--------------|
|
||||
| [traefik](traefik/) | Infrastructure | Reverse proxy, Let's Encrypt, file-provider |
|
||||
| [coredns](coredns/) | Infrastructure | Wildcard DNS for `*.local` domains |
|
||||
| [mealie](mealie/) | Single container | Traefik labels, resource limits, environment vars |
|
||||
| [uptime-kuma](uptime-kuma/) | Single container | Docker socket, user mapping, custom DNS |
|
||||
| [paperless-ngx](paperless-ngx/) | Multi-container | Redis + App stack (SQLite) |
|
||||
| [paperless-ngx](paperless-ngx/) | Multi-container | Redis + PostgreSQL + App stack |
|
||||
| [autokuma](autokuma/) | Multi-host | Demonstrates `all` keyword (runs on every host) |
|
||||
|
||||
## Key Patterns
|
||||
@@ -53,7 +54,8 @@ labels:
|
||||
- traefik.http.routers.myapp-local.entrypoints=web
|
||||
```
|
||||
|
||||
> **Note:** `.local` domains require local DNS (e.g., Pi-hole, Technitium) to resolve to your Traefik host.
|
||||
> **Note:** `.local` domains require local DNS to resolve to your Traefik host.
|
||||
> The [coredns](coredns/) example provides this - edit `Corefile` to set your Traefik IP.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
@@ -88,23 +90,6 @@ stacks:
|
||||
autokuma: all # Runs on every configured host
|
||||
```
|
||||
|
||||
### Multi-Container Stacks
|
||||
|
||||
Database-backed apps with multiple services:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
app:
|
||||
depends_on:
|
||||
- redis
|
||||
```
|
||||
|
||||
> **NFS + PostgreSQL Warning:** PostgreSQL should NOT run on NFS storage due to
|
||||
> fsync and file locking issues. Use SQLite (safe for single-writer on NFS) or
|
||||
> keep PostgreSQL data on local volumes (non-migratable).
|
||||
|
||||
### AutoKuma Labels (Optional)
|
||||
|
||||
The autokuma example demonstrates compose-farm's **multi-host feature** - running the same stack on all hosts using the `all` keyword. AutoKuma itself is not part of compose-farm; it's just a good example because it needs to run on every host to monitor local Docker containers.
|
||||
@@ -125,8 +110,8 @@ cd examples
|
||||
# 1. Create the shared network on all hosts
|
||||
compose-farm init-network
|
||||
|
||||
# 2. Start Traefik first (the reverse proxy)
|
||||
compose-farm up traefik
|
||||
# 2. Start infrastructure (reverse proxy + DNS)
|
||||
compose-farm up traefik coredns
|
||||
|
||||
# 3. Start other stacks
|
||||
compose-farm up mealie uptime-kuma
|
||||
@@ -168,4 +153,4 @@ traefik_file: /opt/stacks/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik
|
||||
```
|
||||
|
||||
With `traefik_file` configured, compose-farm automatically regenerates the config after `up`, `down`, `restart`, and `update` commands.
|
||||
With `traefik_file` configured, compose-farm automatically regenerates the config after `up`, `down`, and `update` commands.
|
||||
|
||||
@@ -3,6 +3,7 @@ deployed:
|
||||
- primary
|
||||
- secondary
|
||||
- local
|
||||
coredns: primary
|
||||
mealie: secondary
|
||||
paperless-ngx: primary
|
||||
traefik: primary
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
compose_dir: /opt/stacks/compose-farm/examples
|
||||
|
||||
# Auto-regenerate Traefik file-provider config after up/down/restart/update
|
||||
# Auto-regenerate Traefik file-provider config after up/down/update
|
||||
traefik_file: /opt/stacks/compose-farm/examples/traefik/dynamic.d/compose-farm.yml
|
||||
traefik_stack: traefik # Skip Traefik's host in file-provider (docker provider handles it)
|
||||
|
||||
@@ -27,6 +27,7 @@ hosts:
|
||||
stacks:
|
||||
# Infrastructure (runs on primary where Traefik is)
|
||||
traefik: primary
|
||||
coredns: primary # DNS for *.local resolution
|
||||
|
||||
# Multi-host stacks (runs on ALL hosts)
|
||||
# AutoKuma monitors Docker containers on each host
|
||||
|
||||
2
examples/coredns/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
# CoreDNS doesn't need environment variables
|
||||
# The Traefik IP is configured in the Corefile
|
||||
22
examples/coredns/Corefile
Normal file
@@ -0,0 +1,22 @@
|
||||
# CoreDNS configuration for .local domain resolution
|
||||
#
|
||||
# Resolves *.local to the Traefik host IP (where your reverse proxy runs).
|
||||
# All other queries are forwarded to upstream DNS.
|
||||
|
||||
# Handle .local domains - resolve everything to Traefik's host
|
||||
local {
|
||||
template IN A {
|
||||
answer "{{ .Name }} 60 IN A 192.168.1.10"
|
||||
}
|
||||
template IN AAAA {
|
||||
# Return empty for AAAA to avoid delays on IPv4-only networks
|
||||
rcode NOERROR
|
||||
}
|
||||
}
|
||||
|
||||
# Forward everything else to upstream DNS
|
||||
. {
|
||||
forward . 1.1.1.1 8.8.8.8
|
||||
cache 300
|
||||
errors
|
||||
}
|
||||
27
examples/coredns/compose.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# CoreDNS - DNS server for .local domain resolution
|
||||
#
|
||||
# Demonstrates:
|
||||
# - Wildcard DNS for *.local domains
|
||||
# - Config file mounting from stack directory
|
||||
# - UDP/TCP port exposure
|
||||
#
|
||||
# This enables all the .local routes in the examples to work.
|
||||
# Point your devices/router DNS to this server's IP.
|
||||
name: coredns
|
||||
services:
|
||||
coredns:
|
||||
image: coredns/coredns:latest
|
||||
container_name: coredns
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mynetwork
|
||||
ports:
|
||||
- "53:53/udp"
|
||||
- "53:53/tcp"
|
||||
volumes:
|
||||
- ./Corefile:/root/Corefile:ro
|
||||
command: -conf /root/Corefile
|
||||
|
||||
networks:
|
||||
mynetwork:
|
||||
external: true
|
||||
@@ -1,3 +1,4 @@
|
||||
# Copy to .env and fill in your values
|
||||
DOMAIN=example.com
|
||||
PAPERLESS_SECRET_KEY=change-me-to-a-random-string
|
||||
POSTGRES_PASSWORD=change-me-to-a-secure-password
|
||||
PAPERLESS_SECRET_KEY=change-me-to-a-long-random-string
|
||||
|
||||
@@ -1,44 +1,57 @@
|
||||
# Paperless-ngx - Document management system
|
||||
#
|
||||
# Demonstrates:
|
||||
# - HTTPS route: paperless.${DOMAIN} (e.g., paperless.example.com) with Let's Encrypt
|
||||
# - HTTP route: paperless.local for LAN access without TLS
|
||||
# - Multi-container stack (Redis + App with SQLite)
|
||||
#
|
||||
# NOTE: This example uses SQLite (the default) instead of PostgreSQL.
|
||||
# PostgreSQL should NOT be used with NFS storage due to fsync/locking issues.
|
||||
# If you need PostgreSQL, use local volumes for the database.
|
||||
# - HTTPS route: paperless.${DOMAIN} with Let's Encrypt
|
||||
# - HTTP route: paperless.local for LAN access
|
||||
# - Multi-container stack (Redis + PostgreSQL + App)
|
||||
# - Separate env_file for app-specific settings
|
||||
name: paperless-ngx
|
||||
services:
|
||||
redis:
|
||||
image: redis:8
|
||||
broker:
|
||||
image: redis:7
|
||||
container_name: paperless-redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mynetwork
|
||||
volumes:
|
||||
- /mnt/data/paperless/redis:/data
|
||||
- /mnt/data/paperless/redisdata:/data
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
container_name: paperless-db
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mynetwork
|
||||
volumes:
|
||||
- /mnt/data/paperless/pgdata:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
paperless:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
container_name: paperless
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- redis
|
||||
- db
|
||||
- broker
|
||||
networks:
|
||||
- mynetwork
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
# SQLite database stored here (safe on NFS for single-writer)
|
||||
- /mnt/data/paperless/data:/usr/src/paperless/data
|
||||
- /mnt/data/paperless/media:/usr/src/paperless/media
|
||||
- /mnt/data/paperless/export:/usr/src/paperless/export
|
||||
- /mnt/data/paperless/consume:/usr/src/paperless/consume
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://redis:6379
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
PAPERLESS_URL: https://paperless.${DOMAIN}
|
||||
PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY}
|
||||
PAPERLESS_TIME_ZONE: America/Los_Angeles
|
||||
PAPERLESS_OCR_LANGUAGE: eng
|
||||
USERMAP_UID: 1000
|
||||
USERMAP_GID: 1000
|
||||
labels:
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Barn with Container Doors -->
|
||||
<defs>
|
||||
<linearGradient id="barnRed" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#D94B4B"/>
|
||||
<stop offset="100%" style="stop-color:#B33A3A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="roofRed" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#C44040"/>
|
||||
<stop offset="100%" style="stop-color:#9E2E2E"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Barn body -->
|
||||
<rect x="40" y="90" width="120" height="80" fill="url(#barnRed)" rx="2"/>
|
||||
|
||||
<!-- Barn roof -->
|
||||
<polygon points="30,90 100,40 170,90" fill="url(#roofRed)"/>
|
||||
|
||||
<!-- Roof cupola -->
|
||||
<rect x="90" y="35" width="20" height="15" fill="#8B4513"/>
|
||||
<polygon points="85,35 100,20 115,35" fill="#A0522D"/>
|
||||
|
||||
<!-- Container doors (left) -->
|
||||
<rect x="48" y="105" width="45" height="58" fill="#2496ED" rx="2"/>
|
||||
<rect x="50" y="107" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="50" y="121" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="50" y="135" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="50" y="149" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
|
||||
<!-- Container doors (right) -->
|
||||
<rect x="107" y="105" width="45" height="58" fill="#2496ED" rx="2"/>
|
||||
<rect x="109" y="107" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="109" y="121" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="109" y="135" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="109" y="149" width="41" height="12" fill="#1A7AC7" rx="1"/>
|
||||
|
||||
<!-- Door handles -->
|
||||
<circle cx="90" cy="134" r="3" fill="#F0F0F0"/>
|
||||
<circle cx="110" cy="134" r="3" fill="#F0F0F0"/>
|
||||
|
||||
<!-- Ground -->
|
||||
<ellipse cx="100" cy="175" rx="70" ry="8" fill="#5D8A3E" opacity="0.6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,72 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Container with Wheat Growing -->
|
||||
<defs>
|
||||
<linearGradient id="containerBlue" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="100%" style="stop-color:#1A7AC7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="wheat" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#F4D03F"/>
|
||||
<stop offset="100%" style="stop-color:#D4AC0D"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Container base -->
|
||||
<rect x="35" y="110" width="130" height="70" fill="url(#containerBlue)" rx="4"/>
|
||||
|
||||
<!-- Container ridges -->
|
||||
<rect x="40" y="115" width="120" height="14" fill="#1A7AC7" rx="2"/>
|
||||
<rect x="40" y="133" width="120" height="14" fill="#1A7AC7" rx="2"/>
|
||||
<rect x="40" y="151" width="120" height="14" fill="#1A7AC7" rx="2"/>
|
||||
<rect x="40" y="169" width="120" height="8" fill="#1A7AC7" rx="2"/>
|
||||
|
||||
<!-- Container corner posts -->
|
||||
<rect x="35" y="110" width="8" height="70" fill="#1565C0"/>
|
||||
<rect x="157" y="110" width="8" height="70" fill="#1565C0"/>
|
||||
|
||||
<!-- Wheat stalks -->
|
||||
<g stroke="#8B7355" stroke-width="2" fill="none">
|
||||
<path d="M70 110 Q68 80 70 50"/>
|
||||
<path d="M100 110 Q100 70 100 35"/>
|
||||
<path d="M130 110 Q132 80 130 50"/>
|
||||
</g>
|
||||
|
||||
<!-- Wheat heads (left) -->
|
||||
<g fill="url(#wheat)">
|
||||
<ellipse cx="70" cy="50" rx="4" ry="8"/>
|
||||
<ellipse cx="65" cy="55" rx="4" ry="7"/>
|
||||
<ellipse cx="75" cy="55" rx="4" ry="7"/>
|
||||
<ellipse cx="63" cy="62" rx="3" ry="6"/>
|
||||
<ellipse cx="77" cy="62" rx="3" ry="6"/>
|
||||
</g>
|
||||
|
||||
<!-- Wheat heads (center - taller) -->
|
||||
<g fill="url(#wheat)">
|
||||
<ellipse cx="100" cy="35" rx="5" ry="9"/>
|
||||
<ellipse cx="94" cy="42" rx="4" ry="8"/>
|
||||
<ellipse cx="106" cy="42" rx="4" ry="8"/>
|
||||
<ellipse cx="91" cy="51" rx="4" ry="7"/>
|
||||
<ellipse cx="109" cy="51" rx="4" ry="7"/>
|
||||
<ellipse cx="94" cy="60" rx="3" ry="6"/>
|
||||
<ellipse cx="106" cy="60" rx="3" ry="6"/>
|
||||
</g>
|
||||
|
||||
<!-- Wheat heads (right) -->
|
||||
<g fill="url(#wheat)">
|
||||
<ellipse cx="130" cy="50" rx="4" ry="8"/>
|
||||
<ellipse cx="125" cy="55" rx="4" ry="7"/>
|
||||
<ellipse cx="135" cy="55" rx="4" ry="7"/>
|
||||
<ellipse cx="123" cy="62" rx="3" ry="6"/>
|
||||
<ellipse cx="137" cy="62" rx="3" ry="6"/>
|
||||
</g>
|
||||
|
||||
<!-- Small leaves on stalks -->
|
||||
<g fill="#6B8E23">
|
||||
<path d="M70 85 Q60 80 70 75"/>
|
||||
<path d="M70 75 Q80 70 70 65"/>
|
||||
<path d="M100 90 Q88 85 100 80"/>
|
||||
<path d="M100 75 Q112 70 100 65"/>
|
||||
<path d="M130 85 Q140 80 130 75"/>
|
||||
<path d="M130 75 Q120 70 130 65"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,56 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Silos as Stacked Containers -->
|
||||
<defs>
|
||||
<linearGradient id="silo1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="50%" style="stop-color:#4AA8F2"/>
|
||||
<stop offset="100%" style="stop-color:#2496ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="silo2" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#1E88E5"/>
|
||||
<stop offset="50%" style="stop-color:#42A5F5"/>
|
||||
<stop offset="100%" style="stop-color:#1E88E5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="silo3" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#1976D2"/>
|
||||
<stop offset="50%" style="stop-color:#2196F3"/>
|
||||
<stop offset="100%" style="stop-color:#1976D2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Left silo (shortest) -->
|
||||
<rect x="25" y="100" width="40" height="75" fill="url(#silo1)" rx="3"/>
|
||||
<ellipse cx="45" cy="100" rx="20" ry="8" fill="#4AA8F2"/>
|
||||
<ellipse cx="45" cy="175" rx="20" ry="5" fill="#1565C0"/>
|
||||
<!-- Container ridges -->
|
||||
<path d="M25 115 h40 M25 130 h40 M25 145 h40 M25 160 h40" stroke="#1A7AC7" stroke-width="2"/>
|
||||
<!-- Roof dome -->
|
||||
<ellipse cx="45" cy="100" rx="18" ry="12" fill="#1565C0"/>
|
||||
<ellipse cx="45" cy="98" rx="16" ry="10" fill="#2196F3"/>
|
||||
|
||||
<!-- Middle silo (tallest) -->
|
||||
<rect x="80" y="55" width="45" height="120" fill="url(#silo2)" rx="3"/>
|
||||
<ellipse cx="102.5" cy="55" rx="22.5" ry="10" fill="#42A5F5"/>
|
||||
<ellipse cx="102.5" cy="175" rx="22.5" ry="5" fill="#1565C0"/>
|
||||
<!-- Container ridges -->
|
||||
<path d="M80 75 h45 M80 95 h45 M80 115 h45 M80 135 h45 M80 155 h45" stroke="#1976D2" stroke-width="2"/>
|
||||
<!-- Roof dome -->
|
||||
<ellipse cx="102.5" cy="55" rx="20" ry="14" fill="#1565C0"/>
|
||||
<ellipse cx="102.5" cy="52" rx="18" ry="12" fill="#2196F3"/>
|
||||
<!-- Cupola -->
|
||||
<rect x="97" y="35" width="11" height="17" fill="#1565C0"/>
|
||||
<polygon points="92,35 102.5,22 113,35" fill="#1976D2"/>
|
||||
|
||||
<!-- Right silo (medium) -->
|
||||
<rect x="140" y="85" width="38" height="90" fill="url(#silo3)" rx="3"/>
|
||||
<ellipse cx="159" cy="85" rx="19" ry="8" fill="#2196F3"/>
|
||||
<ellipse cx="159" cy="175" rx="19" ry="5" fill="#1565C0"/>
|
||||
<!-- Container ridges -->
|
||||
<path d="M140 100 h38 M140 115 h38 M140 130 h38 M140 145 h38 M140 160 h38" stroke="#1565C0" stroke-width="2"/>
|
||||
<!-- Roof dome -->
|
||||
<ellipse cx="159" cy="85" rx="17" ry="11" fill="#1565C0"/>
|
||||
<ellipse cx="159" cy="83" rx="15" ry="9" fill="#2196F3"/>
|
||||
|
||||
<!-- Ground -->
|
||||
<ellipse cx="100" cy="180" rx="85" ry="10" fill="#5D8A3E" opacity="0.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,69 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Minimalist Tractor with Container Trailer -->
|
||||
<defs>
|
||||
<linearGradient id="tractorGreen" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4CAF50"/>
|
||||
<stop offset="100%" style="stop-color:#388E3C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="containerBlue2" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="100%" style="stop-color:#1976D2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Container trailer -->
|
||||
<rect x="90" y="95" width="85" height="50" fill="url(#containerBlue2)" rx="3"/>
|
||||
<!-- Container ridges -->
|
||||
<rect x="95" y="100" width="75" height="10" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="95" y="113" width="75" height="10" fill="#1A7AC7" rx="1"/>
|
||||
<rect x="95" y="126" width="75" height="10" fill="#1A7AC7" rx="1"/>
|
||||
<!-- Corner posts -->
|
||||
<rect x="90" y="95" width="6" height="50" fill="#1565C0"/>
|
||||
<rect x="169" y="95" width="6" height="50" fill="#1565C0"/>
|
||||
|
||||
<!-- Trailer wheels -->
|
||||
<circle cx="115" cy="155" r="12" fill="#333"/>
|
||||
<circle cx="115" cy="155" r="6" fill="#666"/>
|
||||
<circle cx="150" cy="155" r="12" fill="#333"/>
|
||||
<circle cx="150" cy="155" r="6" fill="#666"/>
|
||||
|
||||
<!-- Trailer hitch bar -->
|
||||
<rect x="55" y="125" width="40" height="6" fill="#555"/>
|
||||
|
||||
<!-- Tractor body -->
|
||||
<rect x="20" y="90" width="50" height="40" fill="url(#tractorGreen)" rx="5"/>
|
||||
|
||||
<!-- Tractor hood -->
|
||||
<rect x="15" y="100" width="20" height="25" fill="url(#tractorGreen)" rx="3"/>
|
||||
|
||||
<!-- Tractor cabin -->
|
||||
<rect x="35" y="70" width="30" height="25" fill="#333" rx="3"/>
|
||||
<rect x="38" y="73" width="24" height="18" fill="#87CEEB" rx="2"/>
|
||||
|
||||
<!-- Exhaust pipe -->
|
||||
<rect x="18" y="80" width="5" height="20" fill="#444" rx="1"/>
|
||||
|
||||
<!-- Big rear wheel -->
|
||||
<circle cx="55" cy="145" r="22" fill="#333"/>
|
||||
<circle cx="55" cy="145" r="16" fill="#444"/>
|
||||
<circle cx="55" cy="145" r="8" fill="#666"/>
|
||||
<!-- Wheel treads -->
|
||||
<g stroke="#555" stroke-width="3">
|
||||
<line x1="55" y1="125" x2="55" y2="130"/>
|
||||
<line x1="55" y1="160" x2="55" y2="165"/>
|
||||
<line x1="35" y1="145" x2="40" y2="145"/>
|
||||
<line x1="70" y1="145" x2="75" y2="145"/>
|
||||
<line x1="40" y1="130" x2="43" y2="133"/>
|
||||
<line x1="67" y1="157" x2="70" y2="160"/>
|
||||
<line x1="40" y1="160" x2="43" y2="157"/>
|
||||
<line x1="67" y1="133" x2="70" y2="130"/>
|
||||
</g>
|
||||
|
||||
<!-- Small front wheel -->
|
||||
<circle cx="22" cy="145" r="12" fill="#333"/>
|
||||
<circle cx="22" cy="145" r="8" fill="#444"/>
|
||||
<circle cx="22" cy="145" r="4" fill="#666"/>
|
||||
|
||||
<!-- Ground line -->
|
||||
<line x1="5" y1="167" x2="195" y2="167" stroke="#5D8A3E" stroke-width="3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,57 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Harvest Crate / Container Hybrid -->
|
||||
<defs>
|
||||
<linearGradient id="wood" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#DEB887"/>
|
||||
<stop offset="100%" style="stop-color:#B8860B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="blueAccent" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="100%" style="stop-color:#1976D2"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Crate back panel (3D effect) -->
|
||||
<polygon points="45,60 155,60 175,45 65,45" fill="#A0522D"/>
|
||||
<polygon points="155,60 155,150 175,135 175,45" fill="#8B4513"/>
|
||||
|
||||
<!-- Main crate body -->
|
||||
<rect x="45" y="60" width="110" height="90" fill="url(#wood)" rx="3"/>
|
||||
|
||||
<!-- Horizontal slats -->
|
||||
<rect x="45" y="60" width="110" height="18" fill="#C9A86C" rx="2"/>
|
||||
<rect x="45" y="82" width="110" height="18" fill="#C9A86C" rx="2"/>
|
||||
<rect x="45" y="104" width="110" height="18" fill="#C9A86C" rx="2"/>
|
||||
<rect x="45" y="126" width="110" height="18" fill="#C9A86C" rx="2"/>
|
||||
|
||||
<!-- Slat gaps (darker lines) -->
|
||||
<line x1="45" y1="78" x2="155" y2="78" stroke="#8B7355" stroke-width="2"/>
|
||||
<line x1="45" y1="100" x2="155" y2="100" stroke="#8B7355" stroke-width="2"/>
|
||||
<line x1="45" y1="122" x2="155" y2="122" stroke="#8B7355" stroke-width="2"/>
|
||||
<line x1="45" y1="144" x2="155" y2="144" stroke="#8B7355" stroke-width="2"/>
|
||||
|
||||
<!-- Blue container-style corner brackets -->
|
||||
<path d="M45 60 L45 90 L55 90 L55 70 L75 70 L75 60 Z" fill="url(#blueAccent)"/>
|
||||
<path d="M155 60 L155 90 L145 90 L145 70 L125 70 L125 60 Z" fill="url(#blueAccent)"/>
|
||||
<path d="M45 150 L45 120 L55 120 L55 140 L75 140 L75 150 Z" fill="url(#blueAccent)"/>
|
||||
<path d="M155 150 L155 120 L145 120 L145 140 L125 140 L125 150 Z" fill="url(#blueAccent)"/>
|
||||
|
||||
<!-- Docker/container symbol in center -->
|
||||
<g transform="translate(100, 105)">
|
||||
<rect x="-25" y="-15" width="50" height="30" fill="#2496ED" rx="3" opacity="0.9"/>
|
||||
<!-- Container boxes inside -->
|
||||
<rect x="-22" y="-12" width="12" height="8" fill="#fff" rx="1"/>
|
||||
<rect x="-7" y="-12" width="12" height="8" fill="#fff" rx="1"/>
|
||||
<rect x="8" y="-12" width="12" height="8" fill="#fff" rx="1"/>
|
||||
<rect x="-22" y="0" width="12" height="8" fill="#fff" rx="1"/>
|
||||
<rect x="-7" y="0" width="12" height="8" fill="#fff" rx="1"/>
|
||||
<rect x="8" y="0" width="12" height="8" fill="#fff" rx="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Rope handles -->
|
||||
<ellipse cx="35" cy="100" rx="8" ry="15" fill="none" stroke="#8B7355" stroke-width="4"/>
|
||||
<ellipse cx="165" cy="100" rx="8" ry="15" fill="none" stroke="#8B7355" stroke-width="4"/>
|
||||
|
||||
<!-- Ground shadow -->
|
||||
<ellipse cx="100" cy="160" rx="60" ry="8" fill="#333" opacity="0.2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,42 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Minimal Icon: Container with sprouting plant -->
|
||||
<defs>
|
||||
<linearGradient id="blueGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="100%" style="stop-color:#1565C0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="greenGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#4CAF50"/>
|
||||
<stop offset="100%" style="stop-color:#81C784"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Container -->
|
||||
<rect x="40" y="100" width="120" height="70" fill="url(#blueGrad)" rx="6"/>
|
||||
|
||||
<!-- Container ridges -->
|
||||
<g fill="#1A7AC7">
|
||||
<rect x="48" y="108" width="104" height="12" rx="2"/>
|
||||
<rect x="48" y="124" width="104" height="12" rx="2"/>
|
||||
<rect x="48" y="140" width="104" height="12" rx="2"/>
|
||||
<rect x="48" y="156" width="104" height="10" rx="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Corner posts -->
|
||||
<rect x="40" y="100" width="10" height="70" fill="#0D47A1" rx="2"/>
|
||||
<rect x="150" y="100" width="10" height="70" fill="#0D47A1" rx="2"/>
|
||||
|
||||
<!-- Plant stem -->
|
||||
<path d="M100 100 Q100 75 100 50" stroke="#4CAF50" stroke-width="6" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Leaves -->
|
||||
<ellipse cx="85" cy="65" rx="15" ry="8" fill="url(#greenGrad)" transform="rotate(-30 85 65)"/>
|
||||
<ellipse cx="115" cy="65" rx="15" ry="8" fill="url(#greenGrad)" transform="rotate(30 115 65)"/>
|
||||
<ellipse cx="80" cy="80" rx="12" ry="6" fill="url(#greenGrad)" transform="rotate(-45 80 80)"/>
|
||||
<ellipse cx="120" cy="80" rx="12" ry="6" fill="url(#greenGrad)" transform="rotate(45 120 80)"/>
|
||||
|
||||
<!-- Top leaves (crown) -->
|
||||
<ellipse cx="100" cy="45" rx="10" ry="18" fill="url(#greenGrad)"/>
|
||||
<ellipse cx="90" cy="50" rx="8" ry="14" fill="#66BB6A" transform="rotate(-20 90 50)"/>
|
||||
<ellipse cx="110" cy="50" rx="8" ry="14" fill="#66BB6A" transform="rotate(20 110 50)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,37 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Hexagon grid: containers as honeycomb/farm cells -->
|
||||
<defs>
|
||||
<linearGradient id="hex1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2496ED"/>
|
||||
<stop offset="100%" style="stop-color:#1976D2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hex2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4CAF50"/>
|
||||
<stop offset="100%" style="stop-color:#388E3C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Hexagon cluster -->
|
||||
<g transform="translate(100, 100)">
|
||||
<!-- Center hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)"/>
|
||||
|
||||
<!-- Top hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(0, -52)" opacity="0.8"/>
|
||||
|
||||
<!-- Top-right hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(45, -26)" opacity="0.6"/>
|
||||
|
||||
<!-- Bottom-right hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(45, 26)" opacity="0.7"/>
|
||||
|
||||
<!-- Bottom hex - green accent -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex2)" transform="translate(0, 52)"/>
|
||||
|
||||
<!-- Bottom-left hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(-45, 26)" opacity="0.7"/>
|
||||
|
||||
<!-- Top-left hex -->
|
||||
<polygon points="0,-30 26,-15 26,15 0,30 -26,15 -26,-15" fill="url(#hex1)" transform="translate(-45, -26)" opacity="0.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,16 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Minimal: geometric container with abstract sprout -->
|
||||
|
||||
<!-- Container base - single rounded rectangle -->
|
||||
<rect x="40" y="95" width="120" height="75" rx="8" fill="#2496ED"/>
|
||||
|
||||
<!-- Simple horizontal lines -->
|
||||
<line x1="50" y1="115" x2="150" y2="115" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="50" y1="135" x2="150" y2="135" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
|
||||
<line x1="50" y1="155" x2="150" y2="155" stroke="#1565C0" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- Abstract sprout - two simple leaves -->
|
||||
<path d="M100 95 L100 55" stroke="#4CAF50" stroke-width="4" stroke-linecap="round"/>
|
||||
<ellipse cx="85" cy="55" rx="18" ry="10" fill="#4CAF50" transform="rotate(-35 85 55)"/>
|
||||
<ellipse cx="115" cy="55" rx="18" ry="10" fill="#4CAF50" transform="rotate(35 115 55)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 913 B |
@@ -1,16 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Stacked containers with upward growth arrow -->
|
||||
|
||||
<!-- Bottom container -->
|
||||
<rect x="50" y="130" width="100" height="35" rx="4" fill="#2496ED"/>
|
||||
|
||||
<!-- Middle container -->
|
||||
<rect x="60" y="90" width="80" height="35" rx="4" fill="#42A5F5"/>
|
||||
|
||||
<!-- Top container -->
|
||||
<rect x="70" y="50" width="60" height="35" rx="4" fill="#64B5F6"/>
|
||||
|
||||
<!-- Growth arrow emerging from top -->
|
||||
<path d="M100 50 L100 25" stroke="#4CAF50" stroke-width="6" stroke-linecap="round"/>
|
||||
<polygon points="100,15 90,28 110,28" fill="#4CAF50"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 605 B |
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Negative space: container with plant cutout -->
|
||||
<defs>
|
||||
<mask id="plantMask">
|
||||
<rect width="200" height="200" fill="white"/>
|
||||
<!-- Stem cutout -->
|
||||
<rect x="96" y="40" width="8" height="55" fill="black"/>
|
||||
<!-- Leaf cutouts -->
|
||||
<ellipse cx="82" cy="55" rx="16" ry="9" fill="black" transform="rotate(-40 82 55)"/>
|
||||
<ellipse cx="118" cy="55" rx="16" ry="9" fill="black" transform="rotate(40 118 55)"/>
|
||||
<ellipse cx="100" cy="38" rx="10" ry="14" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<!-- Container with mask -->
|
||||
<g mask="url(#plantMask)">
|
||||
<rect x="35" y="45" width="130" height="120" rx="12" fill="#2496ED"/>
|
||||
<!-- Ridges -->
|
||||
<rect x="35" y="70" width="130" height="4" fill="#1976D2"/>
|
||||
<rect x="35" y="95" width="130" height="4" fill="#1976D2"/>
|
||||
<rect x="35" y="120" width="130" height="4" fill="#1976D2"/>
|
||||
<rect x="35" y="145" width="130" height="4" fill="#1976D2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1018 B |
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- CF Monogram with container lines -->
|
||||
|
||||
<!-- C shape -->
|
||||
<path d="M45 100
|
||||
C45 55, 85 35, 115 45
|
||||
L108 62
|
||||
C88 55, 62 70, 62 100
|
||||
C62 130, 88 145, 108 138
|
||||
L115 155
|
||||
C85 165, 45 145, 45 100"
|
||||
fill="#2496ED"/>
|
||||
|
||||
<!-- F shape integrated with container lines -->
|
||||
<rect x="110" y="45" width="50" height="14" rx="2" fill="#2496ED"/>
|
||||
<rect x="110" y="45" width="14" height="110" rx="2" fill="#2496ED"/>
|
||||
<rect x="110" y="90" width="40" height="12" rx="2" fill="#2496ED"/>
|
||||
|
||||
<!-- Container ridge accents on F -->
|
||||
<rect x="128" y="63" width="32" height="3" fill="#4CAF50"/>
|
||||
<rect x="128" y="73" width="32" height="3" fill="#4CAF50"/>
|
||||
<rect x="128" y="108" width="22" height="3" fill="#4CAF50"/>
|
||||
<rect x="128" y="118" width="22" height="3" fill="#4CAF50"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 909 B |
@@ -1,25 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Ultra-minimal barn silhouette -->
|
||||
|
||||
<!-- Barn shape - single path -->
|
||||
<path d="M35 170
|
||||
L35 85
|
||||
L100 40
|
||||
L165 85
|
||||
L165 170
|
||||
Z"
|
||||
fill="#2496ED"/>
|
||||
|
||||
<!-- Container door accent - simple rectangle grid -->
|
||||
<g fill="#fff" opacity="0.95">
|
||||
<rect x="55" y="100" width="35" height="12" rx="2"/>
|
||||
<rect x="55" y="118" width="35" height="12" rx="2"/>
|
||||
<rect x="55" y="136" width="35" height="12" rx="2"/>
|
||||
<rect x="55" y="154" width="35" height="14" rx="2"/>
|
||||
|
||||
<rect x="110" y="100" width="35" height="12" rx="2"/>
|
||||
<rect x="110" y="118" width="35" height="12" rx="2"/>
|
||||
<rect x="110" y="136" width="35" height="12" rx="2"/>
|
||||
<rect x="110" y="154" width="35" height="14" rx="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 841 B |
@@ -1,42 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Container grid made of dots - represents distributed containers -->
|
||||
|
||||
<g fill="#2496ED">
|
||||
<!-- Row 1 -->
|
||||
<circle cx="60" cy="50" r="12"/>
|
||||
<circle cx="100" cy="50" r="12"/>
|
||||
<circle cx="140" cy="50" r="12"/>
|
||||
|
||||
<!-- Row 2 -->
|
||||
<circle cx="60" cy="90" r="12"/>
|
||||
<circle cx="100" cy="90" r="12"/>
|
||||
<circle cx="140" cy="90" r="12"/>
|
||||
|
||||
<!-- Row 3 -->
|
||||
<circle cx="60" cy="130" r="12"/>
|
||||
<circle cx="100" cy="130" r="12" fill="#4CAF50"/>
|
||||
<circle cx="140" cy="130" r="12"/>
|
||||
</g>
|
||||
|
||||
<!-- Connection lines -->
|
||||
<g stroke="#2496ED" stroke-width="2" opacity="0.4">
|
||||
<line x1="60" y1="62" x2="60" y2="78"/>
|
||||
<line x1="100" y1="62" x2="100" y2="78"/>
|
||||
<line x1="140" y1="62" x2="140" y2="78"/>
|
||||
<line x1="60" y1="102" x2="60" y2="118"/>
|
||||
<line x1="100" y1="102" x2="100" y2="118"/>
|
||||
<line x1="140" y1="102" x2="140" y2="118"/>
|
||||
<line x1="72" y1="50" x2="88" y2="50"/>
|
||||
<line x1="112" y1="50" x2="128" y2="50"/>
|
||||
<line x1="72" y1="90" x2="88" y2="90"/>
|
||||
<line x1="112" y1="90" x2="128" y2="90"/>
|
||||
<line x1="72" y1="130" x2="88" y2="130"/>
|
||||
<line x1="112" y1="130" x2="128" y2="130"/>
|
||||
</g>
|
||||
|
||||
<!-- Sprout from green center -->
|
||||
<path d="M100 118 L100 155" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
|
||||
<circle cx="92" cy="158" r="6" fill="#4CAF50"/>
|
||||
<circle cx="108" cy="158" r="6" fill="#4CAF50"/>
|
||||
<circle cx="100" cy="165" r="5" fill="#66BB6A"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,18 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<!-- Single bold mark: container + leaf combined -->
|
||||
|
||||
<!-- Main container shape -->
|
||||
<rect x="40" y="60" width="120" height="100" rx="10" fill="#2496ED"/>
|
||||
|
||||
<!-- Leaf growing from top-right corner -->
|
||||
<path d="M140 60
|
||||
Q170 30, 160 55
|
||||
Q175 25, 145 50
|
||||
Q165 15, 130 50
|
||||
L140 60"
|
||||
fill="#4CAF50"/>
|
||||
|
||||
<!-- Minimal internal lines -->
|
||||
<line x1="55" y1="95" x2="145" y2="95" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
|
||||
<line x1="55" y1="125" x2="145" y2="125" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 681 B |
@@ -1,136 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1052 523">
|
||||
<defs>
|
||||
<!-- Container blue colors -->
|
||||
<linearGradient id="containerFront" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#4AA8D8"/>
|
||||
<stop offset="100%" style="stop-color:#5BB5E0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="containerSide" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#3A8AB8"/>
|
||||
<stop offset="100%" style="stop-color:#4AA8D8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="containerTop" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#6BC4E8"/>
|
||||
<stop offset="100%" style="stop-color:#7DD0F0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="roofGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#4AA8D8"/>
|
||||
<stop offset="100%" style="stop-color:#6BC4E8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Green farm field -->
|
||||
<path d="M526 420 L120 280 L526 200 L932 280 Z" fill="#7CB342"/>
|
||||
<path d="M526 420 L526 200" stroke="#8BC34A" stroke-width="2"/>
|
||||
<path d="M526 420 L220 265" stroke="#8BC34A" stroke-width="2"/>
|
||||
<path d="M526 420 L832 265" stroke="#8BC34A" stroke-width="2"/>
|
||||
<path d="M526 420 L320 250" stroke="#8BC34A" stroke-width="2"/>
|
||||
<path d="M526 420 L732 250" stroke="#8BC34A" stroke-width="2"/>
|
||||
<!-- Field edge -->
|
||||
<ellipse cx="526" cy="420" rx="410" ry="30" fill="#5D8C2A"/>
|
||||
|
||||
<!-- Left container stack -->
|
||||
<g transform="translate(95, 180)">
|
||||
<!-- Back container -->
|
||||
<polygon points="60,40 130,10 130,70 60,100" fill="#3A8AB8"/>
|
||||
<polygon points="0,70 60,40 60,100 0,130" fill="url(#containerFront)"/>
|
||||
<polygon points="0,70 60,40 130,70 70,100" fill="url(#containerTop)"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="5" y1="80" x2="55" y2="52" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="95" x2="55" y2="67" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="110" x2="55" y2="82" stroke="#2D7A9E" stroke-width="2"/>
|
||||
|
||||
<!-- Front container -->
|
||||
<polygon points="60,80 130,50 130,110 60,140" fill="#3A8AB8"/>
|
||||
<polygon points="0,110 60,80 60,140 0,170" fill="url(#containerFront)"/>
|
||||
<polygon points="0,110 60,80 130,110 70,140" fill="url(#containerTop)"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="5" y1="120" x2="55" y2="92" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="135" x2="55" y2="107" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="150" x2="55" y2="122" stroke="#2D7A9E" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Right container stack -->
|
||||
<g transform="translate(827, 180)">
|
||||
<!-- Back container -->
|
||||
<polygon points="0,10 70,40 70,100 0,70" fill="url(#containerFront)"/>
|
||||
<polygon points="70,40 130,70 130,130 70,100" fill="#3A8AB8"/>
|
||||
<polygon points="0,10 70,40 130,10 60,0" fill="url(#containerTop)"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="5" y1="20" x2="65" y2="48" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="35" x2="65" y2="63" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="50" x2="65" y2="78" stroke="#2D7A9E" stroke-width="2"/>
|
||||
|
||||
<!-- Front container -->
|
||||
<polygon points="0,50 70,80 70,140 0,110" fill="url(#containerFront)"/>
|
||||
<polygon points="70,80 130,110 130,170 70,140" fill="#3A8AB8"/>
|
||||
<polygon points="0,50 70,80 130,50 60,20" fill="url(#containerTop)"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="5" y1="60" x2="65" y2="88" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="75" x2="65" y2="103" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="5" y1="90" x2="65" y2="118" stroke="#2D7A9E" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Dashed connection lines -->
|
||||
<g stroke="#2D3E50" stroke-width="3" stroke-dasharray="8,6" fill="none">
|
||||
<path d="M225 320 Q350 280 430 300"/>
|
||||
<path d="M827 320 Q700 280 620 300"/>
|
||||
<path d="M225 350 Q300 340 380 360"/>
|
||||
<path d="M827 350 Q750 340 670 360"/>
|
||||
</g>
|
||||
|
||||
<!-- Central barn/container structure -->
|
||||
<g transform="translate(400, 85)">
|
||||
<!-- Base left container -->
|
||||
<polygon points="0,220 126,155 126,295 0,360" fill="url(#containerFront)"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="10" y1="232" x2="116" y2="172" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="10" y1="262" x2="116" y2="202" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="10" y1="292" x2="116" y2="232" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="10" y1="322" x2="116" y2="262" stroke="#2D7A9E" stroke-width="3"/>
|
||||
|
||||
<!-- Base right container -->
|
||||
<polygon points="126,155 252,220 252,360 126,295" fill="#3A8AB8"/>
|
||||
<!-- Container ridges -->
|
||||
<line x1="136" y1="172" x2="242" y2="232" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="136" y1="202" x2="242" y2="262" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="136" y1="232" x2="242" y2="292" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="136" y1="262" x2="242" y2="322" stroke="#2D7A9E" stroke-width="3"/>
|
||||
|
||||
<!-- Door opening -->
|
||||
<polygon points="80,260 126,235 172,260 172,360 126,335 80,360" fill="#1A2530"/>
|
||||
|
||||
<!-- Upper left container -->
|
||||
<polygon points="20,135 126,75 126,155 20,215" fill="url(#containerFront)"/>
|
||||
<line x1="28" y1="148" x2="118" y2="92" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="28" y1="173" x2="118" y2="117" stroke="#2D7A9E" stroke-width="3"/>
|
||||
|
||||
<!-- Upper right container -->
|
||||
<polygon points="126,75 232,135 232,215 126,155" fill="#3A8AB8"/>
|
||||
<line x1="134" y1="92" x2="224" y2="148" stroke="#2D7A9E" stroke-width="3"/>
|
||||
<line x1="134" y1="117" x2="224" y2="173" stroke="#2D7A9E" stroke-width="3"/>
|
||||
|
||||
<!-- Roof -->
|
||||
<polygon points="126,0 20,70 20,135 126,75" fill="url(#roofGrad)"/>
|
||||
<polygon points="126,0 232,70 232,135 126,75" fill="#4AA8D8"/>
|
||||
<polygon points="126,0 20,70 126,75 232,70" fill="#7DD0F0"/>
|
||||
<!-- Roof ridges -->
|
||||
<line x1="30" y1="80" x2="118" y2="35" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="30" y1="100" x2="118" y2="55" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="134" y1="35" x2="222" y2="80" stroke="#2D7A9E" stroke-width="2"/>
|
||||
<line x1="134" y1="55" x2="222" y2="100" stroke="#2D7A9E" stroke-width="2"/>
|
||||
|
||||
<!-- Weather vane -->
|
||||
<line x1="126" y1="0" x2="126" y2="-35" stroke="#2D3E50" stroke-width="3"/>
|
||||
<!-- Crossbar -->
|
||||
<line x1="100" y1="-25" x2="152" y2="-25" stroke="#2D3E50" stroke-width="3"/>
|
||||
<!-- Terminal arrows -->
|
||||
<polygon points="100,-25 108,-20 108,-30" fill="#2D3E50"/>
|
||||
<polygon points="152,-25 144,-20 144,-30" fill="#2D3E50"/>
|
||||
<!-- Top arrow -->
|
||||
<polygon points="126,-35 120,-28 132,-28" fill="#2D3E50"/>
|
||||
</g>
|
||||
|
||||
<!-- COMPOSE FARM text -->
|
||||
<text x="526" y="500" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="72" font-weight="900" fill="#2D3E50" letter-spacing="4">COMPOSE FARM</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,38 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<title>Compose Farm - SSH keyed access</title>
|
||||
<defs>
|
||||
<linearGradient id="cfBlue16" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2496ED"/>
|
||||
<stop offset="100%" stop-color="#1565C0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cfGreen16" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#66BB6A"/>
|
||||
<stop offset="100%" stop-color="#2E7D32"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Container -->
|
||||
<g>
|
||||
<rect x="70" y="70" width="115" height="90" rx="10" fill="url(#cfBlue16)"/>
|
||||
<rect x="70" y="70" width="14" height="90" rx="6" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="171" y="70" width="14" height="90" rx="6" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="88" y="86" width="78" height="12" rx="4"/>
|
||||
<rect x="88" y="104" width="78" height="12" rx="4"/>
|
||||
<rect x="88" y="122" width="78" height="12" rx="4"/>
|
||||
<rect x="88" y="140" width="78" height="12" rx="4"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- SSH key -->
|
||||
<g>
|
||||
<circle cx="38" cy="118" r="18" fill="url(#cfGreen16)"/>
|
||||
<circle cx="38" cy="118" r="7" fill="#E8F5E9" opacity="0.95"/>
|
||||
<rect x="54" y="110" width="92" height="16" rx="8" fill="url(#cfGreen16)"/>
|
||||
<rect x="112" y="126" width="12" height="16" rx="3" fill="#2E7D32"/>
|
||||
<rect x="130" y="126" width="12" height="24" rx="3" fill="#2E7D32"/>
|
||||
</g>
|
||||
|
||||
<!-- Key insertion hint -->
|
||||
<path d="M62 118 L78 118" stroke="#E8F5E9" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,40 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<title>Compose Farm - reconcile loop</title>
|
||||
<defs>
|
||||
<linearGradient id="cfBlue17" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2496ED"/>
|
||||
<stop offset="100%" stop-color="#1565C0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cfGreen17" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#66BB6A"/>
|
||||
<stop offset="100%" stop-color="#2E7D32"/>
|
||||
</linearGradient>
|
||||
<marker id="arrowGreen17" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#2E7D32"/>
|
||||
</marker>
|
||||
<marker id="arrowBlue17" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#1565C0"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Reconcile arrows -->
|
||||
<g fill="none" stroke-linecap="round" opacity="0.9">
|
||||
<path d="M46 112 A70 70 0 0 1 112 40" stroke="url(#cfGreen17)" stroke-width="6" marker-end="url(#arrowGreen17)"/>
|
||||
<path d="M154 88 A70 70 0 0 1 88 160" stroke="url(#cfBlue17)" stroke-width="6" marker-end="url(#arrowBlue17)"/>
|
||||
</g>
|
||||
|
||||
<!-- Central container -->
|
||||
<g transform="translate(60,78)">
|
||||
<rect x="0" y="0" width="80" height="52" rx="8" fill="url(#cfBlue17)"/>
|
||||
<rect x="0" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="70" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="12" y="10" width="56" height="9" rx="3"/>
|
||||
<rect x="12" y="22" width="56" height="9" rx="3"/>
|
||||
<rect x="12" y="34" width="56" height="9" rx="3"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Apply check -->
|
||||
<path d="M84 105 L96 117 L118 93" fill="none" stroke="#E8F5E9" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0.95"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,47 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<title>Compose Farm - auto migration</title>
|
||||
<defs>
|
||||
<linearGradient id="cfBlue18" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2496ED"/>
|
||||
<stop offset="100%" stop-color="#1565C0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cfGreen18" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#66BB6A"/>
|
||||
<stop offset="100%" stop-color="#2E7D32"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow18" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#2E7D32"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Hosts -->
|
||||
<g fill="#334155" opacity="0.9">
|
||||
<rect x="20" y="55" width="48" height="90" rx="8"/>
|
||||
<rect x="132" y="55" width="48" height="90" rx="8"/>
|
||||
</g>
|
||||
<g fill="#E2E8F0" opacity="0.85">
|
||||
<rect x="30" y="68" width="28" height="10" rx="3"/>
|
||||
<rect x="30" y="86" width="28" height="10" rx="3"/>
|
||||
<rect x="30" y="104" width="28" height="10" rx="3"/>
|
||||
<circle cx="56" cy="130" r="4"/>
|
||||
|
||||
<rect x="142" y="68" width="28" height="10" rx="3"/>
|
||||
<rect x="142" y="86" width="28" height="10" rx="3"/>
|
||||
<rect x="142" y="104" width="28" height="10" rx="3"/>
|
||||
<circle cx="168" cy="130" r="4"/>
|
||||
</g>
|
||||
|
||||
<!-- Migration arrow -->
|
||||
<path d="M72 100 L128 100" stroke="url(#cfGreen18)" stroke-width="6" stroke-linecap="round" marker-end="url(#arrow18)"/>
|
||||
|
||||
<!-- Moving container -->
|
||||
<g transform="translate(88,86)">
|
||||
<rect x="0" y="0" width="28" height="20" rx="4" fill="url(#cfBlue18)"/>
|
||||
<rect x="0" y="0" width="5" height="20" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="23" y="0" width="5" height="20" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="6" y="5" width="16" height="4" rx="2"/>
|
||||
<rect x="6" y="11" width="16" height="4" rx="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,76 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||
<title>Compose Farm - multi-host compose</title>
|
||||
<defs>
|
||||
<linearGradient id="cfBlue15" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#2496ED"/>
|
||||
<stop offset="100%" stop-color="#1565C0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cfGreen15" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#81C784"/>
|
||||
<stop offset="100%" stop-color="#2E7D32"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Field -->
|
||||
<polygon points="16,170 184,170 150,108 50,108" fill="url(#cfGreen15)" opacity="0.35"/>
|
||||
<g stroke="#2E7D32" stroke-width="2" opacity="0.22" stroke-linecap="round">
|
||||
<line x1="34" y1="170" x2="62" y2="108"/>
|
||||
<line x1="70" y1="170" x2="86" y2="108"/>
|
||||
<line x1="100" y1="170" x2="100" y2="108"/>
|
||||
<line x1="130" y1="170" x2="114" y2="108"/>
|
||||
<line x1="166" y1="170" x2="138" y2="108"/>
|
||||
</g>
|
||||
|
||||
<!-- SSH paths -->
|
||||
<g stroke="#334155" stroke-width="3" stroke-dasharray="4 6" opacity="0.75" fill="none" stroke-linecap="round">
|
||||
<path d="M100 132 C82 118 68 114 46 118"/>
|
||||
<path d="M100 132 C118 118 132 114 154 118"/>
|
||||
</g>
|
||||
<g fill="#334155" opacity="0.75">
|
||||
<circle cx="100" cy="132" r="3"/>
|
||||
<circle cx="46" cy="118" r="3"/>
|
||||
<circle cx="154" cy="118" r="3"/>
|
||||
</g>
|
||||
|
||||
<!-- Remote container: left -->
|
||||
<g transform="translate(18,118)">
|
||||
<rect x="0" y="0" width="56" height="34" rx="5" fill="url(#cfBlue15)"/>
|
||||
<rect x="0" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="49" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="9" y="7" width="38" height="6" rx="2"/>
|
||||
<rect x="9" y="15" width="38" height="6" rx="2"/>
|
||||
<rect x="9" y="23" width="38" height="6" rx="2"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Remote container: right -->
|
||||
<g transform="translate(126,118)">
|
||||
<rect x="0" y="0" width="56" height="34" rx="5" fill="url(#cfBlue15)"/>
|
||||
<rect x="0" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="49" y="0" width="7" height="34" rx="3" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="9" y="7" width="38" height="6" rx="2"/>
|
||||
<rect x="9" y="15" width="38" height="6" rx="2"/>
|
||||
<rect x="9" y="23" width="38" height="6" rx="2"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Central container with prompt -->
|
||||
<g transform="translate(58,80)">
|
||||
<rect x="0" y="0" width="84" height="52" rx="7" fill="url(#cfBlue15)"/>
|
||||
<rect x="0" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
|
||||
<rect x="74" y="0" width="10" height="52" rx="4" fill="#0D47A1" opacity="0.9"/>
|
||||
<g fill="#1A7AC7" opacity="0.9">
|
||||
<rect x="12" y="10" width="60" height="9" rx="3"/>
|
||||
<rect x="12" y="22" width="60" height="9" rx="3"/>
|
||||
<rect x="12" y="34" width="60" height="9" rx="3"/>
|
||||
</g>
|
||||
|
||||
<!-- Prompt: >_ -->
|
||||
<g stroke="#F8FAFC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.95">
|
||||
<polyline points="18,18 30,26 18,34"/>
|
||||
<line x1="36" y1="34" x2="58" y2="34"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
@@ -28,8 +29,9 @@ from compose_farm.cli.common import (
|
||||
)
|
||||
from compose_farm.cli.management import _discover_stacks_full
|
||||
from compose_farm.console import MSG_DRY_RUN, console, print_error, print_success
|
||||
from compose_farm.executor import run_compose_on_host, run_on_stacks, run_sequential_on_stacks
|
||||
from compose_farm.executor import run_compose_on_host, run_on_stacks
|
||||
from compose_farm.operations import (
|
||||
build_up_cmd,
|
||||
stop_orphaned_stacks,
|
||||
stop_stray_stacks,
|
||||
up_stacks,
|
||||
@@ -49,6 +51,14 @@ def up(
|
||||
all_stacks: AllOption = False,
|
||||
host: HostOption = None,
|
||||
service: ServiceOption = None,
|
||||
pull: Annotated[
|
||||
bool,
|
||||
typer.Option("--pull", help="Pull images before starting (--pull always)"),
|
||||
] = False,
|
||||
build: Annotated[
|
||||
bool,
|
||||
typer.Option("--build", help="Build images before starting"),
|
||||
] = False,
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Start stacks (docker compose up -d). Auto-migrates if host changed."""
|
||||
@@ -58,9 +68,13 @@ def up(
|
||||
print_error("--service requires exactly one stack")
|
||||
raise typer.Exit(1)
|
||||
# For service-level up, use run_on_stacks directly (no migration logic)
|
||||
results = run_async(run_on_stacks(cfg, stack_list, f"up -d {service}", raw=True))
|
||||
results = run_async(
|
||||
run_on_stacks(
|
||||
cfg, stack_list, build_up_cmd(pull=pull, build=build, service=service), raw=True
|
||||
)
|
||||
)
|
||||
else:
|
||||
results = run_async(up_stacks(cfg, stack_list, raw=True))
|
||||
results = run_async(up_stacks(cfg, stack_list, raw=True, pull=pull, build=build))
|
||||
maybe_regenerate_traefik(cfg, results)
|
||||
report_results(results)
|
||||
|
||||
@@ -161,19 +175,17 @@ def restart(
|
||||
service: ServiceOption = None,
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Restart stacks (down + up). With --service, restarts just that service."""
|
||||
"""Restart running containers (docker compose restart)."""
|
||||
stack_list, cfg = get_stacks(stacks or [], all_stacks, config)
|
||||
if service:
|
||||
if len(stack_list) != 1:
|
||||
print_error("--service requires exactly one stack")
|
||||
raise typer.Exit(1)
|
||||
# For service-level restart, use docker compose restart (more efficient)
|
||||
raw = True
|
||||
results = run_async(run_on_stacks(cfg, stack_list, f"restart {service}", raw=raw))
|
||||
cmd = f"restart {service}"
|
||||
else:
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_sequential_on_stacks(cfg, stack_list, ["down", "up -d"], raw=raw))
|
||||
maybe_regenerate_traefik(cfg, results)
|
||||
cmd = "restart"
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_on_stacks(cfg, stack_list, cmd, raw=raw))
|
||||
report_results(results)
|
||||
|
||||
|
||||
@@ -184,19 +196,8 @@ def update(
|
||||
service: ServiceOption = None,
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Update stacks. Only recreates containers if images changed."""
|
||||
stack_list, cfg = get_stacks(stacks or [], all_stacks, config)
|
||||
if service:
|
||||
if len(stack_list) != 1:
|
||||
print_error("--service requires exactly one stack")
|
||||
raise typer.Exit(1)
|
||||
cmd = f"up -d --pull always --build {service}"
|
||||
else:
|
||||
cmd = "up -d --pull always --build"
|
||||
raw = len(stack_list) == 1
|
||||
results = run_async(run_on_stacks(cfg, stack_list, cmd, raw=raw))
|
||||
maybe_regenerate_traefik(cfg, results)
|
||||
report_results(results)
|
||||
"""Update stacks (pull + build + up). Shorthand for 'up --pull --build'."""
|
||||
up(stacks=stacks, all_stacks=all_stacks, service=service, pull=True, build=True, config=config)
|
||||
|
||||
|
||||
def _discover_strays(cfg: Config) -> dict[str, list[str]]:
|
||||
@@ -393,10 +394,10 @@ def compose(
|
||||
else:
|
||||
target_host = hosts[0]
|
||||
|
||||
# Build the full compose command
|
||||
# Build the full compose command (quote args to preserve spaces)
|
||||
full_cmd = command
|
||||
if args:
|
||||
full_cmd += " " + " ".join(args)
|
||||
full_cmd += " " + " ".join(shlex.quote(arg) for arg in args)
|
||||
|
||||
# Run with raw=True for proper TTY handling (progress bars, interactive)
|
||||
result = run_async(run_compose_on_host(cfg, resolved_stack, target_host, full_cmd, raw=True))
|
||||
@@ -406,5 +407,9 @@ def compose(
|
||||
raise typer.Exit(result.exit_code)
|
||||
|
||||
|
||||
# Alias: cf a = cf apply
|
||||
app.command("a", hidden=True)(apply)
|
||||
# Aliases (hidden from help, shown in --help as "Aliases: ...")
|
||||
app.command("a", hidden=True)(apply) # cf a = cf apply
|
||||
app.command("r", hidden=True)(restart) # cf r = cf restart
|
||||
app.command("u", hidden=True)(update) # cf u = cf update
|
||||
app.command("p", hidden=True)(pull) # cf p = cf pull
|
||||
app.command("c", hidden=True)(compose) # cf c = cf compose
|
||||
|
||||
@@ -659,3 +659,9 @@ def init_network(
|
||||
failed = [r for r in results if not r.success]
|
||||
if failed:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# Aliases (hidden from help)
|
||||
app.command("rf", hidden=True)(refresh) # cf rf = cf refresh
|
||||
app.command("ck", hidden=True)(check) # cf ck = cf check
|
||||
app.command("tf", hidden=True)(traefik_file) # cf tf = cf traefik-file
|
||||
|
||||
@@ -201,3 +201,8 @@ def stats(
|
||||
|
||||
console.print()
|
||||
console.print(_build_summary_table(cfg, state, pending))
|
||||
|
||||
|
||||
# Aliases (hidden from help)
|
||||
app.command("l", hidden=True)(logs) # cf l = cf logs
|
||||
app.command("s", hidden=True)(stats) # cf s = cf stats
|
||||
|
||||
@@ -280,8 +280,11 @@ def parse_external_networks(config: Config, stack: str) -> list[str]:
|
||||
return []
|
||||
|
||||
external_networks: list[str] = []
|
||||
for name, definition in networks.items():
|
||||
for key, definition in networks.items():
|
||||
if isinstance(definition, dict) and definition.get("external") is True:
|
||||
# Networks may have a "name" field, which may differ from the key.
|
||||
# Use it if present, else fall back to the key.
|
||||
name = str(definition.get("name", key))
|
||||
external_networks.append(name)
|
||||
|
||||
return external_networks
|
||||
|
||||
@@ -76,7 +76,7 @@ stacks:
|
||||
# traefik_file: (optional) Auto-generate Traefik file-provider config
|
||||
# ------------------------------------------------------------------------------
|
||||
# When set, compose-farm automatically regenerates this file after
|
||||
# up/down/restart/update commands. Traefik watches this file for changes.
|
||||
# up/down/update commands. Traefik watches this file for changes.
|
||||
#
|
||||
# traefik_file: /opt/compose/traefik/dynamic.d/compose-farm.yml
|
||||
|
||||
|
||||
@@ -185,18 +185,37 @@ def _report_preflight_failures(
|
||||
print_error(f" missing device: {dev}")
|
||||
|
||||
|
||||
def build_up_cmd(
|
||||
*,
|
||||
pull: bool = False,
|
||||
build: bool = False,
|
||||
service: str | None = None,
|
||||
) -> str:
|
||||
"""Build compose 'up' subcommand with optional flags."""
|
||||
parts = ["up", "-d"]
|
||||
if pull:
|
||||
parts.append("--pull always")
|
||||
if build:
|
||||
parts.append("--build")
|
||||
if service:
|
||||
parts.append(service)
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
async def _up_multi_host_stack(
|
||||
cfg: Config,
|
||||
stack: str,
|
||||
prefix: str,
|
||||
*,
|
||||
raw: bool = False,
|
||||
pull: bool = False,
|
||||
build: bool = False,
|
||||
) -> list[CommandResult]:
|
||||
"""Start a multi-host stack on all configured hosts."""
|
||||
host_names = cfg.get_hosts(stack)
|
||||
results: list[CommandResult] = []
|
||||
compose_path = cfg.get_compose_path(stack)
|
||||
command = f"docker compose -f {compose_path} up -d"
|
||||
command = f"docker compose -f {compose_path} {build_up_cmd(pull=pull, build=build)}"
|
||||
|
||||
# Pre-flight checks on all hosts
|
||||
for host_name in host_names:
|
||||
@@ -269,6 +288,8 @@ async def _up_single_stack(
|
||||
prefix: str,
|
||||
*,
|
||||
raw: bool,
|
||||
pull: bool = False,
|
||||
build: bool = False,
|
||||
) -> CommandResult:
|
||||
"""Start a single-host stack with migration support."""
|
||||
target_host = cfg.get_hosts(stack)[0]
|
||||
@@ -297,7 +318,7 @@ async def _up_single_stack(
|
||||
|
||||
# Start on target host
|
||||
console.print(f"{prefix} Starting on [magenta]{target_host}[/]...")
|
||||
up_result = await _run_compose_step(cfg, stack, "up -d", raw=raw)
|
||||
up_result = await _run_compose_step(cfg, stack, build_up_cmd(pull=pull, build=build), raw=raw)
|
||||
|
||||
# Update state on success, or rollback on failure
|
||||
if up_result.success:
|
||||
@@ -316,24 +337,101 @@ async def _up_single_stack(
|
||||
return up_result
|
||||
|
||||
|
||||
async def _up_stack_simple(
|
||||
cfg: Config,
|
||||
stack: str,
|
||||
*,
|
||||
raw: bool = False,
|
||||
pull: bool = False,
|
||||
build: bool = False,
|
||||
) -> CommandResult:
|
||||
"""Start a single-host stack without migration (parallel-safe)."""
|
||||
target_host = cfg.get_hosts(stack)[0]
|
||||
|
||||
# Pre-flight check
|
||||
preflight = await check_stack_requirements(cfg, stack, target_host)
|
||||
if not preflight.ok:
|
||||
_report_preflight_failures(stack, target_host, preflight)
|
||||
return CommandResult(stack=stack, exit_code=1, success=False)
|
||||
|
||||
# Run with streaming for parallel output
|
||||
result = await run_compose(cfg, stack, build_up_cmd(pull=pull, build=build), raw=raw)
|
||||
if raw:
|
||||
print()
|
||||
if result.interrupted:
|
||||
raise OperationInterruptedError
|
||||
|
||||
# Update state on success
|
||||
if result.success:
|
||||
set_stack_host(cfg, stack, target_host)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def up_stacks(
|
||||
cfg: Config,
|
||||
stacks: list[str],
|
||||
*,
|
||||
raw: bool = False,
|
||||
pull: bool = False,
|
||||
build: bool = False,
|
||||
) -> list[CommandResult]:
|
||||
"""Start stacks with automatic migration if host changed."""
|
||||
"""Start stacks with automatic migration if host changed.
|
||||
|
||||
Stacks without migration run in parallel. Migration stacks run sequentially.
|
||||
"""
|
||||
# Categorize stacks
|
||||
multi_host: list[str] = []
|
||||
needs_migration: list[str] = []
|
||||
simple: list[str] = []
|
||||
|
||||
for stack in stacks:
|
||||
if cfg.is_multi_host(stack):
|
||||
multi_host.append(stack)
|
||||
else:
|
||||
target = cfg.get_hosts(stack)[0]
|
||||
current = get_stack_host(cfg, stack)
|
||||
if current and current != target:
|
||||
needs_migration.append(stack)
|
||||
else:
|
||||
simple.append(stack)
|
||||
|
||||
results: list[CommandResult] = []
|
||||
total = len(stacks)
|
||||
|
||||
try:
|
||||
for idx, stack in enumerate(stacks, 1):
|
||||
prefix = f"[dim][{idx}/{total}][/] [cyan]\\[{stack}][/]"
|
||||
# Simple stacks: run in parallel (no migration needed)
|
||||
if simple:
|
||||
use_raw = raw and len(simple) == 1
|
||||
simple_results = await asyncio.gather(
|
||||
*[
|
||||
_up_stack_simple(cfg, stack, raw=use_raw, pull=pull, build=build)
|
||||
for stack in simple
|
||||
]
|
||||
)
|
||||
results.extend(simple_results)
|
||||
|
||||
# Multi-host stacks: run in parallel
|
||||
if multi_host:
|
||||
multi_results = await asyncio.gather(
|
||||
*[
|
||||
_up_multi_host_stack(
|
||||
cfg, stack, f"[cyan]\\[{stack}][/]", raw=raw, pull=pull, build=build
|
||||
)
|
||||
for stack in multi_host
|
||||
]
|
||||
)
|
||||
for result_list in multi_results:
|
||||
results.extend(result_list)
|
||||
|
||||
# Migration stacks: run sequentially for clear output and rollback
|
||||
if needs_migration:
|
||||
total = len(needs_migration)
|
||||
for idx, stack in enumerate(needs_migration, 1):
|
||||
prefix = f"[dim][{idx}/{total}][/] [cyan]\\[{stack}][/]"
|
||||
results.append(
|
||||
await _up_single_stack(cfg, stack, prefix, raw=raw, pull=pull, build=build)
|
||||
)
|
||||
|
||||
if cfg.is_multi_host(stack):
|
||||
results.extend(await _up_multi_host_stack(cfg, stack, prefix, raw=raw))
|
||||
else:
|
||||
results.append(await _up_single_stack(cfg, stack, prefix, raw=raw))
|
||||
except OperationInterruptedError:
|
||||
raise KeyboardInterrupt from None
|
||||
|
||||
|
||||
@@ -551,7 +551,6 @@ function playFabIntro() {
|
||||
let commands = [];
|
||||
let filtered = [];
|
||||
let selected = 0;
|
||||
let originalTheme = null; // Store theme when palette opens for preview/restore
|
||||
|
||||
const post = (url) => () => htmx.ajax('POST', url, {swap: 'none'});
|
||||
const nav = (url, afterNav) => () => {
|
||||
@@ -575,20 +574,21 @@ function playFabIntro() {
|
||||
}
|
||||
htmx.ajax('POST', `/api/${endpoint}`, {swap: 'none'});
|
||||
};
|
||||
// Get saved theme from localStorage (source of truth)
|
||||
const getSavedTheme = () => localStorage.getItem(THEME_KEY) || 'dark';
|
||||
|
||||
// Apply theme and save to localStorage
|
||||
const setTheme = (theme) => () => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
};
|
||||
// Preview theme without saving (for hover)
|
||||
// Preview theme without saving (for hover). Guards against undefined/invalid themes.
|
||||
const previewTheme = (theme) => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
if (theme) document.documentElement.setAttribute('data-theme', theme);
|
||||
};
|
||||
// Restore original theme (when closing without selection)
|
||||
// Restore theme from localStorage (source of truth)
|
||||
const restoreTheme = () => {
|
||||
if (originalTheme) {
|
||||
document.documentElement.setAttribute('data-theme', originalTheme);
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', getSavedTheme());
|
||||
};
|
||||
// Generate color swatch HTML for a theme
|
||||
const themeSwatch = (theme) => `<span class="flex gap-0.5" data-theme="${theme}"><span class="w-2 h-4 rounded-l bg-primary"></span><span class="w-2 h-4 bg-secondary"></span><span class="w-2 h-4 bg-accent"></span><span class="w-2 h-4 rounded-r bg-neutral"></span></span>`;
|
||||
@@ -628,7 +628,7 @@ function playFabIntro() {
|
||||
stackCmd('Down', 'Stop', 'down', icons.square),
|
||||
stackCmd('Restart', 'Restart', 'restart', icons.rotate_cw),
|
||||
stackCmd('Pull', 'Pull', 'pull', icons.cloud_download),
|
||||
stackCmd('Update', 'Pull + restart', 'update', icons.refresh_cw),
|
||||
stackCmd('Update', 'Pull + recreate', 'update', icons.refresh_cw),
|
||||
stackCmd('Logs', 'View logs for', 'logs', icons.file_text),
|
||||
);
|
||||
|
||||
@@ -721,26 +721,24 @@ function playFabIntro() {
|
||||
// Scroll selected item into view
|
||||
const sel = list.querySelector(`[data-idx="${selected}"]`);
|
||||
if (sel) sel.scrollIntoView({ block: 'nearest' });
|
||||
// Preview theme if selected item is a theme command
|
||||
// Preview theme if selected item is a theme command, otherwise restore saved
|
||||
const selectedCmd = filtered[selected];
|
||||
if (selectedCmd?.themeId) {
|
||||
previewTheme(selectedCmd.themeId);
|
||||
} else if (originalTheme) {
|
||||
// Restore original when navigating away from theme commands
|
||||
previewTheme(originalTheme);
|
||||
} else {
|
||||
restoreTheme();
|
||||
}
|
||||
}
|
||||
|
||||
function open(initialFilter = '') {
|
||||
// Store original theme for preview/restore
|
||||
originalTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
buildCommands();
|
||||
selected = 0;
|
||||
input.value = initialFilter;
|
||||
filter();
|
||||
// If opening theme picker, select current theme
|
||||
if (initialFilter.startsWith('theme:')) {
|
||||
const currentIdx = filtered.findIndex(c => c.themeId === originalTheme);
|
||||
const savedTheme = getSavedTheme();
|
||||
const currentIdx = filtered.findIndex(c => c.themeId === savedTheme);
|
||||
if (currentIdx >= 0) selected = currentIdx;
|
||||
}
|
||||
render();
|
||||
@@ -751,10 +749,6 @@ function playFabIntro() {
|
||||
function exec() {
|
||||
const cmd = filtered[selected];
|
||||
if (cmd) {
|
||||
if (cmd.themeId) {
|
||||
// Theme command commits the previewed choice.
|
||||
originalTheme = null;
|
||||
}
|
||||
dialog.close();
|
||||
cmd.action();
|
||||
}
|
||||
@@ -794,19 +788,14 @@ function playFabIntro() {
|
||||
if (a) previewTheme(a.dataset.themeId);
|
||||
});
|
||||
|
||||
// Mouse leaving list restores to selected item's theme (or original)
|
||||
// Mouse leaving list restores to selected item's theme (or saved)
|
||||
list.addEventListener('mouseleave', () => {
|
||||
const cmd = filtered[selected];
|
||||
previewTheme(cmd?.themeId || originalTheme);
|
||||
previewTheme(cmd?.themeId || getSavedTheme());
|
||||
});
|
||||
|
||||
// Restore theme when dialog closes without selection (Escape, backdrop click)
|
||||
dialog.addEventListener('close', () => {
|
||||
if (originalTheme) {
|
||||
restoreTheme();
|
||||
originalTheme = null;
|
||||
}
|
||||
});
|
||||
// Restore theme from localStorage when dialog closes
|
||||
dialog.addEventListener('close', restoreTheme);
|
||||
|
||||
// FAB click to open
|
||||
if (fab) fab.addEventListener('click', () => open());
|
||||
|
||||
@@ -103,8 +103,8 @@ def _is_self_update(stack: str, command: str) -> bool:
|
||||
"""
|
||||
if not CF_WEB_STACK or stack != CF_WEB_STACK:
|
||||
return False
|
||||
# Commands that involve 'down' need SSH: update, restart, down
|
||||
return command in ("update", "restart", "down")
|
||||
# Commands that involve 'down' need SSH: update, down
|
||||
return command in ("update", "down")
|
||||
|
||||
|
||||
async def _run_cli_via_ssh(
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{{ action_btn("Apply", "/api/apply", "primary", "Make reality match config", check()) }}
|
||||
{{ action_btn("Refresh", "/api/refresh", "outline", "Update state from reality", refresh_cw()) }}
|
||||
{{ action_btn("Pull All", "/api/pull-all", "outline", "Pull latest images for all stacks", cloud_download()) }}
|
||||
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks except web (only restarts if changed)", rotate_cw()) }}
|
||||
{{ action_btn("Update All", "/api/update-all", "outline", "Update all stacks except web (only recreates if changed)", rotate_cw()) }}
|
||||
<div class="tooltip" data-tip="Save compose-farm.yaml config file"><button id="save-config-btn" class="btn btn-outline">{{ save() }} Save Config</button></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<!-- Lifecycle -->
|
||||
{{ action_btn("Up", "/api/stack/" ~ name ~ "/up", "primary", "Start stack (docker compose up -d)", play()) }}
|
||||
{{ action_btn("Down", "/api/stack/" ~ name ~ "/down", "outline", "Stop stack (docker compose down)", square()) }}
|
||||
{{ action_btn("Restart", "/api/stack/" ~ name ~ "/restart", "secondary", "Restart stack (down + up)", rotate_cw()) }}
|
||||
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (only restarts if changed)", download()) }}
|
||||
{{ action_btn("Restart", "/api/stack/" ~ name ~ "/restart", "secondary", "Restart running containers", rotate_cw()) }}
|
||||
{{ action_btn("Update", "/api/stack/" ~ name ~ "/update", "accent", "Update to latest (only recreates if changed)", download()) }}
|
||||
|
||||
<div class="divider divider-horizontal mx-0"></div>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from compose_farm.executor import CommandResult
|
||||
from compose_farm.operations import (
|
||||
_migrate_stack,
|
||||
build_discovery_results,
|
||||
build_up_cmd,
|
||||
)
|
||||
|
||||
|
||||
@@ -95,22 +96,47 @@ class TestMigrationCommands:
|
||||
assert pull_idx < build_idx
|
||||
|
||||
|
||||
class TestBuildUpCmd:
|
||||
"""Tests for build_up_cmd helper."""
|
||||
|
||||
def test_basic(self) -> None:
|
||||
"""Basic up command without flags."""
|
||||
assert build_up_cmd() == "up -d"
|
||||
|
||||
def test_with_pull(self) -> None:
|
||||
"""Up command with pull flag."""
|
||||
assert build_up_cmd(pull=True) == "up -d --pull always"
|
||||
|
||||
def test_with_build(self) -> None:
|
||||
"""Up command with build flag."""
|
||||
assert build_up_cmd(build=True) == "up -d --build"
|
||||
|
||||
def test_with_pull_and_build(self) -> None:
|
||||
"""Up command with both flags."""
|
||||
assert build_up_cmd(pull=True, build=True) == "up -d --pull always --build"
|
||||
|
||||
def test_with_service(self) -> None:
|
||||
"""Up command targeting a specific service."""
|
||||
assert build_up_cmd(service="web") == "up -d web"
|
||||
|
||||
def test_with_all_options(self) -> None:
|
||||
"""Up command with all options."""
|
||||
assert (
|
||||
build_up_cmd(pull=True, build=True, service="web") == "up -d --pull always --build web"
|
||||
)
|
||||
|
||||
|
||||
class TestUpdateCommandSequence:
|
||||
"""Tests for update command sequence."""
|
||||
|
||||
def test_update_command_uses_pull_always_and_build(self) -> None:
|
||||
"""Update command should use --pull always --build flags."""
|
||||
# This is a static check of the command sequence in lifecycle.py
|
||||
# The actual command sequence is defined in the update function
|
||||
|
||||
def test_update_delegates_to_up_with_pull_and_build(self) -> None:
|
||||
"""Update command should delegate to up with pull=True and build=True."""
|
||||
source = inspect.getsource(lifecycle.update)
|
||||
|
||||
# Verify the command uses --pull always (only recreates if image changed)
|
||||
assert "--pull always" in source
|
||||
# Verify --build is included for buildable services
|
||||
assert "--build" in source
|
||||
# Verify up -d is used
|
||||
assert "up -d" in source
|
||||
# Verify update calls up with pull=True and build=True
|
||||
assert "up(" in source
|
||||
assert "pull=True" in source
|
||||
assert "build=True" in source
|
||||
|
||||
|
||||
class TestBuildDiscoveryResults:
|
||||
|
||||
@@ -338,6 +338,26 @@ def test_parse_external_networks_missing_compose(tmp_path: Path) -> None:
|
||||
assert networks == []
|
||||
|
||||
|
||||
def test_parse_external_networks_with_name_field(tmp_path: Path) -> None:
|
||||
"""Network with 'name' field uses actual name, not key."""
|
||||
cfg = Config(
|
||||
compose_dir=tmp_path,
|
||||
hosts={"host1": Host(address="192.168.1.10")},
|
||||
stacks={"app": "host1"},
|
||||
)
|
||||
compose_path = tmp_path / "app" / "compose.yaml"
|
||||
_write_compose(
|
||||
compose_path,
|
||||
{
|
||||
"services": {"app": {"image": "nginx"}},
|
||||
"networks": {"default": {"name": "compose-net", "external": True}},
|
||||
},
|
||||
)
|
||||
|
||||
networks = parse_external_networks(cfg, "app")
|
||||
assert networks == ["compose-net"]
|
||||
|
||||
|
||||
class TestExtractWebsiteUrls:
|
||||
"""Test extract_website_urls function."""
|
||||
|
||||
|
||||