@@ -6,28 +6,41 @@ Docker Command:
66
77 pgpm docker <subcommand> [OPTIONS]
88
9- Manage PostgreSQL Docker containers for local development.
9+ Manage Docker containers for local development.
10+ PostgreSQL is always started by default. Additional services can be
11+ included with the --include flag.
1012
1113Subcommands:
12- start Start PostgreSQL container
13- stop Stop PostgreSQL container
14+ start Start containers
15+ stop Stop containers
16+ ls List available services and their status
1417
15- Options:
16- --help, -h Show this help message
18+ PostgreSQL Options:
1719 --name <name> Container name (default: postgres)
1820 --image <image> Docker image (default: constructiveio/postgres-plus:18)
1921 --port <port> Host port mapping (default: 5432)
2022 --user <user> PostgreSQL user (default: postgres)
2123 --password <pass> PostgreSQL password (default: password)
2224 --shm-size <size> Shared memory size for container (default: 2g)
23- --recreate Remove and recreate container on start
25+
26+ General Options:
27+ --help, -h Show this help message
28+ --recreate Remove and recreate containers on start
29+ --include <svc> Include additional service (can be repeated)
30+
31+ Available Additional Services:
32+ minio MinIO S3-compatible object storage (port 9000)
2433
2534Examples:
26- pgpm docker start Start default PostgreSQL container
35+ pgpm docker start Start PostgreSQL only
36+ pgpm docker start --include minio Start PostgreSQL + MinIO
2737 pgpm docker start --port 5433 Start on custom port
2838 pgpm docker start --shm-size 4g Start with 4GB shared memory
29- pgpm docker start --recreate Remove and recreate container
30- pgpm docker stop Stop PostgreSQL container
39+ pgpm docker start --recreate Remove and recreate containers
40+ pgpm docker start --recreate --include minio Recreate PostgreSQL + MinIO
41+ pgpm docker stop Stop PostgreSQL
42+ pgpm docker stop --include minio Stop PostgreSQL + MinIO
43+ pgpm docker ls List services and status
3144` ;
3245
3346interface DockerRunOptions {
@@ -40,6 +53,32 @@ interface DockerRunOptions {
4053 recreate ?: boolean ;
4154}
4255
56+ interface PortMapping {
57+ host : number ;
58+ container : number ;
59+ }
60+
61+ interface ServiceDefinition {
62+ name : string ;
63+ image : string ;
64+ ports : PortMapping [ ] ;
65+ env : Record < string , string > ;
66+ command ?: string [ ] ;
67+ }
68+
69+ const ADDITIONAL_SERVICES : Record < string , ServiceDefinition > = {
70+ minio : {
71+ name : 'minio' ,
72+ image : 'minio/minio' ,
73+ ports : [ { host : 9000 , container : 9000 } ] ,
74+ env : {
75+ MINIO_ACCESS_KEY : 'minioadmin' ,
76+ MINIO_SECRET_KEY : 'minioadmin' ,
77+ } ,
78+ command : [ 'server' , '/data' ] ,
79+ } ,
80+ } ;
81+
4382interface SpawnResult {
4483 code : number ;
4584 stdout : string ;
@@ -196,6 +235,125 @@ async function stopContainer(name: string): Promise<void> {
196235 }
197236}
198237
238+ async function startService ( service : ServiceDefinition , recreate : boolean ) : Promise < void > {
239+ const { name, image, ports, env : serviceEnv , command } = service ;
240+
241+ const exists = await containerExists ( name ) ;
242+ const running = await isContainerRunning ( name ) ;
243+
244+ if ( running === true ) {
245+ console . log ( `✅ Container "${ name } " is already running` ) ;
246+ return ;
247+ }
248+
249+ if ( recreate && exists ) {
250+ console . log ( `🗑️ Removing existing container "${ name } "...` ) ;
251+ const removeResult = await run ( 'docker' , [ 'rm' , '-f' , name ] , { stdio : 'inherit' } ) ;
252+ if ( removeResult . code !== 0 ) {
253+ await cliExitWithError ( `Failed to remove container "${ name } "` ) ;
254+ return ;
255+ }
256+ }
257+
258+ if ( exists && running === false ) {
259+ console . log ( `🔄 Starting existing container "${ name } "...` ) ;
260+ const startResult = await run ( 'docker' , [ 'start' , name ] , { stdio : 'inherit' } ) ;
261+ if ( startResult . code === 0 ) {
262+ console . log ( `✅ Container "${ name } " started successfully` ) ;
263+ } else {
264+ await cliExitWithError ( `Failed to start container "${ name } "` ) ;
265+ }
266+ return ;
267+ }
268+
269+ console . log ( `🚀 Creating and starting new container "${ name } "...` ) ;
270+ const runArgs = [
271+ 'run' ,
272+ '-d' ,
273+ '--name' , name ,
274+ ] ;
275+
276+ for ( const [ key , value ] of Object . entries ( serviceEnv ) ) {
277+ runArgs . push ( '-e' , `${ key } =${ value } ` ) ;
278+ }
279+
280+ for ( const portMapping of ports ) {
281+ runArgs . push ( '-p' , `${ portMapping . host } :${ portMapping . container } ` ) ;
282+ }
283+
284+ runArgs . push ( image ) ;
285+
286+ if ( command ) {
287+ runArgs . push ( ...command ) ;
288+ }
289+
290+ const runResult = await run ( 'docker' , runArgs , { stdio : 'inherit' } ) ;
291+ if ( runResult . code === 0 ) {
292+ console . log ( `✅ Container "${ name } " created and started successfully` ) ;
293+ const portInfo = ports . map ( p => `localhost:${ p . host } ` ) . join ( ', ' ) ;
294+ console . log ( `📌 ${ name } is available at ${ portInfo } ` ) ;
295+ } else {
296+ const portInfo = ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
297+ await cliExitWithError ( `Failed to create container "${ name } ". Check if port ${ portInfo } is already in use.` ) ;
298+ }
299+ }
300+
301+ async function stopService ( service : ServiceDefinition ) : Promise < void > {
302+ await stopContainer ( service . name ) ;
303+ }
304+
305+ function parseInclude ( args : Partial < Record < string , any > > ) : string [ ] {
306+ const include = args . include ;
307+ if ( ! include ) return [ ] ;
308+ if ( Array . isArray ( include ) ) return include as string [ ] ;
309+ if ( typeof include === 'string' ) return [ include ] ;
310+ return [ ] ;
311+ }
312+
313+ function resolveIncludedServices ( includeNames : string [ ] ) : ServiceDefinition [ ] {
314+ const services : ServiceDefinition [ ] = [ ] ;
315+ for ( const name of includeNames ) {
316+ const service = ADDITIONAL_SERVICES [ name ] ;
317+ if ( ! service ) {
318+ console . warn ( `⚠️ Unknown service: "${ name } ". Available: ${ Object . keys ( ADDITIONAL_SERVICES ) . join ( ', ' ) } ` ) ;
319+ } else {
320+ services . push ( service ) ;
321+ }
322+ }
323+ return services ;
324+ }
325+
326+ async function listServices ( ) : Promise < void > {
327+ const dockerAvailable = await checkDockerAvailable ( ) ;
328+
329+ console . log ( '\nAvailable services:\n' ) ;
330+ console . log ( ' Primary:' ) ;
331+
332+ if ( dockerAvailable ) {
333+ const pgRunning = await isContainerRunning ( 'postgres' ) ;
334+ const pgStatus = pgRunning === true ? '\x1b[32mrunning\x1b[0m' : pgRunning === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m' ;
335+ console . log ( ` postgres constructiveio/postgres-plus:18 ${ pgStatus } ` ) ;
336+ } else {
337+ console . log ( ' postgres constructiveio/postgres-plus:18 \x1b[90m(docker not available)\x1b[0m' ) ;
338+ }
339+
340+ console . log ( '\n Additional (use --include <name>):' ) ;
341+
342+ for ( const [ key , service ] of Object . entries ( ADDITIONAL_SERVICES ) ) {
343+ if ( dockerAvailable ) {
344+ const running = await isContainerRunning ( service . name ) ;
345+ const status = running === true ? '\x1b[32mrunning\x1b[0m' : running === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m' ;
346+ const portInfo = service . ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
347+ console . log ( ` ${ key . padEnd ( 12 ) } ${ service . image . padEnd ( 36 ) } ${ status } port ${ portInfo } ` ) ;
348+ } else {
349+ const portInfo = service . ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
350+ console . log ( ` ${ key . padEnd ( 12 ) } ${ service . image . padEnd ( 36 ) } \x1b[90m(docker not available)\x1b[0m port ${ portInfo } ` ) ;
351+ }
352+ }
353+
354+ console . log ( '' ) ;
355+ }
356+
199357export default async (
200358 argv : Partial < Record < string , any > > ,
201359 _prompter : Inquirerer ,
@@ -211,7 +369,7 @@ export default async (
211369
212370 if ( ! subcommand ) {
213371 console . log ( dockerUsageText ) ;
214- await cliExitWithError ( 'No subcommand provided. Use "start" or "stop ".' ) ;
372+ await cliExitWithError ( 'No subcommand provided. Use "start", "stop", or "ls ".' ) ;
215373 return ;
216374 }
217375 const name = ( args . name as string ) || 'postgres' ;
@@ -221,18 +379,30 @@ export default async (
221379 const password = ( args . password as string ) || 'password' ;
222380 const shmSize = ( args [ 'shm-size' ] as string ) || ( args . shmSize as string ) || '2g' ;
223381 const recreate = args . recreate === true ;
382+ const includeNames = parseInclude ( args ) ;
383+ const includedServices = resolveIncludedServices ( includeNames ) ;
224384
225385 switch ( subcommand ) {
226386 case 'start' :
227387 await startContainer ( { name, image, port, user, password, shmSize, recreate } ) ;
388+ for ( const service of includedServices ) {
389+ await startService ( service , recreate ) ;
390+ }
228391 break ;
229392
230393 case 'stop' :
231394 await stopContainer ( name ) ;
395+ for ( const service of includedServices ) {
396+ await stopService ( service ) ;
397+ }
398+ break ;
399+
400+ case 'ls' :
401+ await listServices ( ) ;
232402 break ;
233403
234404 default :
235405 console . log ( dockerUsageText ) ;
236- await cliExitWithError ( `Unknown subcommand: ${ subcommand } . Use "start" or "stop ".` ) ;
406+ await cliExitWithError ( `Unknown subcommand: ${ subcommand } . Use "start", "stop", or "ls ".` ) ;
237407 }
238408} ;
0 commit comments