A tiny, self-hosted control plane for spinning up Docker containers and using them like lightweight VMs
Find a file
2025-11-23 19:02:38 -05:00
micro-debian-dev 0.1 commit 2025-11-23 19:02:38 -05:00
micro-fedora43-dev 0.1 commit 2025-11-23 19:02:38 -05:00
app.py 0.1 commit 2025-11-23 19:02:38 -05:00
README.md 0.1 commit 2025-11-23 19:02:38 -05:00
start-server.sh 0.1 commit 2025-11-23 19:02:38 -05:00

mentalnet-microcontainers

A DIY provisioning cloud for your homelab.

mentalnet-microcontainers gives you a tiny, self-hosted control plane for spinning up Docker containers and using them like lightweight VMs:

  • Web UI to paste an SSH public key and pick a base image
  • Automatically provisions a container with:
    • User micro (with sudo)
    • SSH keyonly authentication
    • Unique SSH port on the host
    • Dedicated 3 GB ext4 filesystem mounted as /home/micro
  • Designed for “I just want a throwaway dev box on my own hardware”, not full multi-tenant hosting

What this actually does

When you hit the web UI and create a “microcontainer”, the app:

  1. Validates your SSH public key.

  2. Generates a unique container name:

    • e.g. mc-debian-myproject-8c5268
  3. Creates a tenant directory on the host:

    • /srv/microcontainers/<container_name>/
  4. Creates a sparse 3 GB disk.img:

    • fallocate -l 3G disk.img
    • mkfs.ext4 disk.img
  5. Mounts it on the host:

    • /mnt/microcontainers/<container_name>/
  6. Writes your SSH key to:

    • /mnt/microcontainers/<container_name>/.ssh/authorized_keys
  7. Fixes ownership to match the in-container user:

    • chown -R 1000:1000 /mnt/microcontainers/<container_name>
  8. Picks a free port in a range (e.g. 2000021000).

  9. Starts a Docker container:

    docker run -d \
      --name <container_name> \
      --memory=512m \
      --memory-swap=512m \
      -p HOST_PORT:22 \
      -v /mnt/microcontainers/<container_name>:/home/micro \
      micro-debian-dev:latest   # or micro-fedora43-dev:latest
    

10. Shows you an SSH command like:

    ```bash
    ssh micro@your-hostname -p 20013
    ```

From your perspective, it feels like “click → get a tiny VM with a 3 GB home and sudo.”

---

## Features

* **DIY provisioning cloud**
  Turn one Docker host into a small pool of micro-VM-like environments.

* **Web UI (Flask)**

  * Paste SSH key
  * Choose base image (Debian / Fedora)
  * Optional label to tag the container name

* **Per-tenant filesystem**

  * Each microcontainer gets its own 3 GB ext4 filesystem
  * Mounted as `/home/micro`
  * Prevents a single tenant from filling the host via `$HOME`

* **SSH-only login**

  * User: `micro`
  * Auth: SSH public key only (no passwords)
  * `sudo` enabled inside the container

* **Port range allocation**

  * Host ports auto-assigned from a configured range (e.g. `2000021000`)
  * One container per SSH port

* **Image-agnostic**

  * Comes with sample Debian & Fedora dev images
  * Easy to add your own `micro-*-dev` images

---

## Repository layout

Example layout:

```text
mentalnet-microcontainers/
  app.py
  requirements.txt
  docker/
    micro-debian-dev/
      Dockerfile
      build-dockerfile.sh
    micro-fedora43-dev/
      Dockerfile
      build-dockerfile.sh
  README.md
```

You can add more images under `docker/` later (e.g. `micro-alpine-dev`, `micro-arch-dev`, etc.).

---

## Base images

### Build script convention

Each image directory contains a simple build script:

`docker/micro-debian-dev/build-dockerfile.sh` (same pattern for Fedora):

