NexaDock Docs
Swagger UI Open Console

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

TechnologyRoleVersion
BunRuntime, test runner, package managerv1.3+
TypeScriptType-safe backend languagev5+
Express v5HTTP API & view serverv5.x
DockerodeDocker daemon socket clientv4
EJSServer-side HTML templatingv3
http-proxy-middlewareReverse proxy enginev3
swagger-ui-expressOpenAPI documentation UIlatest

Service Endpoints

URLDescription
http://dockplay.mywire.org:80/Landing page
http://dockplay.mywire.org:80/appDashboard console
http://dockplay.mywire.org:80/docsThis documentation
http://dockplay.mywire.org:80/api-docsSwagger OpenAPI UI
http://[container].dockplay.mywire.org:80Dynamic 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

git clone https://github.com/your-org/nexadock && cd nexadock

2. Start with Docker Compose

docker compose up -d --build

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

docker compose up -d --build

Verify it's working

curl http://dockplay.mywire.org:80/container

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.

CLIENT BROWSER web-demo.dockplay.mywire.org :4000 HTTP REVERSE PROXY proxy.ts Port 4000 Subdomain resolver Docker IP lookup HTTP proxy routing / /app /docs proxy to 172.18.0.x MGMT ENGINE index.ts Port 3000 REST API /container REST API /image EJS view rendering /var/run/docker.sock Docker Daemon API deploy-engine bridge network web-demo postgres 172.18.0.x

Port Map

PortServiceAccess
4000Reverse Proxy (proxy.ts)Public — browser
3000Management API (index.ts)Internal only — proxy forwards
docker.sockDocker DaemonContainer-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:

1

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.

2

Container Creation

Calls docker.createContainer() via Dockerode with the full config — name, image, environment variables, port bindings, AutoRemove flag, and labels.

3

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.

4

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

Created container created start() Running active + proxied pause() Paused frozen in memory unpause() stop() Stopped not running start() rm() Deleted irreversible force=true

Deploy a Container

curl -X POST http://dockplay.mywire.org:80/container \ -H "Content-Type: application/json" \ -d '{"image":"nginx","tag":"alpine","containerName":"web-demo"}'

Full Request Body Schema

FieldTypeRequiredDefaultDescription
imagestringrequiredDocker Hub image name e.g. nginx, postgres, node
tagstringoptionallatestImage version tag e.g. alpine, 16-slim
containerNamestringoptionalauto-generatedMust be unique. Becomes your subdomain: [name].dockplay.mywire.org:80
envstring[]optional[]Environment variables in KEY=VALUE format: ["PORT=3000", "NODE_ENV=production"]
portsobject[]optional[]Port bindings: [{"hostPort":"8080","containerPort":"80"}]
autoRemovebooleanoptionalfalseAutomatically 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).

ActionCommandWhen to use
startPOST /container/web-demo/startResume a stopped container
stopPOST /container/web-demo/stopGracefully stop a running container (SIGTERM)
pausePOST /container/web-demo/pauseFreeze container (SIGSTOP) — keeps memory, stops CPU
unpausePOST /container/web-demo/unpauseResume a paused container (SIGCONT)
curl -X POST http://dockplay.mywire.org:80/container/web-demo/stop

Delete Container

curl -X DELETE "http://dockplay.mywire.org:80/container/web-demo?force=true"

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.

