You have learned the pieces one at a time: running
Redis with Docker,
connecting it to Laravel,
using it as the cache driver,
and running background jobs.
This lesson is the capstone: one Laravel Redis Docker Compose file that runs all of it
together in a single docker-compose.yml, with every part explained.
The full docker-compose.yml
Here is the whole stack. Read it once top to bottom, then we will walk through each part.
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
depends_on:
- redis
networks:
- app-net
redis:
image: redis:7
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
networks:
- app-net
worker:
build:
context: .
dockerfile: Dockerfile
command: php artisan queue:work redis --tries=3
volumes:
- .:/var/www/html
depends_on:
- redis
networks:
- app-net
volumes:
redis-data:
networks:
app-net:
Three services (app, redis, worker), one named volume, one network. That is the
whole production shape of a Laravel app that uses Redis. Now the walkthrough.
The app service (php-fpm)
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
depends_on:
- redis
networks:
- app-net
This is your Laravel application running PHP-FPM. Instead of a ready-made image:, it
uses build: to build from your own Dockerfile (where you install PHP, the redis
extension or predis, and your dependencies). The volumes line mounts your project
code into the container at /var/www/html, so edits on your machine show up inside.
depends_on: redis tells Compose to start the redis service first. Note this only
controls start order, not readiness - your app should still retry the connection if
Redis is a moment slow to accept clients. If you want Compose to wait for Redis to be
genuinely ready, give the redis service a healthcheck that runs redis-cli ping and
change this to the long form depends_on: { redis: { condition: service_healthy } }.
Plain depends_on fires the moment the container starts, before Redis is answering.
The redis service
redis:
image: redis:7
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
networks:
- app-net
This runs the official redis:7 image - no custom build needed. The command overrides
the default startup so Redis requires a password, reading it from the REDIS_PASSWORD
environment variable (the ${...} syntax pulls it from your .env file next to the
compose file). This is the requirepass setting from
securing Redis,
applied on the command line.
Notice there is no ports: line. We deliberately do not publish 6379 to the host.
Redis is reachable only by the other containers on app-net, which is exactly the
private-network safety you want in production. If you need to poke at it locally during
development, you can add ports: ["6379:6379"] temporarily - but never on a public
server.
The redis-data named volume
redis:
...
volumes:
- redis-data:/data
volumes:
redis-data:
Redis writes its persistence files (RDB snapshots and the AOF log, from the
persistence lesson)
to /data. A container's own filesystem is thrown away when the container is recreated,
so we mount a named volume there. The volume lives outside the container and survives
docker compose down and image rebuilds, so your data is still there after a restart or
an upgrade. The bottom-level volumes: redis-data: block is what declares that named
volume to Compose.
The worker service
worker:
build:
context: .
dockerfile: Dockerfile
command: php artisan queue:work redis --tries=3
volumes:
- .:/var/www/html
depends_on:
- redis
networks:
- app-net
Queued jobs need a process that pulls them off the queue and runs them. That is what
php artisan queue:work does, and it must run as its own long-lived process - separate
from the web request cycle. So we build the same image as app (same code, same
dependencies) but give it a different command: run the worker instead of PHP-FPM. It
reads jobs from the redis queue connection and retries a failed job up to three times.
Running it as a separate service means you can scale or restart your workers without touching the web app, and vice versa.
How the services find each other
This is the part that trips people up, so it gets its own section. Inside a Docker Compose network, each service is reachable by its service name as a hostname. Docker runs an internal DNS that resolves those names to the right container.
So from the app and worker containers, the Redis host is literally redis - the name
of the service in the compose file. Not localhost, not an IP address. localhost
inside the app container means the app container itself, where no Redis is running.
This is the single most common Docker + Redis mistake, and it is why the
troubleshooting lesson
opens with Connection refused.
Because all three services share networks: app-net, they can all talk to each other by
name, and nothing outside that network can reach them.
The .env wiring
The compose file builds the plumbing; your Laravel .env tells the app to use it:
REDIS_HOST=redis
REDIS_PASSWORD=a-long-random-password-not-this-one
REDIS_PORT=6379
CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis is the key line - it matches the service name from the compose
file, so Laravel connects to the right container over the private network. REDIS_PASSWORD
must match the one you passed to redis-server --requirepass. CACHE_STORE=redis sends
the cache to Redis (from the cache driver lesson),
QUEUE_CONNECTION=redis sends jobs to Redis so the worker service can pick them up, and
SESSION_DRIVER=redis keeps sessions there too. The same ${REDIS_PASSWORD} value feeds
both the redis service and the app, so define it once in .env.
Bringing it up
docker compose up -d
That starts all three services in the background. Redis comes up first, then the app and the worker connect to it by name. Check that everything is running:
docker compose ps
To confirm Redis itself is healthy, open a shell in the redis container and ping it:
docker compose exec redis redis-cli -a a-long-random-password-not-this-one ping
PONG
Common mistake
Setting REDIS_HOST=localhost (or 127.0.0.1) in .env while running under Compose.
Inside the container, localhost is the container itself, so Laravel tries to connect to
a Redis that is not there and you get Connection refused. Under Docker Compose the host
is always the service name - here, redis.
FAQ
Why is the worker a separate service instead of part of the app container?
A container runs one main process. PHP-FPM serves web requests; queue:work is a
separate long-running loop. Splitting them lets you restart or scale workers on their own
and keeps a crash in one from taking down the other. They share the same built image, so
there is no code duplication.
Do I need to publish the Redis port to use it?
No - and you should not on a server. Services on the same Compose network reach Redis by
name (redis:6379) without any ports: entry. Publishing the port only exposes Redis to
the host and, if the host is public, to the internet. See
securing Redis.
Will my cached data survive a restart?
Cache data, yes, as long as the redis-data named volume is intact - it persists across
docker compose down and rebuilds. Remember that cache is still cache: it can also be
evicted under memory pressure (see
eviction policies),
so your app must always be able to rebuild a missing entry.