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 ) ;
0 commit comments