Background Workers
Not every service serves HTTP. Queue consumers, reconcilers, cron-style polling loops, and other background workloads don’t listen on a port — run them in worker mode by setting the container port to 0.
What worker mode changes
When containerPort=0, the platform:
- Skips
PORTinjection — the worker doesn’t bind anywhere. - Skips the port reachability check — no
app port X unreachablelog spam and no false-positive “unhealthy” status. - Skips
EXPOSEin the synthesized Dockerfile. - Skips load-balancer route registration — nothing is served. (A generated
*.onlizard.comdomain may still appear on the service, but it won’t respond.)
Enable worker mode
Three equivalent ways:
# New upload-source worker
lizard up --port 0
# Flip an existing service
lizard port 0 --service worker
# Via the config:apply path
lizard service set worker --set containerPort=0Worker mode is a hard switch — a redeploy is needed for the port change to take effect.
Check the current port
lizard port --service workerPrints the current container port, or worker mode when it’s 0.
When not to use worker mode
Don’t use worker mode for a regular HTTP service that’s just slow to start. Worker mode disables the reachability check entirely, so it will hide “the listener never came up” bugs. If your service is meant to serve traffic, keep a real port and fix the startup instead.
Run a queue consumer
Use the redis-worker example. It moves a job from a pending list to a processing list, stores its uppercase result, and acknowledges the job in a Redis transaction. Keep one replica: startup recovery assumes a single consumer. The example uses Python 3.13 and redis-py 6.4.0.
From the directory containing its Dockerfile:
lizard init --name queue-example
lizard add --service worker
lizard add redis
lizard secrets set REDIS_URL='${{redis.REDIS_URL}}' --service worker
lizard up --service worker --port 0Sign in with lizard login if a command reports that authentication is required. Use the database’s actual name if it is not redis. Keep the reference quoted. Set it before uploading the application. On macOS with Lizard CLI 0.3.92, prefix lizard up with COPYFILE_DISABLE=1 to exclude AppleDouble metadata.
Check the final deploy event, then check the worker:
lizard port --service worker
lizard logs --service worker --json
lizard ssh --service worker -- python jobs.py enqueue first-job
lizard ssh --service worker -- python jobs.py result first-jobThe process prints Queue worker ready. If the log tail is empty, continue with the job check below; the test results record the observed logging limit. The result command waits up to 30 seconds and prints {"id": "first-job", "result": "HELLO"}. A running process alone does not prove it can consume a job. Managed Redis is reached from inside the service; the CLI command does not require your laptop to reach its private address.
Verify a restart
For this test service, restart the worker and submit another job:
lizard restart --service worker
lizard ssh --service worker -- python jobs.py result first-job
lizard ssh --service worker -- python jobs.py enqueue after-restart
lizard ssh --service worker -- python jobs.py result after-restartWait for the service to return to running in lizard ps --json if SSH is not yet available. Both results should be HELLO. The first result lives in Redis, so a worker restart does not remove it. On startup the single consumer returns unfinished processing entries to the pending list.
This demo only accepts the JSON jobs created by jobs.py. It does not implement a dead-letter queue, validation for untrusted producers, or exactly-once external side effects. Use an established queue library and idempotent handlers for payments, email or other effects. Redis durability and backup are separate from worker restart behavior; see storage and recovery.
Troubleshooting
| Symptom | Check |
|---|---|
| Missing service | Run lizard add --service worker after creating the project. |
No Queue worker ready | Check the service-scoped REDIS_URL, addon readiness and connection errors. Never print the connection string. |
| Worker expects an HTTP port | Set containerPort=0; changing it on a running service requires a redeploy. |
| No result after 30 seconds | Read worker logs and verify the job ID. This example accepts jobs from its own jobs.py only. |
| Duplicate effects | A retry can run work again. This example stores a result by job ID; external effects require their own idempotency. |
See also
lizard port— show or change a service’s container port.- Service never becomes healthy — the most common worker-mode misconfiguration.
- Managed Addons — Redis, Postgres, and S3 for stateful workloads.
- Python app hosting — a Celery worker is the same image with a different command and no HTTP port.
See scenario test results for checked versions, cloud results and remaining limits.
Updated