Skip to content

Commit 3e09a13

Browse files
committed
wip: feat: docker image templates
1 parent bf4f530 commit 3e09a13

8 files changed

Lines changed: 329 additions & 63 deletions

File tree

create-a-container/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ erDiagram
5858

5959
- **User Authentication** - Proxmox VE authentication integration
6060
- **Container Management** - Create, list, and track LXC containers
61+
- **Docker/OCI Support** - Pull and deploy containers from Docker Hub, GHCR, or any OCI registry
6162
- **Service Registry** - Track HTTP/TCP/UDP services running on containers
6263
- **Dynamic Nginx Config** - Generate nginx reverse proxy configurations on-demand
6364
- **Real-time Progress** - SSE (Server-Sent Events) for container creation progress
@@ -209,12 +210,13 @@ Display container creation form
209210

210211
#### `POST /containers`
211212
Create a container asynchronously via a background job
212-
- **Body**: `{ hostname, template, services }` where:
213+
- **Body**: `{ hostname, template, customTemplate, services }` where:
213214
- `hostname`: Container hostname
214-
- `template`: Template selection in format "nodeName,vmid"
215+
- `template`: Template selection in format "nodeName,vmid" OR "custom" for Docker images
216+
- `customTemplate`: Docker image reference when template="custom" (e.g., `nginx`, `nginx:alpine`, `myorg/myapp:v1`, `ghcr.io/org/image:tag`)
215217
- `services`: Object of service definitions
216218
- **Returns**: Redirect to containers list with flash message
217-
- **Process**: Creates pending container, services, and job in a single transaction. The job-runner executes the actual Proxmox operations.
219+
- **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.
218220

219221
#### `DELETE /containers/:id` (Auth Required)
220222
Delete a container from both Proxmox and the database

create-a-container/bin/create-container.js

Lines changed: 153 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
*
1111
* The script will:
1212
* 1. Load the container record from the database
13-
* 2. Clone the template in Proxmox
13+
* 2. Either clone a Proxmox template OR pull a Docker image and create from it
1414
* 3. Configure the container (cores, memory, network)
1515
* 4. Start the container
1616
* 5. Query MAC address from Proxmox config
1717
* 6. Query IP address from Proxmox interfaces API
1818
* 7. Update the container record with MAC, IP, and status='running'
1919
*
20+
* Docker images are detected by the presence of '/' in the template field.
21+
* Format: host/org/image:tag (e.g., docker.io/library/nginx:latest)
22+
*
2023
* All output is logged to STDOUT for capture by the job-runner.
2124
* Exit code 0 = success, non-zero = failure.
2225
*/
@@ -42,6 +45,61 @@ function parseArgs() {
4245
return args;
4346
}
4447

