Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a6445cc
fix: update npm version for unprivileged container use
runleveldev Jan 26, 2026
e37c3a8
fix: build docker on all pushes
runleveldev Jan 26, 2026
cedcce5
fix: disable nginx-debug service
runleveldev Jan 26, 2026
1a396e8
fix: prefer username/password for root@pam privileges
runleveldev Jan 26, 2026
c1d417b
fix: disable the required auth for search setting for compatibility
runleveldev Jan 26, 2026
e4acb7c
fix: catch database deletion errors
runleveldev Jan 26, 2026
9b44d18
feat: enable features for docker
runleveldev Jan 26, 2026
2dc6b40
fix: add cascade delete back to services table
runleveldev Jan 26, 2026
771b5c7
refactor: cleanup
runleveldev Jan 27, 2026
13ce272
refactor: prefer postgres in development over sqlite
runleveldev Jan 27, 2026
02c6c1b
fix: postgres fixes in migrations
runleveldev Jan 27, 2026
e85e233
feat: install postgres into docker appliance
runleveldev Jan 27, 2026
5c9b7fa
fix: dumb
runleveldev Jan 27, 2026
be06566
feat: async container creation
runleveldev Jan 27, 2026
af4be9e
feat: job streaming
runleveldev Jan 27, 2026
bf4f530
feat: delete sanity checks
runleveldev Jan 27, 2026
a3a3bfd
feat: docker image templates
runleveldev Jan 27, 2026
5a4d5f3
feat: add nodes storage option
runleveldev Jan 27, 2026
a87af72
feat: add env and entrypoint options for OCI containers
runleveldev Jan 27, 2026
16761c6
fix: prevent colisions by appending image hash
runleveldev Jan 27, 2026
ea4b1d7
refactor: view current job immediately
runleveldev Jan 27, 2026
74bfcb5
fix: allow editing failed containers
runleveldev Jan 28, 2026
3aea382
fix: get container Mac/Ip on recreate
runleveldev Jan 28, 2026
afdc3c3
feat: dedicate restart button for containers
runleveldev Jan 28, 2026
67eecd7
fix: fail recreate job when container fails to come up cleanly
runleveldev Jan 28, 2026
a44e659
fix: improve service delete UX
runleveldev Jan 28, 2026
0dae5c3
wip: feat: volume management
runleveldev Jan 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/create-a-container/data/database.sqlite
/create-a-container/certs/*
/create-a-container/certs/*
/create-a-container/.envcompose.override.yml
/mie-opensource-landing/build
*/node_modules
11 changes: 0 additions & 11 deletions .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ name: Build and Push Docker Image

on:
push:
branches:
- main
pull_request:
branches:
- main

env:
REGISTRY: ghcr.io
Expand Down Expand Up @@ -34,10 +29,6 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract branch name
id: branch
run: echo "name=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
Expand All @@ -54,7 +45,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
OPENSOURCE_SERVER_BRANCH=${{ steps.branch.outputs.name }}
cache-from: type=gha
cache-to: type=gha,mode=max
31 changes: 18 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ RUN apt update && apt -y install curl gnupg2 ca-certificates lsb-release debian-
&& cat /etc/apt/preferences.d/99nginx \
&& apt update \
&& apt install -y nginx ssl-cert \
&& systemctl enable nginx
&& echo 'disable nginx-debug.service' >/etc/systemd/system-preset/00-nginx.preset

# Install DNSMasq and configure it to only get it's config from our pull-config
RUN apt update && apt -y install dnsmasq && systemctl enable dnsmasq
Expand All @@ -39,19 +39,18 @@ ARG LEGO_VERSION=v4.28.1
RUN curl -fsSL "https://github.com/go-acme/lego/releases/download/${LEGO_VERSION}/lego_${LEGO_VERSION}_linux_amd64.tar.gz" \
| tar -xz -C /usr/local/bin lego

# Install Postgres 18 from the PGDG repository
RUN apt update && apt -y install postgresql-common \
&& /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \
&& apt -y install postgresql-18

# We install the nodesource repo for newer versions of NPM fixing compatibility
# with unprivileged containers. This sets 24.x up which is the LTS at this time
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash -

# Install requisites: git for updating the software, make and npm for installing
# and management.
RUN apt update && apt -y install git make npm

# Install the software. We include the .git directory so that the software can
# update itself without replacing the entire container.
ARG OPENSOURCE_SERVER_BRANCH=main
RUN git clone \
--branch=${OPENSOURCE_SERVER_BRANCH} \
https://github.com/mieweb/opensource-server.git \
/opt/opensource-server \
&& cd /opt/opensource-server \
&& make install
RUN apt update && apt -y install git make nodejs sudo

