ADR 033: Docker Integration Strategy¶
Context¶
Hop3 supports native deployment using uWSGI and nginx. However, some applications require containerization, either because:
- They have complex dependencies that are difficult to install natively
- They need process isolation beyond what uWSGI provides
- They are already packaged as Docker images
- They require multi-container setups (app + database + cache)
The Docker plugin (hop3/plugins/docker/) provides an alternative build and deployment path for containerized applications.
Decision¶
Architecture¶
Docker integration follows the two-level build architecture (ADR 030):
Level 1 (Builder) Level 2 (Toolchain)
┌─────────────────┐ ┌─────────────────────┐
│ LocalBuilder │───────▶│ PythonToolchain │
│ │ │ NodeToolchain │
│ │ │ ... │
└─────────────────┘ └─────────────────────┘
┌─────────────────┐
│ DockerBuilder │ (standalone - no toolchains)
└─────────────────┘
DockerBuilder is a Level 1 Builder that:
- Does NOT use Level 2 toolchains
- Builds directly from a Dockerfile
- Produces docker-image artifacts
Build Flow¶
Source Code DockerBuilder DockerComposeDeployer
┌───────────────┐ ┌─────────────┐ ┌────────────────┐
│ app/ │ │ │ │ │
│ ├─ src/ │──────────▶│ docker │──────────▶│ docker compose │
│ ├─ Dockerfile │ │ build │ │ up -d │
│ └─ compose.yml│ │ │ │ │
└───────────────┘ └─────────────┘ └────────────────┘
│ │
▼ ▼
BuildArtifact DeploymentInfo
kind="docker-image" protocol="http"
location="hop3/app:latest" port=8080
Note: Docker images manage their own runtime environment internally (PATH, env vars, etc. are baked into the image layers). Unlike native builds (ADR 035), DockerBuilder does not need to produce a
RuntimeConfig- the container runtime handles this.
Builder Selection¶
Explicit Selection (Recommended)¶
Users can explicitly choose Docker via hop3.toml:
Available builder names:
- "docker" - Use DockerBuilder (requires Dockerfile)
- "local" - Use LocalBuilder (native toolchains)
- "auto" - Auto-detect (default)
Auto-Detection Rules¶
If builder = "auto" or not specified, the first matching builder wins:
| Builder | Accepts When |
|---|---|
| DockerBuilder | Dockerfile exists in source directory |
| LocalBuilder | Any language toolchain accepts (requirements.txt, package.json, etc.) |
Note: If both Dockerfile and requirements.txt exist, use explicit selection to ensure predictable behavior.
Deployer Selection¶
| Deployer | Accepts When |
|---|---|
| UWSGIDeployer | Artifact kind is virtualenv, node, ruby, etc. |
| StaticDeployer | Artifact kind is static |
| DockerComposeDeployer | Artifact kind is docker-image |
Docker Compose File Generation¶
By default, Hop3 generates a docker-compose.yml based on the Dockerfile. This simplifies deployment and ensures correct port binding.
The generated compose file:
# Generated by Hop3 - do not edit manually
services:
web:
image: ${HOP3_IMAGE_TAG}
ports:
- "127.0.0.1:${PORT:-8080}:8080" # Port from EXPOSE directive
environment:
- PORT=8080
restart: unless-stopped
For advanced use cases (multi-container, volumes, custom networks), users can provide their own docker-compose.yml, compose.yml, or docker-compose.yaml. If a user-supplied compose file exists, Hop3 uses that file and skips generation.
Hop3 provides these environment variables to the compose file:
- HOP3_IMAGE_TAG: The Docker image tag (e.g., hop3/myapp:latest)
- HOP3_APP_NAME: The application name
- HOP3_APP_PORT: The exposed port from Dockerfile (if detected)- PORT: The allocated host port for the container
Host Reference Substitution¶
A container cannot reach a service bound to 127.0.0.1/localhost on the host through that name; localhost inside the container resolves to the container itself. When passing environment values into the container, Hop3 rewrites host references of localhost and 127.0.0.1 to host.docker.internal, the Docker-provided alias for the host.
The substitution is value-based: it matches host references at host boundaries within the value (via regex). A name-based whitelist would silently miss custom variables such as GF_DATABASE_HOST; a value-based rewrite catches any variable that carries a host reference, regardless of its name.
Proxy Integration¶
Decision: Use the Direct Proxy Integration pattern, following StaticDeployer's approach.
This approach:
- Works with any configured proxy (nginx, caddy, traefik)
- Is consistent with existing Hop3 architecture
- Requires no special proxy-specific configuration
- Uses the same get_proxy_strategy() / proxy.setup() flow as other deployers
Architecture¶
┌─────────────────────────────────┐
│ Hop3 Server │
│ │
┌─────────────┐ ┌──────────────────┐ │
│ Proxy │ │ Docker Container │ │
Internet ──▶ Port 80/443 ──▶ │ nginx/caddy │──▶ │ 127.0.0.1:PORT │ │
│ /traefik │ │ (app:8080) │ │
└─────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────┘
The proxy does not care whether the backend is a uWSGI process, a static folder, or a Docker container. It only needs:
1. A HOST_NAME to route requests
2. A BIND_ADDRESS:PORT to forward to
3. Standard environment configuration
Container Port Binding¶
Docker containers MUST bind to the host's loopback interface:
# docker-compose.yml - CORRECT
services:
web:
image: ${HOP3_IMAGE_TAG}
ports:
- "127.0.0.1:${HOP3_HOST_PORT:-8080}:8080" # Host-only binding
Why 127.0.0.1?
- Prevents direct external access to container
- All traffic goes through Hop3's proxy (enabling SSL, rate limiting, etc.)
- Consistent with uWSGI apps that bind to localhost
Environment Variables for Proxy¶
DockerComposeDeployer provides these variables:
| Variable | Purpose | Example |
|---|---|---|
HOP3_IMAGE_TAG |
Docker image to run | hop3/myapp:latest |
HOP3_APP_NAME |
Application name | myapp |
HOP3_HOST_PORT |
Port on host (for proxy) | 8080 |
HOP3_APP_PORT |
Port inside container | 8080 |
HOST_NAME |
Domain name(s) for routing | myapp.example.com |
BIND_ADDRESS |
Always 127.0.0.1 |
127.0.0.1 |
PORT |
Alias for HOP3_HOST_PORT |
8080 |
Implementation in DockerComposeDeployer¶
The deployer follows StaticDeployer's pattern:
def deploy(self, deltas: dict[str, int] | None = None) -> DeploymentInfo:
# 1. Start container
self._start_container()
# 2. Build environment for proxy
env = self._make_proxy_env()
# 3. Setup proxy (if HOST_NAME configured)
host_name = env.get("HOST_NAME", "_")
if host_name and host_name != "_":
proxy = get_proxy_strategy(self.app, env, self._get_workers())
proxy.setup()
# 4. Update app state
self.app._transition_state(AppStateEnum.RUNNING)
return DeploymentInfo(
protocol="http",
address="127.0.0.1",
port=self._get_host_port(),
)
Alternatives Considered¶
- Traefik Labels (Not chosen)
- Pros: Auto-discovery, no manual config
-
Cons: Traefik-only, requires Docker network management, different pattern from other deployers
-
Docker Network Bridge (Not chosen)
- Pros: No port mapping needed, cleaner networking
-
Cons: Requires nginx in container, complex network setup, diverges from native deployment model
-
Direct Proxy Integration (Chosen)
- Pros: Consistent with existing architecture, works with any proxy, simple implementation
- Cons: Requires explicit port mapping in compose file
Lifecycle Management¶
DockerComposeDeployer implements the full Deployer protocol:
| Method | Docker Compose Command |
|---|---|
deploy() |
docker compose up -d --remove-orphans |
start() |
docker compose up -d |
stop() |
docker compose stop |
restart() |
docker compose restart |
destroy() |
docker compose down --volumes |
scale() |
docker compose up -d --scale web=N |
check_status() |
docker compose ps |
Port Discovery¶
Ports are discovered in this order:
EXPOSEdirective in Dockerfile (parsed during build)docker compose portcommand (runtime discovery)- Fallback to 8080
Future Work¶
This design leaves room for the following extensions:
- Build improvements: build arguments derived from environment variables, multi-stage Dockerfiles, BuildKit/buildx features, and layer caching reused across deployments.
- Advanced orchestration: HTTP-endpoint container health checks, zero-downtime blue-green rolling updates, CPU/memory limits expressed via compose, and secrets management integrating Docker secrets or Hop3 secrets.
- Multi-container apps: sidecar services (redis, postgres, etc.), internal DNS for service discovery between containers, and persistent volume management across deployments.
Consequences¶
Positive¶
- Supports applications that require containerization
- Consistent lifecycle management (same API as uWSGI deployer)
- Multi-container applications supported via user-supplied compose
- Proxy integration uses same pattern as other deployers (consistency)
- Works with any configured proxy (nginx, caddy, traefik)
- A Dockerfile is all that is needed; Hop3 generates the compose file
- Correct port binding ensured by the generated compose file
Negative¶
- Requires Docker to be installed on the server
- Port discovery is heuristic-based
- Scaling requires user-supplied compose file
- Generated compose only supports single-container apps
Neutral¶
- Docker apps are isolated from native apps
- Different deployment model than uWSGI (containers vs processes)
Example Usage¶
Minimal Docker App (Compose Generated)¶
For simple single-container apps, just provide a Dockerfile:
myapp/
├── hop3.toml # Optional: explicit builder selection
├── Dockerfile
├── app.py
└── requirements.txt
hop3.toml:
Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "app.py"]
Hop3 automatically generates the docker-compose.yml based on the EXPOSE port.
Deploy:
Docker App with Database (User-Supplied Compose)¶
For multi-container apps, provide your own docker-compose.yml:
myapp/
├── hop3.toml
├── Dockerfile
├── docker-compose.yml # User-supplied for multi-container
├── app.py
└── requirements.txt
# docker-compose.yml - User-supplied for advanced use cases
services:
web:
image: ${HOP3_IMAGE_TAG}
ports:
- "127.0.0.1:${PORT:-8080}:8080"
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
References¶
- ADR 030: Two-Level Build Architecture
- ADR 032: Deployment Strategies & Artifact Lifecycle
- Docker Compose specification: https://docs.docker.com/compose/compose-file/
Related ADRs: ADR 022: Build and Deployment Plugin System, ADR 030: Two-Level Build Architecture, ADR 032: Deployment Strategies and Artifact Lifecycle, ADR 035: Build Artifacts as Runtime Contract