48+
/**
49+
* Check if a template is a Docker image reference (contains '/')
50+
* @param {string} template - The template string
51+
* @returns {boolean} True if Docker image, false if Proxmox template
52+
*/
53+
function isDockerImage(template) {
54+
return template.includes('/');
55+
}
56+
57+
/**
58+
* Parse a normalized Docker image reference into components
59+
* Format: host/org/image:tag
60+
* @param {string} ref - The normalized Docker reference
61+
* @returns {object} Parsed components: { registry, namespace, image, tag }
62+
*/
63+
function parseDockerRef(ref) {
64+
// Split off tag
65+
const [imagePart, tag] = ref.split(':');
66+
const parts = imagePart.split('/');
67+
68+
// Format is always host/org/image after normalization
69+
const registry = parts[0];
70+
const image = parts[parts.length - 1];
71+
const namespace = parts.slice(1, -1).join('/');
72+
73+
return { registry, namespace, image, tag };
74+
}
75+
76+
/**
77+
* Generate a filename for a pulled Docker image
78+
* Replaces special chars with underscores
79+
* @param {object} parsed - Parsed Docker ref components
80+
* @returns {string} Sanitized filename (e.g., "docker.io_library_nginx_latest.tar")
81+
*/
82+
function generateImageFilename(parsed) {
83+
const { registry, namespace, image, tag } = parsed;
84+
const sanitized = `${registry}_${namespace}_${image}_${tag}`.replace(/[/:]/g, '_');
85+
return `${sanitized}.tar`;
86+
}
87+
88+
/**
89+
* Parse command line arguments
90+
* @returns {object} Parsed arguments
91+
*/
92+
function parseArgs() {
93+
const args = {};
94+
for (const arg of process.argv.slice(2)) {
95+
const match = arg.match(/^--([^=]+)=(.+)$/);
96+
if (match) {
97+
args[match[1]] = match[2];
98+
}
99+
}
100+
return args;
101+
}
102+
45103
/**
46104
* Wait for a Proxmox task to complete
47105
* @param {ProxmoxApi} client - The Proxmox API client
@@ -175,6 +233,9 @@ async function main() {
175233
console.log(`Site: ${site.name} (${site.internalDomain})`);
176234
console.log(`Template: ${container.template}`);
177235

236+
const isDocker = isDockerImage(container.template);
237+
console.log(`Template type: ${isDocker ? 'Docker image' : 'Proxmox template'}`);
238+
178239
try {
179240
// Update status to 'creating'
180241
await container.update({ status: 'creating' });
@@ -184,54 +245,105 @@ async function main() {
184245
const client = await node.api();
185246
console.log('Proxmox API client initialized');
186247

187-
// Find the template VMID by matching the template name
188-
console.log(`Looking for template: ${container.template}`);
189-
const templates = await client.getLxcTemplates(node.name);
190-
const templateContainer = templates.find(t => t.name === container.template);
191-
192-
if (!templateContainer) {
193-
throw new Error(`Template "${container.template}" not found on node ${node.name}`);
194-
}
195-
196-
const templateVmid = templateContainer.vmid;
197-
console.log(`Found template VMID: ${templateVmid}`);
198-
199-
// Allocate VMID right before cloning to minimize race condition window
248+
// Allocate VMID right before creating to minimize race condition window
200249
console.log('Allocating VMID from Proxmox...');
201250
const vmid = await client.nextId();
202251
console.log(`Allocated VMID: ${vmid}`);
203252

204-
// Clone the template
205-
console.log(`Cloning template ${templateVmid} to VMID ${vmid}...`);
206-
const cloneUpid = await client.cloneLxc(node.name, templateVmid, vmid, {
207-
hostname: container.hostname,
208-
description: `Cloned from template ${container.template}`,
209-
full: 1
210-
});
211-
console.log(`Clone task started: ${cloneUpid}`);
212-
213-
// Wait for clone to complete
214-
await waitForTask(client, node.name, cloneUpid);
215-
console.log('Clone completed successfully');
253+
if (isDocker) {
254+
// Docker image: pull from OCI registry, then create container
255+
const parsed = parseDockerRef(container.template);
256+
console.log(`Docker image: ${parsed.registry}/${parsed.namespace}/${parsed.image}:${parsed.tag}`);
257+
258+
const filename = generateImageFilename(parsed);
259+
console.log(`Target filename: ${filename}`);
260+
261+
const storage = node.imageStorage || 'local';
262+
console.log(`Using storage: ${storage}`);
263+
264+
// Pull the image from OCI registry using full image reference
265+
const imageRef = container.template;
266+
console.log(`Pulling image ${imageRef}...`);
267+
const pullUpid = await client.pullOciImage(node.name, storage, {
268+
reference: imageRef,
269+
filename
270+
});
271+
console.log(`Pull task started: ${pullUpid}`);
272+
273+
// Wait for pull to complete
274+
await waitForTask(client, node.name, pullUpid);
275+
console.log('Image pulled successfully');
276+
277+
// Create container from the pulled image
278+
console.log(`Creating container from ${filename}...`);
279+
const ostemplate = `${storage}:vztmpl/${filename}`;
280+
const createUpid = await client.createLxc(node.name, {
281+
vmid,
282+
hostname: container.hostname,
283+
ostemplate,
284+
description: `Created from Docker image ${container.template}`,
285+
cores: 4,
286+
features: 'nesting=1,keyctl=1,fuse=1',
287+
memory: 4096,
288+
net0: 'name=eth0,ip=dhcp,bridge=vmbr0,host-managed=1',
289+
searchdomain: site.internalDomain,
290+
swap: 0,
291+
onboot: 1,
292+
tags: container.username,
293+
unprivileged: 1,
294+
storage: 'local-lvm'
295+
});
296+
console.log(`Create task started: ${createUpid}`);
297+
298+
// Wait for create to complete
299+
await waitForTask(client, node.name, createUpid);
300+
console.log('Container created successfully');
301+
302+
} else {
303+
// Proxmox template: clone existing container
304+
console.log(`Looking for template: ${container.template}`);
305+
const templates = await client.getLxcTemplates(node.name);
306+
const templateContainer = templates.find(t => t.name === container.template);
307+
308+
if (!templateContainer) {
309+
throw new Error(`Template "${container.template}" not found on node ${node.name}`);
310+
}
311+
312+
const templateVmid = templateContainer.vmid;
313+
console.log(`Found template VMID: ${templateVmid}`);
314+
315+
// Clone the template
316+
console.log(`Cloning template ${templateVmid} to VMID ${vmid}...`);
317+
const cloneUpid = await client.cloneLxc(node.name, templateVmid, vmid, {
318+
hostname: container.hostname,
319+
description: `Cloned from template ${container.template}`,
320+
full: 1
321+
});
322+
console.log(`Clone task started: ${cloneUpid}`);
323+
324+
// Wait for clone to complete
325+
await waitForTask(client, node.name, cloneUpid);
326+
console.log('Clone completed successfully');
327+
328+
// Configure the container (Docker containers are configured at creation time)
329+
console.log('Configuring container...');
330+
await client.updateLxcConfig(node.name, vmid, {
331+
cores: 4,
332+
features: 'nesting=1,keyctl=1,fuse=1',
333+
memory: 4096,
334+
net0: 'name=eth0,ip=dhcp,bridge=vmbr0',
335+
searchdomain: site.internalDomain,
336+
swap: 0,
337+
onboot: 1,
338+
tags: container.username
339+
});
340+
console.log('Container configured');
341+
}
216342

217-
// Store the VMID now that clone succeeded
343+
// Store the VMID now that creation succeeded
218344
await container.update({ containerId: vmid });
219345
console.log(`Container VMID ${vmid} stored in database`);
220346

221-
// Configure the container
222-
console.log('Configuring container...');
223-
await client.updateLxcConfig(node.name, vmid, {
224-
cores: 4,
225-
features: 'nesting=1,keyctl=1,fuse=1',
226-
memory: 4096,
227-
net0: 'name=eth0,ip=dhcp,bridge=vmbr0',
228-
searchdomain: site.internalDomain,
229-
swap: 0,
230-
onboot: 1,
231-
tags: container.username
232-
});
233-
console.log('Container configured');
234-
235347
// Start the container
236348
console.log('Starting container...');
237349
const startUpid = await client.startLxc(node.name, vmid);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface, Sequelize) {
6+
await queryInterface.addColumn('Nodes', 'imageStorage', {
7+
type: Sequelize.STRING(255),
8+
allowNull: false,
9+
defaultValue: 'local'
10+
});
11+
},
12+
13+
async down(queryInterface, Sequelize) {
14+
await queryInterface.removeColumn('Nodes', 'imageStorage');
15+
}
16+
};

create-a-container/models/node.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ module.exports = (sequelize, DataTypes) => {
8181
tlsVerify: {
8282
type: DataTypes.BOOLEAN,
8383
allowNull: true
84+
},
85+
imageStorage: {
86+
type: DataTypes.STRING(255),
87+
allowNull: false,
88+
defaultValue: 'local'
8489
}
8590
}, {
8691
sequelize,

0 commit comments

Comments
 (0)