Docker Compose Tutorial: FastAPI, PostgreSQL and Redis With Persistent Data addresses a problem that appears when teams move from working containers to dependable services. This tutorial gives commands, a realistic example, failure diagnosis and safe operational practices. Perform privileged commands only on infrastructure you administer. Use documented, supported component versions and test against non-production data first.
What the three-service example teaches
Developers often run a web server in one terminal, PostgreSQL in another, and Redis on an unrelated host. A Compose project keeps this local integration environment reproducible. The API should talk to Postgres using the Compose service name rather than 127.0.0.1 because localhost inside the API container refers to itself. Likewise, Redis can be addressed using its service name on the shared project network. This tutorial describes a development stack, not a production high-availability database deployment.
Define services and networks
In a compose.yaml, add services named api, db and cache. The API may build from your local Dockerfile and expose only the development port 127.0.0.1:8000:8000. Use the official Postgres image with a supported major version, persistent data volume and development-only credentials. Redis can use an explicit image tag and an internal-only service endpoint. Avoid publishing database ports unless a local tool genuinely requires them, and bind those ports to loopback instead of every interface when appropriate.
Set configuration and persistence
Provide DATABASE_URL pointing to the db service and REDIS_URL pointing to cache from the API’s environment. Store reusable, non-secret defaults in an example environment file; keep real secrets outside Git. Mount a named volume for PostgreSQL’s durable data directory, verifying the correct path for the chosen image version because official layouts may change between releases. Remember that docker compose down -v deletes named volumes associated with the project; never run that as an innocent cleanup command on important data.
Handle startup dependencies correctly
A depends_on entry controls service relationships, but starting a database process is not the same as being ready for queries. Add an appropriate Postgres health check, and configure an API readiness policy or retry logic for transient connection failures. Use docker compose config to validate the merged YAML and docker compose up --build -d to start the stack. Then inspect docker compose ps and docker compose logs --tail=100 api. Avoid sleeping for a hardcoded ten seconds and assuming every laptop or CI agent starts equally fast.
Verify each layer
Request the API health endpoint; then run a database query from an authorized service container using a password-safe method. Confirm Redis connectivity through your application’s integration test rather than exposing Redis to the internet. Restart the API and verify that database records persist, then restart the full Compose stack without deleting volumes. A good demo creates one record, reads it back, restarts, and reads it again. This simple exercise catches accidental ephemeral storage that a “containers are running” screenshot would miss.
Know when to outgrow Compose
Compose works well for development, integration tests and modest single-host workloads. It does not automatically provide managed failover, backup guarantees or Kubernetes-style scheduling. In production, define database dumps and restores, credential rotation, resource limits, image patching and deployment ownership. Keep the database accessible only to intended services. Document whether Redis is just a cache or contains irreplaceable queue state; persistence requirements differ. If the app moves to Kubernetes later, carry forward these data boundaries and health criteria instead of mechanically translating every YAML field.
How to validate the installation end to end
Do not stop at a successful command. Record the starting state and build an observable test that crosses the same interface your users rely on. First inspect process or container status; then verify connectivity; then exercise one representative application operation and examine its logs. Repeat after a restart or recreation. If data should persist, explicitly test persistence; if credentials should not be exposed, search logs and image settings for leaks. This separates “Docker accepted my configuration” from “the service behaves correctly.”
Check which addresses and ports are actually reachable from another machine, not just from inside the container. Verify whether the example uses a local-only bind, a development credential, or a disposable volume so nobody accidentally carries a lab shortcut into production. Record the exact command and version that produced each result for future troubleshooting.
Deployment review and rollback notes
Before promoting a container change, specify the image digest, configuration version, persistent storage ownership, expected resource use and recovery trigger. Make one meaningful change at a time. Build in CI, deploy to a test environment, and automate a health or smoke test that would have caught a previous incident. If a release degrades, return to the last known-good image and configuration; database migrations require their own backward-compatibility plan. Keep Docker daemon access restricted, log secret-free operational events and update the runbook as soon as a new failure mode appears.
Frequently asked questions
Should I use the same commands on a production host?
Use them as an understood starting point, not an unreviewed production script. Confirm image tags, daemon version, persistence rules, access boundaries and backup availability. Some examples deliberately bind ports to localhost to prevent accidental exposure.
What if the container starts but the application fails?
Inspect container logs, the real application listening address, environment variables and service dependencies. Check whether the failing request reaches the application before changing network rules or restarting unrelated services.
Official references and further reading
Use the upstream Docker documentation for this topic as the primary source for version-specific details. For a practical application example, compare our Python RAG chatbot implementation, which also benefits from repeatable images, secure configuration and observable deployment processes.
Integration exercise: prove database persistence
Start the stack, create a disposable record through the API, and verify the response contains its generated identifier. Run docker compose restart api and fetch the record again. Next, stop and recreate the entire stack without deleting named volumes and repeat the lookup. If the record disappears, inspect the Postgres image’s actual data directory and Compose volume mapping. Do not use down -v for this exercise, since that intentionally removes volume data.
Simulate a database unavailable scenario in your lab and see whether the API reports an actionable error or blocks indefinitely. Add a timeout and bounded retry policy if needed. This connects Compose health checks to application behavior. Read our PostgreSQL volume backup guide before treating persistence as a backup.




