polkovnik-z/docker-jobs-bundle
Docker Jobs Bundle lets Symfony/PHP apps run background jobs in isolated Docker containers. Configure containerized workers, dispatch jobs, and manage queues while keeping processing reproducible, scalable, and separate from your host environment.
Install and register. composer require polkovnik-z/docker-jobs-bundle. On Symfony 4+/5+, add PolkovnikZ\DockerJobsBundle\DockerJobsBundle::class => ['all' => true] to config/bundles.php (Flex will usually do this for you). On Symfony 6 check composer.json constraints first — the last tagged release is from 2022-08, so you may need --with-all-dependencies or a dev-main constraint.
Make the Docker daemon reachable from PHP. This is the real prerequisite, not the bundle install. The PHP process (often www-data or www-data inside a container) must be able to talk to /var/run/docker.sock or a TCP endpoint. Before touching bundle config, verify with docker info as the same OS user PHP runs as, or set DOCKER_HOST in the environment. Nine out of ten "the bundle doesn't work" issues are this.
Look here first, in this order:
config/packages/docker_jobs.yaml after bin/console config:dump-reference docker_jobs — this tells you the real option names (image, volumes, network, env, working dir, timeouts).bin/console list filtered for the bundle's command namespace (likely docker-jobs:* or jobs:*). Commands are the clearest window into intended workflow.DependencyInjection/Configuration.php and Resources/config/services.* — small bundles like this are best read as source. With 19 stars and a single release, the README will be thin and the code is the documentation.JobInterface, DockerJobHandler, or event classes) — those are your extension points.First use case to target: run one trivial job in a container end-to-end before designing anything. Configure the smallest possible image (e.g. alpine:3.19), point a job at echo "hello", and confirm you see stdout and a zero exit code returned to PHP. Once that loop works, the rest is configuration shaping.
Job-as-service. The idiomatic use of a "batch processing with Docker" bundle is to define each job as a Symfony service implementing the bundle's job interface (or tagged with its job tag) and let the bundle resolve and execute it. Keep job classes thin: build the container spec (image, command, env, mounts) and let the bundle handle lifecycle. Do not shell out to docker run yourself — you lose the bundle's cleanup and result handling.
Dispatch from commands/controllers, execute from a worker. Typical split: a producer (HTTP controller, console command, cron) enqueues a job; a long-running worker command (bin/console docker-jobs:worker or similar) picks it up and starts the container. Run the worker under a process supervisor (systemd, supervisor, or a docker compose service with restart: always). Treat it like a Messenger consumer.
One container per job, not per batch, unless pooling is explicit. The straightforward model is: each job invocation = one container, exited and removed on completion. This gives clean isolation and simple semantics. Pooling/pre-warming is an optimization you should only add if startup latency is measurably hurting you.
Exchange data via mounted volumes, not stdout parsing. If a job needs input files or needs to write results back, mount a host path (or named volume) into the container and have the job read/write there. Parsing large outputs out of docker logs is fragile. Keep a per-job working directory keyed by job ID so concurrent runs don't collide.
Pass configuration as environment variables. Container env is the clean boundary between Symfony config and the image. Read %env(...)% parameters in your job service and translate them into container env entries; don't bake secrets into the image.
Collect results deliberately. Decide up front how exit code, stdout, stderr, and timing are surfaced: return value from the job runner, an event subscriber that persists a job record to Doctrine, or a JobResult object. Wire this immediately — retrofitting result capture after you have production jobs is painful.
Integrate with Symfony Messenger if the bundle is queue-based. If the bundle exposes a transport or handler, register it as a Messenger transport so retries, failures, and the messenger:failed workflow come for free. If it doesn't, wrap each Docker invocation in a small handler and let Messenger own the retry policy — it's more reliable than anything you'll build ad hoc.
Use events for observability. Most bundles in this space dispatch pre-run/post-run events. Subscribe to them for structured logging (job ID, image, duration, exit code) and for metrics shipping. This is cheaper than instrumenting each job class.
Docker socket permissions are the #1 failure mode. Error messages surface as permission denied while trying to connect to the Docker daemon socket. Fixes: add the PHP user to the docker group and restart the PHP-FPM/worker, or run rootless Docker, or point at a TCP endpoint with TLS. Do not chmod 777 /var/run/docker.sock. If PHP runs inside a container, mount the socket (-v /var/run/docker.sock:/var/run/docker.sock) — and understand that this is effectively host root access; treat it as a security boundary.
Orphaned containers and images. If the PHP worker is killed mid-job, the container keeps running. Add a reconciliation sweep (label containers with a job ID and reap stale ones on worker boot), and pin a no pull policy plus periodic docker image prune outside the bundle. The bundle likely removes containers on clean exit but cannot recover from a hard kill.
Container networking back to the host. A container cannot reach the host's 127.0.0.1:5432 by default. Use host.docker.internal (Docker Desktop / with --add-host), the compose service name if the worker is also containerized, or a shared network. This trips up almost everyone the first time they try to give a job DB access.
Volume path and permission mismatches. Bind mounts inherit host UID/GID; a container running as non-root may get Permission denied writing to a mounted dir. Either run the container as the host UID (many images accept a --user $(id -u):$(id -g) equivalent via config), or use a named volume with permissive ownership. On SELinux hosts, add the :z/:Z suffix to mounts.
Startup latency is real. Container cold start plus image pull can be seconds. Pre-pull images in your deploy step; never let a production job trigger a fresh pull of a large image on the critical path. If you see timeouts, raise the bundle's startup timeout before you suspect the code.
Exit code handling and stderr. Confirm how the bundle distinguishes a job that ran and returned nonzero from a job that failed to start. Wrap job execution so timeouts, image-not-found, and daemon-unreachable errors all surface as distinct exceptions rather than silent failures.
Debugging workflow. When a job fails, inspect before retrying: the container may still exist with docker ps -a --filter label=..., docker logs <id>, docker inspect <id> for exit code and mounts. Enable the bundle's debug/verbose logging (usually a debug or logger config flag); Symfony's bin/console debug:container <service> confirms what's actually wired.
2022 release = compatibility risk. The last tag predates Symfony 6.3/7 and modern PHP 8.2/8.3. Before adopting, check composer.json (php, symfony/* constraints) and run the test suite locally. If it's abandoned for your Symfony version, the bundle is small enough to fork or vendor — it's a thin wrapper over the Docker API, so the surface you actually depend on is likely a handful of service classes. The extension point you'll most likely need is a custom job handler or a config-level override of image/env/mounts; fork and patch rather than fight the container spec.
Keep the container spec in config, not in job code. Even if the bundle allows per-job overrides, centralize image tags, resource limits, and mounts in docker_jobs.yaml (or a parameter file) so you can bump an image across all jobs in one diff. Job classes should describe what to run, not how the container is configured.
How can I help you explore Laravel packages today?