@@ -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,39 @@ interface DockerRunOptions {
4053 recreate ?: boolean ;
4154}
4255
56+ interface PortMapping {
57+ host : number ;
58+ container : number ;
59+ }
60+
61+ interface VolumeMapping {
62+ name : string ;
63+ containerPath : string ;
64+ }
65+
66+ interface ServiceDefinition {
67+ name : string ;
68+ image : string ;
69+ ports : PortMapping [ ] ;
70+ env : Record < string , string > ;
71+ command ?: string [ ] ;
72+ volumes ?: VolumeMapping [ ] ;
73+ }
74+
75+ const ADDITIONAL_SERVICES : Record < string , ServiceDefinition > = {
76+ minio : {
77+ name : 'minio' ,
78+ image : 'minio/minio' ,
79+ ports : [ { host : 9000 , container : 9000 } ] ,
80+ env : {
81+ MINIO_ACCESS_KEY : 'minioadmin' ,
82+ MINIO_SECRET_KEY : 'minioadmin' ,
83+ } ,
84+ command : [ 'server' , '/data' ] ,
85+ volumes : [ { name : 'minio-data' , containerPath : '/data' } ] ,
86+ } ,
87+ } ;
88+
4389interface SpawnResult {
4490 code : number ;
4591 stdout : string ;
@@ -196,6 +242,131 @@ async function stopContainer(name: string): Promise<void> {
196242 }
197243}
198244
245+ async function startService ( service : ServiceDefinition , recreate : boolean ) : Promise < void > {
246+ const { name, image, ports, env : serviceEnv , command } = service ;
247+
248+ const exists = await containerExists ( name ) ;
249+ const running = await isContainerRunning ( name ) ;
250+
251+ if ( running === true ) {
252+ console . log ( `✅ Container "${ name } " is already running` ) ;
253+ return ;
254+ }
255+
256+ if ( recreate && exists ) {
257+ console . log ( `🗑️ Removing existing container "${ name } "...` ) ;
258+ const removeResult = await run ( 'docker' , [ 'rm' , '-f' , name ] , { stdio : 'inherit' } ) ;
259+ if ( removeResult . code !== 0 ) {
260+ await cliExitWithError ( `Failed to remove container "${ name } "` ) ;
261+ return ;
262+ }
263+ }
264+
265+ if ( exists && running === false ) {
266+ console . log ( `🔄 Starting existing container "${ name } "...` ) ;
267+ const startResult = await run ( 'docker' , [ 'start' , name ] , { stdio : 'inherit' } ) ;
268+ if ( startResult . code === 0 ) {
269+ console . log ( `✅ Container "${ name } " started successfully` ) ;
270+ } else {
271+ await cliExitWithError ( `Failed to start container "${ name } "` ) ;
272+ }
273+ return ;
274+ }
275+
276+ console . log ( `🚀 Creating and starting new container "${ name } "...` ) ;
277+ const runArgs = [
278+ 'run' ,
279+ '-d' ,
280+ '--name' , name ,
281+ ] ;
282+
283+ for ( const [ key , value ] of Object . entries ( serviceEnv ) ) {
284+ runArgs . push ( '-e' , `${ key } =${ value } ` ) ;
285+ }
286+
287+ for ( const portMapping of ports ) {
288+ runArgs . push ( '-p' , `${ portMapping . host } :${ portMapping . container } ` ) ;
289+ }
290+
291+ if ( service . volumes ) {
292+ for ( const vol of service . volumes ) {
293+ runArgs . push ( '-v' , `${ vol . name } :${ vol . containerPath } ` ) ;
294+ }
295+ }
296+
297+ runArgs . push ( image ) ;
298+
299+ if ( command ) {
300+ runArgs . push ( ...command ) ;
301+ }
302+
303+ const runResult = await run ( 'docker' , runArgs , { stdio : 'inherit' } ) ;
304+ if ( runResult . code === 0 ) {
305+ console . log ( `✅ Container "${ name } " created and started successfully` ) ;
306+ const portInfo = ports . map ( p => `localhost:${ p . host } ` ) . join ( ', ' ) ;
307+ console . log ( `📌 ${ name } is available at ${ portInfo } ` ) ;
308+ } else {
309+ const portInfo = ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
310+ await cliExitWithError ( `Failed to create container "${ name } ". Check if port ${ portInfo } is already in use.` ) ;
311+ }
312+ }
313+
314+ async function stopService ( service : ServiceDefinition ) : Promise < void > {
315+ await stopContainer ( service . name ) ;
316+ }
317+
318+ function parseInclude ( args : Partial < Record < string , any > > ) : string [ ] {
319+ const include = args . include ;
320+ if ( ! include ) return [ ] ;
321+ if ( Array . isArray ( include ) ) return include as string [ ] ;
322+ if ( typeof include === 'string' ) return [ include ] ;
323+ return [ ] ;
324+ }
325+
326+ function resolveIncludedServices ( includeNames : string [ ] ) : ServiceDefinition [ ] {
327+ const services : ServiceDefinition [ ] = [ ] ;
328+ for ( const name of includeNames ) {
329+ const service = ADDITIONAL_SERVICES [ name ] ;
330+ if ( ! service ) {
331+ console . warn ( `⚠️ Unknown service: "${ name } ". Available: ${ Object . keys ( ADDITIONAL_SERVICES ) . join ( ', ' ) } ` ) ;
332+ } else {
333+ services . push ( service ) ;
334+ }
335+ }
336+ return services ;
337+ }
338+
339+ async function listServices ( ) : Promise < void > {
340+ const dockerAvailable = await checkDockerAvailable ( ) ;
341+
342+ console . log ( '\nAvailable services:\n' ) ;
343+ console . log ( ' Primary:' ) ;
344+
345+ if ( dockerAvailable ) {
346+ const pgRunning = await isContainerRunning ( 'postgres' ) ;
347+ const pgStatus = pgRunning === true ? '\x1b[32mrunning\x1b[0m' : pgRunning === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m' ;
348+ console . log ( ` postgres constructiveio/postgres-plus:18 ${ pgStatus } ` ) ;
349+ } else {
350+ console . log ( ' postgres constructiveio/postgres-plus:18 \x1b[90m(docker not available)\x1b[0m' ) ;
351+ }
352+
353+ console . log ( '\n Additional (use --include <name>):' ) ;
354+
355+ for ( const [ key , service ] of Object . entries ( ADDITIONAL_SERVICES ) ) {
356+ if ( dockerAvailable ) {
357+ const running = await isContainerRunning ( service . name ) ;
358+ const status = running === true ? '\x1b[32mrunning\x1b[0m' : running === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m' ;
359+ const portInfo = service . ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
360+ console . log ( ` ${ key . padEnd ( 12 ) } ${ service . image . padEnd ( 36 ) } ${ status } port ${ portInfo } ` ) ;
361+ } else {
362+ const portInfo = service . ports . map ( p => String ( p . host ) ) . join ( ', ' ) ;
363+ console . log ( ` ${ key . padEnd ( 12 ) } ${ service . image . padEnd ( 36 ) } \x1b[90m(docker not available)\x1b[0m port ${ portInfo } ` ) ;
364+ }
365+ }
366+
367+ console . log ( '' ) ;
368+ }
369+
199370export default async (
200371 argv : Partial < Record < string , any > > ,
201372 _prompter : Inquirerer ,
@@ -211,7 +382,7 @@ export default async (
211382
212383 if ( ! subcommand ) {
213384 console . log ( dockerUsageText ) ;
214- await cliExitWithError ( 'No subcommand provided. Use "start" or "stop ".' ) ;
385+ await cliExitWithError ( 'No subcommand provided. Use "start", "stop", or "ls ".' ) ;
215386 return ;
216387 }
217388 const name = ( args . name as string ) || 'postgres' ;
@@ -221,18 +392,30 @@ export default async (
221392 const password = ( args . password as string ) || 'password' ;
222393 const shmSize = ( args [ 'shm-size' ] as string ) || ( args . shmSize as string ) || '2g' ;
223394 const recreate = args . recreate === true ;
395+ const includeNames = parseInclude ( args ) ;
396+ const includedServices = resolveIncludedServices ( includeNames ) ;
224397
225398 switch ( subcommand ) {
226399 case 'start' :
227400 await startContainer ( { name, image, port, user, password, shmSize, recreate } ) ;
401+ for ( const service of includedServices ) {
402+ await startService ( service , recreate ) ;
403+ }
228404 break ;
229405
230406 case 'stop' :
231407 await stopContainer ( name ) ;
408+ for ( const service of includedServices ) {
409+ await stopService ( service ) ;
410+ }
411+ break ;
412+
413+ case 'ls' :
414+ await listServices ( ) ;
232415 break ;
233416
234417 default :
235418 console . log ( dockerUsageText ) ;
236- await cliExitWithError ( `Unknown subcommand: ${ subcommand } . Use "start" or "stop ".` ) ;
419+ await cliExitWithError ( `Unknown subcommand: ${ subcommand } . Use "start", "stop", or "ls ".` ) ;
237420 }
238421} ;
0 commit comments