435 lines
11 KiB
Markdown
435 lines
11 KiB
Markdown
# 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 key–only 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. `20000–21000`).
|
||
9. Starts a Docker container:
|
||
|
||
```bash
|
||
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. `20000–21000`)
|
||
* 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 VM’s 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 you’ll 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. `30000–30100`:
|
||
|
||
```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`.
|
||
It’s intended to run as root (or equivalent) on a box you control.
|
||
|
||
* **SSH ports exposed on the host**
|
||
Example: `20000–21000`.
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
|
||
|