NexaDock Engine
Enterprise-grade Docker deployment engine and dynamic subdomain reverse proxy. Self-hosted, type-safe, built for speed.
What is NexaDock?
NexaDock is a self-hosted platform that combines a Docker container management API with a dynamic subdomain reverse proxy. Every container you deploy instantly gets its own URL — no nginx config files, no manual DNS.
Instant Deployment
Pull, create, start, and proxy a container with a single API call or from the dashboard console.
Dynamic Subdomain Proxy
Every container on the deploy-engine network gets a name.dockplay.mywire.org:80 URL automatically.
Full Lifecycle Control
Start, stop, pause, resume, inspect, and delete containers — with confirmation dialogs on destructive actions.
Real-time Metrics
Monitor CPU, memory, and network I/O for running containers from the metrics dashboard tab.
Technology Stack
| Technology | Role | Version |
|---|---|---|
| Bun | Runtime, test runner, package manager | v1.3+ |
| TypeScript | Type-safe backend language | v5+ |
| Express v5 | HTTP API & view server | v5.x |
| Dockerode | Docker daemon socket client | v4 |
| EJS | Server-side HTML templating | v3 |
| http-proxy-middleware | Reverse proxy engine | v3 |
| swagger-ui-express | OpenAPI documentation UI | latest |
Service Endpoints
| URL | Description |
|---|---|
http://dockplay.mywire.org:80/ | Landing page |
http://dockplay.mywire.org:80/app | Dashboard console |
http://dockplay.mywire.org:80/docs | This documentation |
http://dockplay.mywire.org:80/api-docs | Swagger OpenAPI UI |
http://[container].dockplay.mywire.org:80 | Dynamic subdomain proxy |
Quick Start
Get NexaDock running locally in under 2 minutes.
Prerequisites: Docker Desktop (or Docker Engine) installed and running. Bun v1.3+ for local development.
Option A — Docker Compose (Recommended)
1. Clone the repository
2. Start with Docker Compose
This builds the image, creates the deploy-engine bridge network, and starts dockpoly-engine container. Visit http://dockplay.mywire.org:80.
Option B — Local Development
1. Install dependencies
2. Start development server
3. Run tests
4. Rebuild after changes
Verify it's working
You should receive a JSON array of containers. An empty array [] is valid — it means no containers are running on the deploy-engine network yet.
Dashboard at http://dockplay.mywire.org:80/app — ready to deploy your first container!
Architecture
NexaDock runs two Express servers inside one Docker container — a public proxy and an internal management API.
Port Map
| Port | Service | Access |
|---|---|---|
4000 | Reverse Proxy (proxy.ts) | Public — browser |
3000 | Management API (index.ts) | Internal only — proxy forwards |
docker.sock | Docker Daemon | Container-internal socket mount |
Project Structure
nexadock/
├── index.ts # Management API server (port 3000)
├── proxy.ts # Reverse proxy server (port 4000)
├── config/
│ ├── app.config.ts # Environment & app config
│ ├── docker.config.ts # Dockerode socket client
│ └── swagger.config.ts # OpenAPI spec & Swagger UI setup
├── routes/
│ └── managementApp.route.ts
├── controller/
│ └── managementApp.controller.ts
├── services/
│ ├── container.service.ts
│ ├── image.service.ts
│ ├── network.service.ts
│ └── volume.service.ts
├── middleware/
│ └── errorHandler.middleware.ts
├── types/ # TypeScript interface definitions
├── views/
│ ├── landing.ejs # Landing page
│ ├── index.ejs # Dashboard console
│ ├── docs.ejs # This documentation
│ └── partials/
│ └── header.ejs # Shared navbar component
├── public/
│ ├── style.css # Global CSS design system
│ ├── app.js # Client-side JS
│ └── favicon.svg # NexaDock logo
└── tests/
└── managementApp.test.ts
Container Management
Full Docker container lifecycle — deploy, control, inspect, and destroy. Every action happens through Dockerode speaking directly to the Docker socket.
How Deployment Works
When you hit POST /container, NexaDock does not just call docker run. It executes a precise multi-step orchestration sequence:
Image Availability Check
Checks if the requested image:tag exists in the local Docker image cache. If not, it automatically pulls from Docker Hub — streaming progress to the response.
Container Creation
Calls docker.createContainer() via Dockerode with the full config — name, image, environment variables, port bindings, AutoRemove flag, and labels.
Network Attachment
Attaches the container to the deploy-engine bridge network via network.connect(). This is what makes the subdomain proxy work — the container gets a stable internal IP.
Container Start
Calls container.start(). The container is now running, reachable at its internal IP, and accessible via [name].dockplay.mywire.org:80.
Container Lifecycle State Machine
Deploy a Container
Full Request Body Schema
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
image | string | required | — | Docker Hub image name e.g. nginx, postgres, node |
tag | string | optional | latest | Image version tag e.g. alpine, 16-slim |
containerName | string | optional | auto-generated | Must be unique. Becomes your subdomain: [name].dockplay.mywire.org:80 |
env | string[] | optional | [] | Environment variables in KEY=VALUE format: ["PORT=3000", "NODE_ENV=production"] |
ports | object[] | optional | [] | Port bindings: [{"hostPort":"8080","containerPort":"80"}] |
autoRemove | boolean | optional | false | Automatically removes the container when it stops. Useful for one-shot tasks. |
Power Control Actions
All power control actions follow POST /container/:identifier/:action. The identifier can be the container name or ID (first 12 chars).
| Action | Command | When to use |
|---|---|---|
start | POST /container/web-demo/start | Resume a stopped container |
stop | POST /container/web-demo/stop | Gracefully stop a running container (SIGTERM) |
pause | POST /container/web-demo/pause | Freeze container (SIGSTOP) — keeps memory, stops CPU |
unpause | POST /container/web-demo/unpause | Resume a paused container (SIGCONT) |
Delete Container
Naming Best Practices
✅ Good Names
- Short, lowercase, hyphenated:
web-api - Descriptive:
postgres-db,redis-cache - Versioned:
api-v2
❌ Avoid
- Spaces or special characters:
my app - Starting with numbers:
1web - Too long: breaks URL readability
Destructive. Container deletion is permanent and immediate. The dashboard shows a confirmation dialog before executing. Use force=true to skip the graceful stop step.
Images & Registry Hub
Pull, inspect, search Docker Hub, and delete images — all through REST or the dashboard.
Search Docker Hub (Auto-complete)
Used by the dashboard's deploy modal for real-time image search with star counts and official image badges.
{
"results": [
{
"name": "postgres",
"description": "The PostgreSQL object-relational database system",
"star_count": 14000,
"is_official": true,
"is_automated": false,
"pull_count": 1000000000
}
]
}
List Local Cached Images
Inspect Full Image Metadata (A-Z)
Returns architecture, OS, creation date, layer digests, size, environment variables, labels, exposed ports, entrypoint, and more.
Delete Local Image
An image cannot be deleted while any container (even stopped) is using it. Remove dependent containers first.
Dynamic Subdomain Reverse Proxy
Every deployed container instantly gets a name.dockplay.mywire.org:80 URL with zero configuration.
How it Works
Request → http://web-demo.dockplay.mywire.org:80/api/users
↓
proxy.ts parses Host header → "web-demo.dockplay.mywire.org"
↓
Extracts subdomain → "web-demo"
↓
docker.getContainer("web-demo").inspect()
↓
Finds container on "deploy-engine" bridge network
↓
Resolves internal IP → 172.18.0.4
↓
http-proxy-middleware → http://172.18.0.4:80/api/users
↓
Response returned to browser
Root Domain Routes
| Path | Destination | Renders |
|---|---|---|
/ | Management App | landing.ejs |
/app | Management App | index.ejs (Dashboard) |
/docs | Management App | docs.ejs (This page) |
/api-docs | Management App | Swagger UI |
/container | Management App | REST API JSON |
Accessing a Deployed Container
The container must be on the deploy-engine bridge network. All containers deployed via NexaDock are automatically attached.
Attach an Existing Container to the Network
Then immediately access it via:
Port Auto-selection Logic
The proxy selects the container's exposed port in this priority order:
- Port
80/tcp(standard HTTP) - Port
3000or8080 - Any non-443 port
- First exposed port
- Falls back to
80
Networks
Monitor, inspect, and manage Docker bridge networks from the dashboard or REST API.
List All Networks
Delete a Network
Critical: Never delete the deploy-engine network — it is required for the subdomain proxy to resolve container IPs. Deleting it will break all subdomain routing until the network is recreated and containers reconnected.
Recreate the deploy-engine Network
If accidentally deleted, recreate it with:
Storage Volumes
Monitor persistent Docker volumes and safely clean up unused storage.
List All Volumes
Delete a Volume
Volumes can only be deleted when they are not mounted by any container (running or stopped). Stop and remove dependent containers first. Volume deletion is permanent — all data inside will be lost.
REST API Reference
Complete endpoint reference. See Swagger UI for interactive testing.
Containers
| Method | Endpoint | Description |
|---|---|---|
| GET | /container | List all containers on deploy-engine network |
| POST | /container | Deploy & start a new container |
| GET | /container/:id | Inspect container details |
| POST | /container/:id/start | Start stopped container |
| POST | /container/:id/stop | Stop running container |
| POST | /container/:id/pause | Pause running container |
| POST | /container/:id/unpause | Resume paused container |
| DELETE | /container/:id | Force delete container |
Images
| Method | Endpoint | Description |
|---|---|---|
| GET | /image | List local cached images |
| GET | /image/search?q= | Search Docker Hub |
| GET | /image/inspect/:id | Full A-Z image metadata |
| DELETE | /image/:id | Delete local image |
Networks & Volumes
| Method | Endpoint | Description |
|---|---|---|
| GET | /network | List all Docker networks |
| DELETE | /network/:id | Delete a network |
| GET | /volume | List all storage volumes |
| DELETE | /volume/:id | Delete a volume |
CLI Commands
All essential shell commands — click any block to copy.
Docker Compose
Container Operations
Image Operations
Network & Volume
Testing & Validation
Unit and integration test suite with 100% service-layer coverage.
Run Tests
Expected Output
bun test v1.3.14
tests/managementApp.test.ts:
(pass) should list Docker containers active on internal network
(pass) should list local cached Docker images
(pass) should list Docker networks including deploy-engine bridge
(pass) should list Docker storage volumes
(pass) should search Docker Hub for official image auto-complete
5 pass
0 fail
45 expect() calls
Ran 5 tests across 1 file. [2.23s]
TypeScript Type Check
Zero TypeScript errors expected on a clean build.
Always run both bun test and bun x tsc --noEmit before pushing to production. Both should pass with zero failures or errors.
.env Reference
Complete guide to every environment variable — what it does, when to change it, and whether to commit it.
Should You Commit .env?
Never commit .env to git for production deployments. It may contain secrets (API keys, DB passwords, tokens). Add .env to .gitignore immediately.
| Scenario | Use .env? | Reason |
|---|---|---|
| Local development | ✅ Yes | Convenient for local overrides. Never leave the machine. |
| Docker Compose local | ✅ Yes | docker-compose.yml reads it via env_file: .env. |
| Production / CI/CD | ❌ No | Inject variables via platform secrets or CI environment variables instead. |
| Git repository | ❌ Never | Exposes all secrets publicly. Use .env.example instead. |
| Docker image build | ⚠️ Caution | Don't bake secrets into layers. Use runtime injection. |
Safe Practice — .env.example
Commit a template file with no real values. Other developers copy it and fill in their own values:
# .env.example — Safe to commit. Contains no real secrets.
PORT=4000
MANAGEMENT_PORT=3000
MANAGEMENT_HOST=127.0.0.1
DOMAIN=localhost
DOCKER_NETWORK=deploy-engine
NODE_ENV=production
Current .env — All Variables
| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
4000 |
optional | The public-facing port for the reverse proxy server (proxy.ts). This is the port you open in your browser. Exposed by Docker Compose as HOST:CONTAINER. |
MANAGEMENT_PORT |
3000 |
optional | Internal port for the management API server (index.ts). Never exposed to the host — only the proxy communicates with it. |
MANAGEMENT_HOST |
127.0.0.1 |
optional | The hostname the proxy uses to reach the management server. Keep as 127.0.0.1 (loopback). Only change if running as separate containers. |
DOMAIN |
localhost |
optional | The root domain used by the proxy to distinguish direct requests from subdomain requests. Change to yourdomain.com when deploying publicly. Subdomain URLs become [container].yourdomain.com:PORT. |
DOCKER_NETWORK |
deploy-engine |
optional | Name of the Docker bridge network used for container-to-container routing and subdomain IP resolution. Must match the network name in docker-compose.yml. Changing this requires recreating the network. |
NODE_ENV |
production |
optional | Runtime environment. Set to development locally for verbose logging. Set to production for Docker deployments. |
How Variables Are Loaded
NexaDock reads environment variables directly from process.env via config/app.config.ts. All variables have hardcoded fallback defaults, meaning the app works even without any .env file:
// config/app.config.ts
export const config = {
port: Number(process.env.PORT) || 4000,
managementPort: Number(process.env.MANAGEMENT_PORT) || 3000,
managementHost: process.env.MANAGEMENT_HOST || '127.0.0.1',
domain: process.env.DOMAIN || 'localhost',
dockerNetwork: process.env.DOCKER_NETWORK || 'deploy-engine',
nodeEnv: process.env.NODE_ENV || 'production',
};
Docker Compose Injection
docker-compose.yml reads .env via env_file and also injects variables individually with fallback syntax:
services:
app:
env_file:
- .env # <-- reads the .env file
environment:
- PORT=${PORT:-4000} # fallback if not in .env
- MANAGEMENT_PORT=${MANAGEMENT_PORT:-3000}
- MANAGEMENT_HOST=${MANAGEMENT_HOST:-127.0.0.1}
- DOMAIN=${DOMAIN:-localhost}
- DOCKER_NETWORK=${DOCKER_NETWORK:-deploy-engine}
- NODE_ENV=${NODE_ENV:-production}
Adding .env to .gitignore
Summary: Use .env locally and with Docker Compose. Never commit it. Commit .env.example instead. In production CI/CD, inject variables as platform secrets or Docker secrets — never as files in the image.
Configuration
All environment variables and configuration options for NexaDock.
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT | 4000 | Public reverse proxy port |
MANAGEMENT_PORT | 3000 | Internal management API port |
MANAGEMENT_HOST | 127.0.0.1 | Internal management API host |
DOCKER_NETWORK | deploy-engine | Bridge network for subdomain routing |
DOMAIN | localhost | Root domain for proxy routing |
docker-compose.yml
services:
app:
build: .
container_name: dockpoly-engine
ports:
- "4000:4000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- deploy-engine
networks:
deploy-engine:
driver: bridge
name: deploy-engine
The Docker socket mount (/var/run/docker.sock) is what allows NexaDock to control the host Docker daemon from inside the container. This is required for all container management functionality.