curl "http://dockplay.mywire.org:80/image/search?q=postgres"
{
  "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

curl http://dockplay.mywire.org:80/image

Inspect Full Image Metadata (A-Z)

Returns architecture, OS, creation date, layer digests, size, environment variables, labels, exposed ports, entrypoint, and more.

curl http://dockplay.mywire.org:80/image/inspect/nginx:alpine

Delete Local Image

curl -X DELETE "http://dockplay.mywire.org:80/image/nginx:alpine?force=true"

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

PathDestinationRenders
/Management Applanding.ejs
/appManagement Appindex.ejs (Dashboard)
/docsManagement Appdocs.ejs (This page)
/api-docsManagement AppSwagger UI
/containerManagement AppREST API JSON

Accessing a Deployed Container

http://web-demo.dockplay.mywire.org:80

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

docker network connect deploy-engine my-existing-container

Then immediately access it via:

http://my-existing-container.dockplay.mywire.org:80

Port Auto-selection Logic

The proxy selects the container's exposed port in this priority order:

  1. Port 80/tcp (standard HTTP)
  2. Port 3000 or 8080
  3. Any non-443 port
  4. First exposed port
  5. Falls back to 80

Networks

Monitor, inspect, and manage Docker bridge networks from the dashboard or REST API.

List All Networks

curl http://dockplay.mywire.org:80/network

Delete a Network

curl -X DELETE http://dockplay.mywire.org:80/network/my-custom-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:

docker network create --driver bridge deploy-engine

Storage Volumes

Monitor persistent Docker volumes and safely clean up unused storage.

List All Volumes

curl http://dockplay.mywire.org:80/volume

Delete a Volume

curl -X DELETE "http://dockplay.mywire.org:80/volume/my-data-volume?force=true"

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

MethodEndpointDescription
GET/containerList all containers on deploy-engine network
POST/containerDeploy & start a new container
GET/container/:idInspect container details
POST/container/:id/startStart stopped container
POST/container/:id/stopStop running container
POST/container/:id/pausePause running container
POST/container/:id/unpauseResume paused container
DELETE/container/:idForce delete container

Images

MethodEndpointDescription
GET/imageList local cached images
GET/image/search?q=Search Docker Hub
GET/image/inspect/:idFull A-Z image metadata
DELETE/image/:idDelete local image

Networks & Volumes

MethodEndpointDescription
GET/networkList all Docker networks
DELETE/network/:idDelete a network
GET/volumeList all storage volumes
DELETE/volume/:idDelete a volume

CLI Commands

All essential shell commands — click any block to copy.

Docker Compose

docker compose up -d --build
docker compose down
docker compose logs -f dockpoly-engine

Container Operations

curl -X POST http://dockplay.mywire.org:80/container \ -H "Content-Type: application/json" \ -d '{"image":"nginx","tag":"alpine","containerName":"web-demo"}'
curl http://dockplay.mywire.org:80/container
curl -X POST http://dockplay.mywire.org:80/container/web-demo/stop
curl -X DELETE "http://dockplay.mywire.org:80/container/web-demo?force=true"

Image Operations

curl "http://dockplay.mywire.org:80/image/search?q=postgres"
curl http://dockplay.mywire.org:80/image
curl http://dockplay.mywire.org:80/image/inspect/nginx:alpine
curl -X DELETE "http://dockplay.mywire.org:80/image/nginx:alpine?force=true"

Network & Volume

curl http://dockplay.mywire.org:80/network
curl http://dockplay.mywire.org:80/volume
docker network connect deploy-engine my-container

Testing & Validation

Unit and integration test suite with 100% service-layer coverage.

Run Tests

bun test

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

bun x tsc --noEmit

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.

ScenarioUse .env?Reason
Local development✅ YesConvenient for local overrides. Never leave the machine.
Docker Compose local✅ Yesdocker-compose.yml reads it via env_file: .env.
Production / CI/CD❌ NoInject variables via platform secrets or CI environment variables instead.
Git repository❌ NeverExposes all secrets publicly. Use .env.example instead.
Docker image build⚠️ CautionDon'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:

cp .env.example .env
# .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

VariableDefaultRequiredDescription
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

echo ".env" >> .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

VariableDefaultDescription
PORT4000Public reverse proxy port
MANAGEMENT_PORT3000Internal management API port
MANAGEMENT_HOST127.0.0.1Internal management API host
DOCKER_NETWORKdeploy-engineBridge network for subdomain routing
DOMAINlocalhostRoot 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.