@@ -668,3 +668,120 @@ def logout(server: str | None = None):
668668 command_bits .append (QuoteString (server ))
669669
670670 yield StringCommand (* command_bits )
671+
672+
673+ @operation (is_idempotent = False )
674+ def compose (
675+ project_directory : str ,
676+ files : str | list [str ] | None = None ,
677+ project_name : str | None = None ,
678+ present : bool = True ,
679+ pull : str | None = None ,
680+ build : bool = False ,
681+ force_recreate : bool = False ,
682+ remove_orphans : bool = True ,
683+ remove_volumes : bool = False ,
684+ compose_command : str = "docker compose" ,
685+ ):
686+ """
687+ Deploy a Docker Compose stack on the target.
688+
689+ + project_directory: project directory on the target (maps to
690+ ``--project-directory``). Compose discovers ``compose.yaml`` /
691+ ``compose.yml`` / ``docker-compose.yaml`` / ``docker-compose.yml`` inside
692+ this directory by default.
693+ + files: optional path or list of paths to specific compose file(s) on the
694+ target (maps to ``-f``); use this to override the default discovery or to
695+ layer overrides.
696+ + project_name: compose project name (maps to ``--project-name``; defaults
697+ to compose's own default — typically the basename of ``project_directory``)
698+ + present: ``True`` runs ``up -d``, ``False`` runs ``down``
699+ + pull: policy for ``up -d --pull`` (``None``, ``"always"``, ``"missing"``, ``"never"``)
700+ + build: pass ``--build`` on ``up``
701+ + force_recreate: pass ``--force-recreate`` on ``up``
702+ + remove_orphans: pass ``--remove-orphans`` on ``up`` / ``down``
703+ + remove_volumes: pass ``-v`` on ``down`` (only honored when ``present=False``)
704+ + compose_command: compose binary to invoke; use ``"docker-compose"`` for v1
705+
706+ This operation is not idempotent from pyinfra's perspective: it always shells out
707+ to compose. Docker itself skips services whose definition has not changed, so
708+ re-runs are safe and cheap.
709+
710+ ``_env`` and ``_chdir`` are the standard pyinfra global operation kwargs and
711+ work here without any special handling, which is useful for compose variable
712+ interpolation.
713+
714+ **Examples:**
715+
716+ .. code:: python
717+
718+ from pyinfra.operations import docker, files
719+
720+ # Upload the compose file then bring the stack up using compose's
721+ # default discovery (looks for compose.yaml / docker-compose.yml in
722+ # the project directory)
723+ files.put(
724+ name="Upload compose file",
725+ src="files/docker-compose.yml",
726+ dest="/srv/app/docker-compose.yml",
727+ )
728+ docker.compose(
729+ name="Deploy app stack",
730+ project_directory="/srv/app",
731+ project_name="app",
732+ _env={"DIR_STORAGE": "/srv/app/data"},
733+ )
734+
735+ # Layer a base compose file with an override
736+ docker.compose(
737+ name="Deploy app stack with override",
738+ project_directory="/srv/app",
739+ files=["docker-compose.yml", "docker-compose.prod.yml"],
740+ )
741+
742+ # Tear the stack down, including named volumes
743+ docker.compose(
744+ name="Remove app stack",
745+ project_directory="/srv/app",
746+ project_name="app",
747+ present=False,
748+ remove_volumes=True,
749+ )
750+ """
751+ if not project_directory :
752+ raise OperationError ("docker.compose requires a project_directory" )
753+
754+ if pull is not None and pull not in ("always" , "missing" , "never" ):
755+ raise OperationError (
756+ 'docker.compose pull must be one of None, "always", "missing", "never"' ,
757+ )
758+
759+ file_list : list [str ] = (
760+ [files ] if isinstance (files , str ) else list (files ) if files is not None else []
761+ )
762+
763+ command_bits : list [str | QuoteString ] = [compose_command ]
764+ command_bits .extend (["--project-directory" , QuoteString (project_directory )])
765+ if project_name :
766+ command_bits .extend (["--project-name" , QuoteString (project_name )])
767+ for compose_file in file_list :
768+ command_bits .extend (["-f" , QuoteString (compose_file )])
769+
770+ if present :
771+ command_bits .append ("up -d" )
772+ if pull :
773+ command_bits .extend (["--pull" , pull ])
774+ if build :
775+ command_bits .append ("--build" )
776+ if force_recreate :
777+ command_bits .append ("--force-recreate" )
778+ if remove_orphans :
779+ command_bits .append ("--remove-orphans" )
780+ else :
781+ command_bits .append ("down" )
782+ if remove_volumes :
783+ command_bits .append ("-v" )
784+ if remove_orphans :
785+ command_bits .append ("--remove-orphans" )
786+
787+ yield StringCommand (* command_bits )
0 commit comments