```bash
#!/usr/bin/env bash
set -euo pipefail

IMAGE_NAME="$(basename "$(pwd)")"

echo "Building Docker image: ${IMAGE_NAME}:latest"
docker build -t "${IMAGE_NAME}:latest" .
```

Name your directory the same as the image you want (`micro-debian-dev`, `micro-fedora43-dev`, etc.) and the script will build `IMAGE_NAME:latest` automatically.

---

### Debian dev box (`micro-debian-dev`)

`docker/micro-debian-dev/Dockerfile`:

```dockerfile
FROM debian:stable-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        openssh-server \
        sudo \
        ca-certificates \
        git \
        curl wget \
        vim nano \
        htop \
        build-essential && \
    rm -rf /var/lib/apt/lists/*

# Create 'micro' user with fixed uid/gid 1000
RUN useradd -m -u 1000 -U -s /bin/bash micro && \
    echo "micro:ChangeMe123" | chpasswd && \
    usermod -aG sudo micro

# Prepare .ssh directory (runtime volume will mount over /home/micro)
RUN mkdir -p /home/micro/.ssh && \
    chown -R micro:micro /home/micro && \
    chmod 700 /home/micro/.ssh

# SSH server config: key-only login, use ~/.ssh/authorized_keys
RUN sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^#KbdInteractiveAuthentication yes/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config || true && \
    sed -i 's|^#AuthorizedKeysFile.*|AuthorizedKeysFile .ssh/authorized_keys|' /etc/ssh/sshd_config || true && \
    echo 'UsePAM no' >> /etc/ssh/sshd_config

RUN mkdir -p /var/run/sshd && \
    ssh-keygen -A

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]
```

Build:

```bash
cd docker/micro-debian-dev
./build-dockerfile.sh
```

---

### Fedora 43 dev box (`micro-fedora43-dev`)

`docker/micro-fedora43-dev/Dockerfile`:

```dockerfile
FROM fedora:43

RUN dnf -y update && \
    dnf -y install \
        openssh-server \
        sudo \
        ca-certificates \
        git \
        curl wget \
        vim nano \
        htop \
        gcc gcc-c++ make && \
    dnf clean all && \
    rm -rf /var/cache/dnf

RUN useradd -m -u 1000 -U -s /bin/bash micro && \
    echo "micro:ChangeMe123" | chpasswd && \
    usermod -aG wheel micro

RUN mkdir -p /home/micro/.ssh && \
    chown -R micro:micro /home/micro && \
    chmod 700 /home/micro/.ssh

RUN sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^#KbdInteractiveAuthentication yes/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config || true && \
    sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config || true && \
    sed -i 's|^#AuthorizedKeysFile.*|AuthorizedKeysFile .ssh/authorized_keys|' /etc/ssh/sshd_config || true && \
    echo 'UsePAM no' >> /etc/ssh/sshd_config

RUN mkdir -p /var/run/sshd && \
    ssh-keygen -A

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]
```

Build:

```bash
cd docker/micro-fedora43-dev
./build-dockerfile.sh
```

---

## The Flask app (`app.py`)

Core ideas in `app.py`:

* Configuration:

  ```python
  IMAGES = {
      "debian": {
          "docker_image": "micro-debian-dev:latest",
          "label": "Debian Dev Box (micro-debian-dev)"
      },
      "fedora": {
          "docker_image": "micro-fedora43-dev:latest",
          "label": "Fedora 43 Dev Box (micro-fedora43-dev)"
      },
  }

  DEFAULT_IMAGE_KEY = "debian"

  DATA_ROOT = "/srv/microcontainers"   # per-tenant metadata + disk.img
  MOUNT_ROOT = "/mnt/microcontainers"  # where disk.img files are mounted
  HOSTNAME = "arthur.lan"              # only used for display in UI
  PORT_RANGE_START = 20000
  PORT_RANGE_END = 21000
  ```

