And then use this script in your Dockerfile’s CMD instead of calling e.g. gunicorn directly.
That said, it’s definitely recommended and best practice to run them in separate machines. There are several reasons:
Dedicated resources. If your workers are overworked (heh) they will not interfere with your web service, and viceversa.
Assymetrical vertical scaling (wow that sounded fancy but it’s really not). If the web server needs a lot of memory but the workers do not, you can scale independently; with the single-machine approach you have less flexibility in this respect. The Celery Beat machine typically can get by with little resources since it’s just queueing up jobs for the workers at intervals.
Horizontal scaling per workload. If your service mainly processes background tasks but doesn’t see a lot of web requests, you can scale the worker process group independently as much as you’d like while keeping only a few web servers. This works the other way too: if you’re mostly serving web requests and have the occasional long-running process, you can scale to many web servers but keep only a few worker nodes.
Single celery beat instance. This is actually important because if you have several beat instances you might end up with duplicated tasks from them. A single beat instance schedules the jobs, and multiple workers take them from the queue for processing.