Skip to content

Commit 5b3c70e

Browse files
committed
wip: feat: docker image templates
1 parent bf4f530 commit 5b3c70e

8 files changed

Lines changed: 330 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: 154 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,62 @@ 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+
* Note: Proxmox automatically appends .tar, so we don't include it here
80+
* @param {object} parsed - Parsed Docker ref components
81+
* @returns {string} Sanitized filename (e.g., "docker.io_library_nginx_latest")
82+
*/
83+
function generateImageFilename(parsed) {
84+
const { registry, namespace, image, tag } = parsed;
85+
const sanitized = `${registry}_${namespace}_${image}_${tag}`.replace(/[/:]/g, '_');
86+
return sanitized;
87+
}
88+
89+
/**
90+
* Parse command line arguments
91+
* @returns {object} Parsed arguments
92+
*/
93+
function parseArgs() {
94+
const args = {};
95+
for (const arg of process.argv.slice(2)) {
96+
const match = arg.match(/^--([^=]+)=(.+)$/);
97+
if (match) {
98+
args[match[1]] = match[2];
99+
}
100+
}
101+
return args;
102+
}
103+
45104
/**
46105
* Wait for a Proxmox task to complete
47106
* @param {ProxmoxApi} client - The Proxmox API client
@@ -175,6 +234,9 @@ async function main() {
175234
console.log(`Site: ${site.name} (${site.internalDomain})`);
176235
console.log(`Template: ${container.template}`);
177236

237+
const isDocker = isDockerImage(container.template);
238+
console.log(`Template type: ${isDocker ? 'Docker image' : 'Proxmox template'}`);
239+
178240
try {
179241
// Update status to 'creating'
180242
await container.update({ status: 'creating' });
@@ -184,54 +246,105 @@ async function main() {
184246
const client = await node.api();
185247
console.log('Proxmox API client initialized');
186248

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
249+
// Allocate VMID right before creating to minimize race condition window
200250
console.log('Allocating VMID from Proxmox...');
201251
const vmid = await client.nextId();
202252
console.log(`Allocated VMID: ${vmid}`);
203253

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

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

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-
235348
// Start the container
236349
console.log('Starting container...');
237350
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)