* Each POST to `/`:

  * Validates the SSH key
  * Picks an image
  * Generates a container name
  * Calls `ensure_tenant_disk()` to create/mount the 3 GB ext4 filesystem and write `authorized_keys`
  * Chooses a free port in the range
  * Calls `run_docker_container()` with:

    * `-p HOST_PORT:22`
    * `-v home_mount:/home/micro`

Returned info includes container name, host port, and an SSH command.

---

## Running the app

### 1. Install dependencies

Create a venv (optional but recommended):

```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

`requirements.txt`:

```text
Flask>=3.0,<4.0
```

### 2. Run the Flask server

```bash
python app.py
```

By default it listens on `0.0.0.0:5000`.

You may also want a firewall rule to allow your LAN:

```bash
# Example with iptables (adjust to your LAN subnet)
iptables -A INPUT -s 192.168.86.0/24 -p tcp --dport 5000 -m state --state NEW -j ACCEPT
```

### 3. Use the UI

From a machine on your LAN:

1. Open `http://HOST:5000/` (replace `HOST` with your VMs IP/hostname).
2. Paste your SSH public key (one line, e.g. `ssh-ed25519 AAAA... user@host`).
3. Choose an image (`debian` or `fedora`).
4. Optionally add a label (e.g. `project-x-dev`).
5. Click **Provision MicroContainer**.

If successful youll see something like:

```text
Container created!
Image: debian — Debian Dev Box (micro-debian-dev)
Container name: mc-debian-project-x-8c5268
Host port: 20013
SSH command:
ssh micro@arthur.lan -p 20013
```

---

## Configuration

You can tune basic behavior at the top of `app.py`:

```python
DATA_ROOT = "/srv/microcontainers"
MOUNT_ROOT = "/mnt/microcontainers"
HOSTNAME = "arthur.lan"   # used only for the SSH hint in the UI
PORT_RANGE_START = 20000
PORT_RANGE_END = 21000
```

Examples:

* On a VPS, you might set:

  ```python
  HOSTNAME = "your.vps.domain"
  ```

* To change ports to e.g. `3000030100`:

  ```python
  PORT_RANGE_START = 30000
  PORT_RANGE_END = 30100
  ```

---

## Security notes

This is a **homelab prototype**, not hardened multi-tenant hosting.

Things to be aware of:

* **No auth on the UI**
  Anyone who can reach port 5000 can create containers.
  Recommended:

  * Bind to `127.0.0.1` and reverse tunnel
  * Or put it behind a VPN / reverse proxy with auth

* **Runs as root**
  The app calls `fallocate`, `mkfs.ext4`, `mount`, `chown`, and `docker run`.
  Its intended to run as root (or equivalent) on a box you control.

* **SSH ports exposed on the host**
  Example: `2000021000`.
  Use your firewall / router to decide who can reach them.

* **No automatic cleanup yet**
  Stopping/removing containers and unmounting disks is manual for now:

  * `docker ps`, `docker stop`, `docker rm`
  * `umount /mnt/microcontainers/<container_name>`
  * Optionally `rm -rf /srv/microcontainers/<container_name>/`

Use this like a DIY “micro-VPS factory” in your lab, not a fully managed public cloud.

---

### Secret key

Flask uses `app.secret_key` to sign session data and flash messages.

In `app.py` this is read from the `MICROCONTAINERS_SECRET_KEY` environment variable:

```python
app.secret_key = os.environ.get("MICROCONTAINERS_SECRET_KEY", "dev-only-not-secret")

For anything beyond local testing, set a real random secret:

export MICROCONTAINERS_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"

Ideas / future work

  • Add a simple cleanup UI for stopping/removing containers and unmounting disks.

  • Optional basic auth or token-protected UI.

  • More SKUs:

    • micro-alpine-dev, micro-arch-dev, micro-nix-dev, etc.
  • Stats page showing:

    • Used vs total space per tenant
    • Running containers and their ports