hop3-server Deep Dive¶
This document provides detailed internal documentation for the hop3-server package. For a quick overview, see the package overview.
Architecture Overview¶
hop3-server is the central orchestrator for the Hop3 PaaS. It handles:
- API Layer - JSON-RPC over HTTP via Litestar
- Command Handlers - Business logic for each operation
- Deployment Pipeline - Build and deploy applications
- Plugin System - Extensible architecture via pluggy
- Process Management - uWSGI configuration and lifecycle
- Proxy Configuration - Nginx/Caddy/Traefik setup
Module Structure¶
Server Layer (hop3/server/)¶
The ASGI application built on Litestar:
server/
├── asgi.py # Application factory
├── cli/ # Server CLI (serve, db, git, plugins, setup, ...)
├── controllers/
│ ├── auth.py # Authentication endpoints
│ ├── dashboard/ # Dashboard API
│ ├── rpc.py # JSON-RPC endpoint
│ ├── stream.py # Streaming / log-tail endpoint
│ └── root.py # Root routes
├── security/
│ ├── tokens.py # JWT token handling
│ └── rate_limit.py # Request rate limiting
├── middleware/ # ASGI middleware
└── lib/
├── database.py # DB session helpers
└── scanner.py # Command / plugin auto-discovery
Command Layer (hop3/commands/)¶
RPC command handlers implementing business logic:
commands/
├── app.py # Single-app operations (logs, deploy, restart, ...)
├── apps.py # App listing / lifecycle across all apps
├── auth.py # Authentication commands
├── config.py # Environment variable management
├── services.py # Addon / service management
├── git.py # Git operations
├── domains.py # Domain management
├── system.py # Server status / info / cleanup
└── misc.py # Utility commands
Each command is a Command subclass, registered with @register and
dispatched by its name tuple (the space-separated CLI path). Dependencies
are declared as dataclass fields and injected at construction:
@register
@dataclass(frozen=True)
class AppsCmd(Command):
"""List all applications."""
db_session: Session
name: ClassVar[tuple[str, ...]] = ("app", "list")
def call(self, *args):
app_repo = AppRepository(session=self.db_session)
...
ORM Layer (hop3/orm/)¶
SQLAlchemy models with Advanced Alchemy patterns:
orm/
├── app.py # App model with deployment logic
├── env.py # EnvVar model
├── backup.py # Backup model
├── security.py # User, Role models
├── addon_credential.py # Addon credentials
├── repositories.py # Repository pattern
└── session.py # Session management
Key model: App:
class App(Base):
name: str
run_state: AppStateEnum # STOPPED, STARTING, RUNNING, STOPPING, FAILED
port: int
runtime: str # e.g. "uwsgi", "docker-compose"
# Paths
@property
def app_path(self) -> Path: ...
@property
def src_path(self) -> Path: ...
@property
def repo_path(self) -> Path: ...
# Lifecycle
def deploy(self) -> None: ...
def start(self) -> None: ...
def stop(self) -> None: ...
Plugin System (hop3/plugins/, hop3/core/)¶
Pluggy-based extensibility:
core/
├── protocols.py # Protocol definitions (Builder, Deployer, Addon, etc.)
├── hookspecs.py # Hook specifications
└── plugins.py # Plugin manager
plugins/
├── build/
│ ├── local_build/ # LocalBuilder
│ ├── nix/ # Nix builder
│ └── language_toolchains/ # Python, Node, etc.
├── docker/ # DockerBuilder
├── deploy/
│ ├── uwsgi/ # UWSGIDeployer
│ └── static/ # StaticDeployer
├── proxy/
│ ├── nginx/ # NginxVirtualHost
│ ├── caddy/ # CaddyVirtualHost
│ └── traefik/ # TraefikVirtualHost
├── postgresql/ # PostgreSQL addon
├── mysql/ # MySQL addon
├── redis/ # Redis addon
├── s3/ # S3/MinIO addon
└── oses/ # OS implementations
Deployment Pipeline (hop3/deployers/)¶
Orchestrates the deployment process:
deployers/
├── deployer.py # Main do_deploy() orchestration
└── git_based_deployer.py # Git-based deployment flow
Deployment stages:
- Receive - Accept git push or tarball upload
- Checkout - Extract source to
src/directory - Build - Run builder (LocalBuilder, DockerBuilder, or Nix builder)
- Configure - Generate uWSGI and proxy configs
- Deploy - Activate the application
- Verify - Health check
Runtime Management (hop3/run/)¶
uWSGI configuration generation:
run/
├── spawn.py # Process spawning
├── reaper.py # Reap leftover vassal processes on teardown
└── uwsgi/
├── settings.py # uWSGI config generation
└── worker.py # Worker management
Key Concepts¶
Application States¶
class AppStateEnum(Enum):
STOPPED = 1 # Application is not running
STARTING = 2 # Application is starting up (transitional)
RUNNING = 3 # Application is running normally
STOPPING = 4 # Application is shutting down (transitional)
FAILED = 5 # Application failed to start or crashed
State transitions:
STOPPED → STARTING → RUNNING
RUNNING → STOPPING → STOPPED
any state → FAILED (on error)
FAILED → STOPPED (manual recovery)
FAILED → STARTING (manual recovery)
Dependency Injection¶
Uses Dishka for DI:
# Providers define how to create services
class DatabaseProvider(Provider):
@provide(scope=Scope.APP)
def engine(self, config: HopConfig) -> Engine:
return create_engine(config.database_url)
# Controllers receive dependencies
class AppController:
def __init__(self, app_repo: AppRepository):
self.app_repo = app_repo
Configuration¶
Configuration sources (in order of precedence):
- Environment variables (
HOP3_*) - Config file (
hop3-server.tomlunder$HOP3_ROOT) - Defaults
Key settings:
| Setting | Env Var | Default |
|---|---|---|
| Root directory | HOP3_ROOT |
/home/hop3 |
| Database URI | HOP3_DATABASE_URI |
sqlite:///$HOP3_ROOT/hop3.db |
| Secret key | HOP3_SECRET_KEY |
(required) |
| Log level | HOP3_LOG_LEVEL |
INFO |
Data Flow¶
Deployment Request¶
sequenceDiagram
participant C as Client
participant S as Server
participant F as Filesystem
C->>S: POST /rpc deploy
S->>F: Create App record
S->>F: Extract source → apps/{name}/src/
S->>F: Run builder → apps/{name}/venv/
S->>F: Generate uWSGI config → uwsgi-available/
S->>F: Generate nginx config → nginx/
S->>F: Symlink to enabled → uwsgi-enabled/
S->>F: Reload nginx
S->>F: Update App state
S-->>C: deployed
Request Routing¶
flowchart LR
Internet --> Nginx --> Emperor["uWSGI Emperor"] --> Worker["App Worker"] --> Response
Nginx --> Static["Static files<br/>(if configured)"]
Testing¶
Testing follows ADR 043. There are three pytest layers under tests/. A test's layer is decided by what it needs (Docker, root, or host mutation), not by how complex it is, so duplication across layers is allowed:
a_unit/- Unit tests; no Docker; counts toward coverage; tierfast.b_integration/- Integration tests; in-process, against a real in-memory database; no Docker; counts toward coverage; tiercheck.c_e2e/- End-to-end tests; real Docker deploys; no coverage; runs in the check tier (Docker) and nightly.
Markers are stamped from the directory by the root conftest.py (fast / integration / e2e / needs_docker), so selection works from anywhere:
uv run pytest -m fast # a_unit (the inner loop)
uv run pytest -m "not needs_docker" # everything except the Docker e2e layer
uv run pytest packages/hop3-server/tests/c_e2e # just the Docker e2e layer
Make targets:
make test-fast # unit, all packages, no Docker (< 1 min)
make test # check tier: a_unit + b_integration, all packages, no Docker
make test-e2e # the Docker e2e layer (c_e2e): real deploys, backups, git-push
make test-cov # coverage on the in-process layers (a_unit + b_integration)
When a Docker e2e or app test fails, a diagnostic bundle is collected; run hop3-test why <run-id> to inspect it.
Key fixtures:
# In-memory database for unit and integration tests
@pytest.fixture
def db_session():
engine = create_engine("sqlite:///:memory:")
...
# Docker container for end-to-end tests
@pytest.fixture(scope="session")
def hop3_container():
...
Performance Considerations¶
- Database: SQLite for single-server, PostgreSQL for scale
- Caching: Nginx proxy cache for static assets
- Process model: uWSGI emperor manages app workers
- Connection pooling: SQLAlchemy connection pool
Security¶
- Authentication: JWT tokens with configurable expiry
- Authorization: Role-based access (admin, user)
- Transport: HTTPS via Nginx, with pluggable certificate engines (self-signed or certbot/Let's Encrypt)
- Secrets: Environment variables, never in code
Debugging¶
Common issues and debugging approaches:
| Symptom | Check |
|---|---|
| App not starting | uwsgi-enabled/<app>.ini, uWSGI emperor logs |
| 502 errors | App health, uWSGI socket, nginx upstream |
| Deploy fails | Build logs in apps/<name>/log/ |
| Database errors | Check migrations, connection string |
Useful commands:
# Check the uWSGI emperor service
systemctl status uwsgi-hop3
# View app logs
tail -f /home/hop3/apps/<name>/log/*.log
# Test nginx config
nginx -t
# Check database
sqlite3 /home/hop3/hop3.db ".tables"
Future Work¶
See the NGI release plans (0.5, 0.6) for the current roadmap, including:
- Web dashboard
- Multi-server support
- Additional addon types
- Improved scaling