@mittwald/container-deploy
v1.0.0
Published
Reusable orchestration and API adapters for building and deploying containerized projects.
Readme
Container Deploy
Reusable orchestration and API adapters for building and deploying containerized projects.
Overview
container-deploy is a TypeScript library that provides a unified interface for orchestrating Docker-based application deployments with the Mittwald API. It handles the complete deployment pipeline including Docker image building, registry management, and service deployment.
The library is organized around a three-tier architecture that cleanly separates concerns and enables flexible composition:
- Entities (Tier 1) – Single-responsibility API wrappers and operations
- Orchestration (Tier 2) – Multi-step workflows coordinating entities
- Consumers (Tier 3) – Applications using either tier depending on needs
This architecture enables both high-level deployment (deployProject()) and fine-grained control over individual operations.
Features
- Modular Architecture – Three-tier design with clear separation of concerns
- Docker Integration – Seamless Docker image building and pushing
- Registry Management – Automatic registry setup and configuration
- Service Deployment – Quick and reliable service deployment
- Secure Credentials – Built-in password generation with special character support
- Duration Handling – Flexible timeout and scheduling utilities
- TypeScript First – Fully typed API for excellent IDE support
- Well Tested – Comprehensive jest test suite included
Installation
npm install container-deployPrerequisites
- Node.js 18+
- TypeScript 5.9+
- Docker (for image building)
- Mittwald API Client credentials
Architecture
Three-Tier Design Pattern
┌─────────────────────────────────────────────────────┐
│ Tier 3: Consumers │
│ (CLI, CI/CD, external tools use either tier below) │
└──────────┬──────────────────────────────────────────┘
│
┌────┴─────────────────────────────────┐
│ │
┌─────▼──────────────────────┐ ┌────────────▼──────────────────┐
│ Tier 2: Orchestration │ │ Tier 1: Entities (Direct) │
│ Multi-step workflows │ │ Single-responsibility ops │
│ (deploy_project.ts, │ │ (registry.ts, domain.ts, │
│ registry_setup.ts) │ │ service.ts, docker.ts) │
└────────────┬───────────────┘ │ │
│ │ Can be used independently │
┌─────▼──────────────────┤ for fine-grained control │
│ │ │
│ Compose & delegate └──────────────────────────────┘
│
┌──────▼─────────────────────────────────────────────────────┐
│ External Dependencies (@mittwald/api-client, Docker CLI) │
└───────────────────────────────────────────────────────────┘Entity Modules (Tier 1)
Pure API wrappers with no orchestration logic. Each handles a single domain:
entities/registry.ts – Container Registry API
- Responsibility: Container registry operations via Mittwald API
- Key Functions:
getProjectRegistry()– Fetch existing registry for a projectcreateRegistry()– Create new registry servicecheckProjectRegistry()– Verify registry exists
- Used By:
registry_setup.ts, direct consumers for registry-only operations
entities/service.ts – Service Deployment API
- Responsibility: Service creation and deployment via Mittwald API
- Key Functions:
deployService()– Deploy a standard service (returns full DeployRes)deployServiceAs(apiClient, projectId, serviceName, serviceConfig, timeout)– Deploy any named service with custom config (returns service ID string)
- Used By:
registry_setup.tsfor registry service,deploy_project.tsfor application service - Example: Registry setup uses
deployServiceAs()to create the container registry service
entities/domain.ts – Domain & Ingress API
- Responsibility: Domain creation and ingress readiness checking
- Key Functions:
createDomain()– Create ingress for a servicewaitForDomainReachable()– Poll until domain IPs assigned and TLS certificate createdwaitForIngressReady()– Semantic alias for clarity in orchestration contextscreateAndWaitForDomain()– Combine domain creation and waiting
- Used By:
registry_setup.tsto expose registry, direct consumers for domain operations
entities/docker.ts – Docker Operations
- Responsibility: Docker image building, checking, and pushing
- Key Functions:
checkDocker()– Verify Docker is installed and runningcheckRailpack()– Verify Railpack build tool is availablelocalDockerBuild()– Build Docker image locallylocalBuildWithRailpack()– Build with Mittwald's Railpack toolbuildDockerImage()– Smart build selection (Railpack if available, else Docker)localDockerPush()– Push image to registry
- Used By:
deploy_project.tsfor image building/pushing
Orchestration Modules (Tier 2)
Multi-step workflows that compose entities to achieve higher-level goals:
orchestration/registry_setup.ts – Complete Registry Orchestration
- Responsibility: Full registry setup pipeline (service → domain → registration)
- Key Function:
setupProjectRegistry()executes 6 steps:- Check if registry already exists
- Create registry service via
service.ts - Create and wait for registry domain via
domain.ts - Wait 2 minutes for DNS/TLS propagation (documented Mittwald platform behavior)
- Register in Mittwald API via
registry.ts - Return registry details
- Uses: All entity modules (registry, service, domain)
- Pattern: Step orchestration with error handling and timeouts
orchestration/deploy_project.ts – Complete Project Deployment
- Responsibility: Full end-to-end deployment (registry → build → push → deploy)
- Key Function:
deployProject()executes sequence:- Validate environment
- Set up project registry via
registry_setup.ts - Build Docker image via
docker.ts - Push image to registry via
docker.ts - Deploy service via
service.ts
- Uses: All entity modules plus registry orchestration
- Entry Point: Primary API exported for consumer use
Quick Start
High-Level: Complete Deployment
import { deployProject, Duration } from 'container-deploy';
// Deploy entire project (registry setup + build + push + service)
const result = await deployProject({
apiClient: myApiClient,
projectId: 'your-project-id',
waitTimeout: Duration.fromSeconds(600),
});
console.log(`✓ Deployed: ${result.serviceName} (${result.deployedServiceId})`);Mid-Level: Custom Registry Setup
import { setupProjectRegistry } from 'container-deploy';
import { buildDockerImage, localDockerPush } from 'container-deploy';
// Set up registry with custom timeout
const registry = await setupProjectRegistry(
apiClient,
projectId,
Duration.fromMinutes(5)
);
// Then handle build/push with your own logic
await buildDockerImage(buildDir, registry.username, registry.password);
await localDockerPush(imageName, registry.host, registry.username, registry.password);Fine-Grained: Compose Entities Directly
import {
checkProjectRegistry,
createRegistry,
createDomain,
waitForDomainReachable,
} from 'container-deploy';
// Check existing registry
const existing = await checkProjectRegistry(apiClient, projectId);
if (!existing) {
// Create registry service
const serviceId = await deployServiceAs(apiClient, projectId, 'registry', {...});
// Expose via domain
const domain = await createDomain(apiClient, serviceId, {...});
await waitForDomainReachable(apiClient, domain.id, Duration.fromMinutes(2));
}Core API Reference
Main Exports
deployProject(options: DeployOptions): Promise<DeployResult>
Complete deployment pipeline: registry setup → Docker build → push → service deployment.
Parameters:
apiClient– Mittwald API v2 client instanceprojectId– UUID of target projectwaitTimeout– Maximum time to wait for operations
Returns:
{
deployedServiceId: string; // ID of deployed service
serviceName: string; // Service name
}setupProjectRegistry(apiClient, projectId, timeout): Promise<RegistryData>
Set up complete registry infrastructure (service + domain + registration).
Parameters:
apiClient– Mittwald API v2 client instanceprojectId– UUID of target projecttimeout– Maximum time to wait for registry readiness
Returns:
{
id: string; // Registry service ID
projectId: string; // Project UUID
host: string; // Registry hostname/domain
username: string; // Registry username
password: string; // Registry password
source: 'existing' | 'created'; // Whether newly created or already existed
}Docker Operations
import { buildDockerImage, localDockerPush, checkDocker } from 'container-deploy';
// Check Docker availability
await checkDocker();
// Build image (auto-selects Railpack or Docker)
await buildDockerImage(buildDir, dockerUsername, dockerPassword);
// Push to registry
await localDockerPush(imageName, registryHost, username, password);Entity APIs
import {
// Registry
getProjectRegistry,
createRegistry,
checkProjectRegistry,
// Service
deployService,
deployServiceAs,
// Domain
createDomain,
waitForDomainReachable,
createAndWaitForDomain,
} from 'container-deploy';
// Example: Create registry domain
const domain = await createAndWaitForDomain(
apiClient,
serviceId,
{ ingressName: 'my-registry', tlsEnabled: true },
Duration.fromMinutes(3)
);Utility Classes
Duration
Flexible duration handling for timeouts and scheduling:
import { Duration } from 'container-deploy';
// Creation methods
const dur1 = Duration.fromSeconds(30);
const dur2 = Duration.fromMilliseconds(5000);
const dur3 = Duration.fromZero();
// Calculations
const combined = dur1.add(dur2);
const futureDate = dur1.fromNow();
const comparison = dur1.compare(dur2);
// Conversion
console.log(dur1.seconds); // 30
console.log(dur1.milliseconds); // 30000Password Generation
import { generatePassword, generatePasswordWithSpecialChars } from 'container-deploy';
// Basic password (32 chars, alphanumeric)
const password = generatePassword();
// With special characters (32 chars, 4 special)
const securePassword = generatePasswordWithSpecialChars(32, 4);Design Patterns & Architecture Notes
1. Single Responsibility Principle
Each entity module handles exactly one domain:
- registry.ts = Mittwald Container Registry API only
- service.ts = Mittwald Service deployment API only
- domain.ts = Mittwald Domain/Ingress API only
- docker.ts = Local Docker operations only
This separation enables:
- Code reuse across different orchestration flows
- Independent testing of each domain
- Clear import dependencies (no circular imports)
- Easy extension with new orchestration workflows
2. Composable Orchestration
Entity operations are stateless and composable. Orchestration modules (registry_setup.ts, deploy_project.ts) coordinate them:
User Request
↓
Orchestration Module ← Decides flow & manages state
↓
├→ Entity 1 ← Pure operation (Get → Transform → Return)
├→ Entity 2 ← Pure operation (Post → Wait → Return)
└→ Entity 3 ← Pure operation (Validate → Transform → Return)
↓
ResultThis allows:
- New orchestration flows without modifying entities
- Testing orchestration logic independently of entities
- Different consumers composing entities differently (e.g., CLI might use registry setup orchestration, CI/CD might compose entities directly)
3. Waiting Patterns
All operations that require polling use waitUntil() helper with exponential backoff:
// Example from domain.ts
await waitUntil(
() => isIngressReady(ingress), // Poll condition
Duration.fromSeconds(1), // Initial wait
Duration.fromMinutes(3), // Max total time
);Critical known issue: Registry setup includes hardcoded 2-minute wait before API registration (documented Mittwald behavior for DNS/TLS propagation). This prevents race conditions when domain is created but not yet globally available.
4. Type Safety at Boundaries
All public functions are fully typed. Internal helper types are in src/types/index.ts:
export interface RegistryData {
id: string;
projectId: string;
host?: string; // Optional because checkProjectRegistry doesn't populate
username: string;
password: string;
source: 'existing' | 'created';
}
export interface DeployOptions {
apiClient: MittwaldAPIV2Client;
projectId: string;
waitTimeout: Duration;
}The optional host field in RegistryData is intentional – different code paths populate different fields.
Development & Extension
Adding a New Orchestration Flow
- Identify entities needed – Which entity modules are involved?
- Create new orchestration file –
src/orchestration/my_flow.ts - Compose entity imports – Import needed entity functions
- Define flow function – Export main function that coordinates steps
- Update exports – Add to
src/index.ts - Add tests – Mock entity modules in test suite
Example: If you need "registry + service without domain":
// src/orchestration/registry_and_service_setup.ts
import { createRegistry } from '../entities/registry';
import { deployServiceAs } from '../entities/service';
export async function setupRegistryAndService(
apiClient: MittwaldAPIV2Client,
projectId: string,
timeout: Duration
): Promise<{ registry: RegistryData; serviceId: string }> {
const registry = await createRegistry(apiClient, projectId);
const serviceId = await deployServiceAs(apiClient, projectId, 'app', {...});
return { registry, serviceId };
}Adding New Entity Operations
- Identify domain – Does it belong in existing module or new one?
- Add function – Export from entity module, keep pure (no multi-step logic)
- Type all parameters – Leverage MittwaldAPIV2Client types
- Document side effects – Comment on API calls, state changes, waiting
- Test independently – Mock API client in test suite
Testing Strategy
- Unit tests – Entity functions with mocked API client
- Integration tests – Orchestration functions with mocked entity functions
- End-to-end tests – Full flows against real API (in CI with proper credentials)
Current test suite validates primary deployProject() flow with mocked dependencies. See test/deploy.test.ts for patterns.
Known Limitations & Considerations
- Registry DNS Propagation – 2-minute hardcoded wait in
registry_setup.tsis necessary for Mittwald platform - Docker Availability – Operations in
docker.tsrequire local Docker installation - File:// Dependencies – When using in another package, ensure proper dependency management (Yarn 3.x tracks via content hash)
- Timeout Heuristics – Durations in orchestration are conservative estimates; adjust based on actual deployment patterns
Contributing
This package follows TypeScript strict mode and Jest testing conventions. Before committing:
npm run build # Compile TypeScript
npm run test # Run test suite (jest)
npm run lint # Type check (tsc --noEmit)All new features should include:
- Type definitions in
src/types/index.tsif needed - Implementation in appropriate entity or orchestration module
- Tests in
test/ - Documentation in this README
Project Structure
src/
├── entities/ # Core domain entities
│ ├── docker.ts # Docker build configuration
│ ├── project.ts # Project metadata
│ ├── registry.ts # Registry setup and image operations
│ ├── repository.ts # Repository validation
│ └── service.ts # Service deployment logic
├── orchestration/ # High-level orchestration
│ └── deploy_project.ts # Main deployment orchestrator
├── types/ # TypeScript type definitions
│ └── index.ts # Core types (DeployOptions, DeployResult, etc.)
└── utils/ # Utility functions
└── helpers.ts # Duration, password generation, etc.
test/
└── deploy.test.ts # Integration tests for deployProjectDevelopment
Setup
npm install
npm run buildBuilding
# One-time build
npm run build
# Watch mode for development
npm run build:watchTesting
# Run all tests
npm test
# Watch mode
npm test -- --watchTests are configured with Jest and ts-jest for TypeScript support. Current test coverage includes full integration tests for the deployProject function.
Development Branches
This package can be tested directly from git branches before publishing to npm:
"@mittwald/container-deploy": "github:mittwald/container-deploy#branch-name"For this to work, the dist folder must be committed after building. When released to npm, users receive the published version and don't need to worry about this.
Type Definitions
DeployOptions
{
apiClient: any; // Mittwald API client
projectId: string; // Project UUID
waitTimeout: Duration; // Deployment timeout
}DeployResult
{
deployedServiceId: string; // ID of deployed service
serviceName: string; // Name of deployed service
}RegistryData
{
username: string;
password: string;
uri: string;
host: string;
registryServiceId: string;
registry: any;
created?: boolean;
}RepositoryData
{
buildContext: string;
ports: string[];
dockerfilePath?: string;
dockerfileContent?: string;
dockerfileCreated?: boolean;
imageId?: string;
imageName?: string;
railpackPlanPath?: string | null;
}Configuration
The library uses sensible defaults:
- Default password length: 32 characters
- Special characters in passwords: ~12.5% of password length
- Allowed special characters:
%,_,-,+,&
Architecture
The deployment pipeline follows these steps:
- Project Lookup – Convert project UUID to short ID
- Environment Validation – Check Docker and Railpack availability
- Registry Setup – Create/configure container registry
- Repository Validation – Check local repository state
- Image Building – Build Docker image with buildkit
- Image Push – Push to configured registry
- Service Deployment – Deploy service to Mittwald infrastructure
License
MIT License – see LICENSE file for details
Author
Lars Bergmann [email protected]
For more information or issues, please visit the GitHub repository.