# Install the ldap-gateway package
ARG LDAP_GATEWAY_BRANCH=main
Expand All @@ -67,6 +66,12 @@ RUN git clone \
&& cp /opt/ldap-gateway/nfpm/systemd/ldap-gateway.service /etc/systemd/system/ldap-gateway.service \
&& systemctl enable ldap-gateway

# Install the software. We include the .git directory so that the software can
# update itself without replacing the entire container.
COPY . /opt/opensource-server
RUN cd /opt/opensource-server \
&& make install

# Tag the exposed ports for services handled by the container
# NGINX (http, https, quic)
EXPOSE 80
Expand All @@ -81,4 +86,4 @@ EXPOSE 686
# Configure systemd to run properly in a container. This isn't nessary for LXC
# in Proxmox, but is useful for testing with Docker directly.
STOPSIGNAL SIGRTMIN+3
ENTRYPOINT [ "/sbin/init" ]
ENTRYPOINT [ "/sbin/init" ]
12 changes: 7 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ help:

install: install-create-container install-pull-config install-docs

SYSTEMD_DIR := create-a-container/systemd
SERVICES := $(wildcard $(SYSTEMD_DIR)/*.service)
install-create-container:
cd create-a-container && npm install --production
cd create-a-container && npm run db:migrate
install -m644 -oroot -groot create-a-container/systemd/container-creator.service /etc/systemd/system/container-creator.service
cd create-a-container && npm install --omit=dev
install -m 644 -o root -g root $(SERVICES) /etc/systemd/system/
systemctl daemon-reload || true
systemctl enable container-creator.service
systemctl start container-creator.service || true
@for service in $(notdir $(SERVICES)); do \
systemctl enable $$service; \
done

install-pull-config:
cd pull-config && bash install.sh
Expand Down
128 changes: 117 additions & 11 deletions create-a-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ A web application for managing LXC container creation, configuration, and lifecy
```mermaid
erDiagram
Node ||--o{ Container : "hosts"
Node ||--o{ Volume : "stores"
Container ||--o{ Service : "exposes"
Container ||--o{ ContainerVolume : "mounts"
ContainerVolume }o--|| Volume : "references"

Node {
int id PK
string name UK "Proxmox node name"
string apiUrl "Proxmox API URL"
boolean tlsVerify "Verify TLS certificates"
int placeholderCtId "VMID for volume storage"
datetime createdAt
datetime updatedAt
}
Expand All @@ -22,16 +26,39 @@ erDiagram
int id PK
string hostname UK "FQDN hostname"
string username "Owner username"
string osRelease "OS distribution"
string status "pending,creating,running,failed"
string template "Template name"
int creationJobId FK "References Job"
int nodeId FK "References Node"
int containerId UK "Proxmox VMID"
string macAddress UK "MAC address"
string ipv4Address UK "IPv4 address"
string macAddress UK "MAC address (nullable)"
string ipv4Address UK "IPv4 address (nullable)"
string aiContainer "Node type flag"
datetime createdAt
datetime updatedAt
}

Volume {
int id PK
string name "User-friendly name"
string username "Owner username"
string proxmoxVolume "Proxmox reference"
int sizeGb "Size in GB"
int siteId FK "References Site"
int nodeId FK "References Node"
datetime createdAt
datetime updatedAt
}

ContainerVolume {
int id PK
int containerId FK "References Container"
int volumeId FK "References Volume"
string mountPath "Mount point path"
datetime createdAt
datetime updatedAt
}

Service {
int id PK
int containerId FK "References Container"
Expand All @@ -49,13 +76,18 @@ erDiagram
- `(Node.name)` - Unique
- `(Container.hostname)` - Unique
- `(Container.nodeId, Container.containerId)` - Unique (same VMID can exist on different nodes)
- `(Volume.username, Volume.name, Volume.siteId)` - Unique (volume names unique per user per site)
- `(ContainerVolume.containerId, ContainerVolume.volumeId)` - Unique (one attachment per volume per container)
- `(ContainerVolume.containerId, ContainerVolume.mountPath)` - Unique (mount paths unique per container)
- `(Service.externalHostname)` - Unique when type='http'
- `(Service.type, Service.externalPort)` - Unique when type='tcp' or type='udp'

## Features

- **User Authentication** - Proxmox VE authentication integration
- **Container Management** - Create, list, and track LXC containers
- **Docker/OCI Support** - Pull and deploy containers from Docker Hub, GHCR, or any OCI registry
- **Persistent Volumes** - Named volumes that survive container deletion for data persistence
- **Service Registry** - Track HTTP/TCP/UDP services running on containers
- **Dynamic Nginx Config** - Generate nginx reverse proxy configurations on-demand
- **Real-time Progress** - SSE (Server-Sent Events) for container creation progress
Expand Down Expand Up @@ -206,12 +238,14 @@ List all containers for authenticated user
Display container creation form

#### `POST /containers`
Create or register a container
- **Query Parameter**: `init` (boolean) - If true, requires auth and spawns container creation
- **Body (init=true)**: `{ hostname, osRelease, httpPort, aiContainer }`
- **Body (init=false)**: Container registration data (for scripts)
- **Returns (init=true)**: Redirect to status page
- **Returns (init=false)**: `{ containerId, message }`
Create a container asynchronously via a background job
- **Body**: `{ hostname, template, customTemplate, services }` where:
- `hostname`: Container hostname
- `template`: Template selection in format "nodeName,vmid" OR "custom" for Docker images
- `customTemplate`: Docker image reference when template="custom" (e.g., `nginx`, `nginx:alpine`, `myorg/myapp:v1`, `ghcr.io/org/image:tag`)
- `services`: Object of service definitions
- **Returns**: Redirect to containers list with flash message
- **Process**: Creates pending container, services, and job in a single transaction. Docker image references are normalized to full format (`host/org/image:tag`). The job-runner executes the actual Proxmox operations.

#### `DELETE /containers/:id` (Auth Required)
Delete a container from both Proxmox and the database
Expand Down Expand Up @@ -402,6 +436,75 @@ SELECT id, status FROM Jobs WHERE id = <ID>;
- Add batching or file-based logs for high-volume output to reduce DB pressure
- Implement job timeout/deadline and automatic cancellation

### Volume Management Routes

#### `GET /sites/:siteId/volumes` (Auth Required)
List all volumes owned by the authenticated user in a site
- **Returns**: HTML page with volume list

#### `GET /sites/:siteId/volumes/new` (Auth Required)
Display volume creation form
- **Returns**: HTML page with form

#### `POST /sites/:siteId/volumes` (Auth Required)
Create a new persistent volume
- **Body**: `{ name, nodeId }`
- `name`: Volume name (alphanumeric, dash, underscore only)
- `nodeId`: Node where the volume should be created
- **Process**:
1. Allocates disk on the node's storage
2. Attaches to the node's placeholder container
3. Creates Volume record in database
- **Returns**: Redirect to volumes list

#### `DELETE /sites/:siteId/volumes/:id` (Auth Required)
Delete a volume permanently
- **Path Parameter**: `id` - Volume database ID
- **Authorization**: User can only delete their own volumes
- **Validation**: Volume must not be attached to any container
- **Process**:
1. Detaches from placeholder container
2. Deletes disk from Proxmox storage
3. Removes Volume record from database
- **Returns**: `{ success: true, message: "Volume deleted successfully" }`
- **Errors**:
- `400` - Volume is currently attached to a container
- `403` - User doesn't own the volume
- `404` - Volume not found

### Volume Attachment

Volumes can be attached to containers during container creation:

#### During `POST /sites/:siteId/containers`
- **Additional Body Fields**:
- `volumes`: Array of volume attachments
- `volumes[N][volumeId]`: Volume ID to attach
- `volumes[N][mountPath]`: Mount point inside container (e.g., `/data`)
- **Process**:
1. Validates all volumes exist and are owned by user
2. Validates volumes are on the same node as the target container
3. Creates container and ContainerVolume records
4. Job runner moves volumes from placeholder to new container
- **Note**: Cross-node volume attachment requires manual migration

#### During `DELETE /sites/:siteId/containers/:id`
When a container with attached volumes is deleted:
1. All attached volumes are transferred to the placeholder container
2. ContainerVolume records are deleted
3. Volume records are preserved (data persists)
4. Volumes can be reattached to new containers

### Placeholder Container

Each Proxmox node has a "placeholder container" for volume storage:

- **Purpose**: Holds volumes not attached to user containers
- **Auto-creation**: Created when node is registered
- **Configuration**: Minimal Alpine, 16MB RAM, no network, protection enabled
- **VMID**: Stored in `Node.placeholderCtId`
- **Never started**: Exists only to own volumes

### Configuration Routes

#### `GET /nginx.conf`
Expand Down Expand Up @@ -430,8 +533,11 @@ Test email configuration (development/testing)
id INT PRIMARY KEY AUTO_INCREMENT
hostname VARCHAR(255) UNIQUE NOT NULL
username VARCHAR(255) NOT NULL
osRelease VARCHAR(255)
containerId INT UNSIGNED UNIQUE
status VARCHAR(20) NOT NULL DEFAULT 'pending'
template VARCHAR(255)
creationJobId INT FOREIGN KEY REFERENCES Jobs(id)
nodeId INT FOREIGN KEY REFERENCES Nodes(id)
containerId INT UNSIGNED NOT NULL
macAddress VARCHAR(17) UNIQUE
ipv4Address VARCHAR(45) UNIQUE
aiContainer VARCHAR(50) DEFAULT 'N'
Expand Down
Loading
Loading