{"id":188,"date":"2026-10-10T16:00:50","date_gmt":"2026-10-10T16:00:50","guid":{"rendered":"https:\/\/devdojo.co.in\/?p=188"},"modified":"2026-10-10T16:00:50","modified_gmt":"2026-10-10T16:00:50","slug":"docker-compose-tutorial","status":"publish","type":"post","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/","title":{"rendered":"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together"},"content":{"rendered":"\n<div class=\"dd-post\">\n<style>\n.single article:has(.dd-hero) .entry-media{display:none}\n.dd-post{--dd-navy:#0b1020;--dd-blue:#3178c6;--dd-violet:#7c5cff;--dd-ink:#1d2333;--dd-muted:#5b6478;--dd-line:#e3e7f0;--dd-soft:#f5f7fc;line-height:1.75;color:var(--dd-ink)}\n.dd-post h2{margin-top:2.2em;scroll-margin-top:90px}\n.dd-post h3{margin-top:1.6em;scroll-margin-top:90px}\n.dd-post a{color:var(--dd-blue)}\n.dd-post .dd-hero{margin:0 0 1.8em;border-radius:16px;overflow:hidden;box-shadow:0 12px 40px rgba(11,16,32,.35)}\n.dd-post .dd-hero svg{display:block;width:100%;height:auto}\n.dd-post .dd-hero .node{transition:transform .25s ease,filter .25s ease;transform-box:fill-box;transform-origin:center;cursor:default}\n.dd-post .dd-hero .node:hover{transform:scale(1.07);filter:drop-shadow(0 0 14px rgba(124,92,255,.85))}\n.dd-post .dd-hero .node:hover rect{stroke:#ffffff}\n.dd-post pre{background:#0f1430 !important;color:#e6e9f5 !important;border-radius:12px;padding:18px 20px;overflow-x:auto;border:1px solid #232a52;font-size:14.5px !important;line-height:1.6;margin:1.2em 0}\n.dd-post pre code{font-size:1em !important;background:none !important;color:inherit !important;padding:0 !important;font-family:\"JetBrains Mono\",\"Fira Code\",Consolas,Menlo,monospace;white-space:pre}\n.dd-post :not(pre)>code{background:#eef1fb;color:#3b2fb0;padding:2px 6px;border-radius:5px;font-size:.92em}\n.dd-post .dd-file{display:inline-block;background:#232a52;color:#c9d1ff;font:600 12.5px\/1 Consolas,Menlo,monospace;padding:7px 12px;border-radius:8px 8px 0 0;margin:1.2em 0 -1.2em 0;position:relative;z-index:1}\n.dd-post .dd-file + pre{border-top-left-radius:0}\n.dd-post .dd-summary{background:linear-gradient(135deg,#f3f0ff,#eaf3ff);border:1px solid #d9d2ff;border-left:5px solid var(--dd-violet);border-radius:12px;padding:18px 22px;margin:1.4em 0}\n.dd-post .dd-summary strong.dd-label{display:block;color:var(--dd-violet);text-transform:uppercase;letter-spacing:.06em;font-size:.82em;margin-bottom:6px}\n.dd-post .dd-summary ul{margin:.3em 0 0 1.1em;padding:0}\n.dd-post .dd-toc{background:var(--dd-soft);border:1px solid var(--dd-line);border-radius:12px;padding:16px 22px;margin:1.4em 0}\n.dd-post .dd-toc strong{display:block;margin-bottom:6px}\n.dd-post .dd-toc ol{margin:0 0 0 1.2em;padding:0;columns:2;column-gap:32px}\n.dd-post .dd-toc li{margin:.2em 0;break-inside:avoid}\n.dd-post .dd-tip,.dd-post .dd-note,.dd-post .dd-warn{border-radius:10px;padding:14px 18px;margin:1.3em 0}\n.dd-post .dd-tip{background:#ecfdf3;border-left:5px solid #16a34a}\n.dd-post .dd-note{background:#eef5ff;border-left:5px solid var(--dd-blue)}\n.dd-post .dd-warn{background:#fff7e8;border-left:5px solid #f59e0b}\n.dd-post .dd-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:14px;margin:1.3em 0}\n.dd-post .dd-card{background:#fff;border:1px solid var(--dd-line);border-radius:12px;padding:16px 18px;box-shadow:0 2px 10px rgba(20,30,70,.05)}\n.dd-post .dd-card b{display:block;color:var(--dd-violet);margin-bottom:4px}\n.dd-post table{width:100%;border-collapse:collapse;margin:1.3em 0;font-size:.95em}\n.dd-post th{background:#151b3b;color:#fff;text-align:left;padding:10px 12px}\n.dd-post td{border-bottom:1px solid var(--dd-line);padding:10px 12px;vertical-align:top}\n.dd-post tr:nth-child(even) td{background:#f8f9fd}\n.dd-post .dd-table-wrap{overflow-x:auto}\n.dd-post figure{margin:1.6em 0;text-align:center}\n.dd-post figure svg{width:100%;height:auto;max-width:900px;border-radius:12px}\n.dd-post figcaption{font-size:.9em;color:var(--dd-muted);margin-top:8px}\n.dd-post details{border:1px solid var(--dd-line);border-radius:10px;padding:12px 16px;margin:.7em 0;background:#fff}\n.dd-post details summary{cursor:pointer;font-weight:600}\n.dd-post details[open] summary{color:var(--dd-violet);margin-bottom:6px}\n.dd-post .dd-tabs{margin:1.4em 0;border:1px solid var(--dd-line);border-radius:12px;overflow:hidden;background:#fff}\n.dd-post .dd-tabs input{position:absolute;opacity:0;pointer-events:none}\n.dd-post .dd-tabs .dd-tab-labels{display:flex;flex-wrap:wrap;background:#151b3b}\n.dd-post .dd-tabs label{flex:1 1 auto;text-align:center;padding:12px 14px;color:#b9c2e8;cursor:pointer;font-weight:600;font-size:.95em;border-bottom:3px solid transparent;margin:0}\n.dd-post .dd-tabs label:hover{color:#fff}\n.dd-post .dd-tab-panel{display:none;padding:6px 18px 14px}\n.dd-post #dd-t1:checked ~ .dd-tab-labels label[for=\"dd-t1\"],.dd-post #dd-t2:checked ~ .dd-tab-labels label[for=\"dd-t2\"],.dd-post #dd-t3:checked ~ .dd-tab-labels label[for=\"dd-t3\"]{color:#fff;border-bottom-color:#7c5cff;background:#1f2752}\n.dd-post #dd-t1:checked ~ .dd-p1,.dd-post #dd-t2:checked ~ .dd-p2,.dd-post #dd-t3:checked ~ .dd-p3{display:block}\n.dd-post .dd-cta{background:linear-gradient(135deg,#0b1020,#151b3b);color:#e6e9f5;border-radius:16px;padding:26px 28px;margin:2em 0;text-align:center}\n.dd-post .dd-cta h3{color:#fff;margin:0 0 8px}\n.dd-post .dd-cta a{color:#a8b8ff;font-weight:600}\n<\/style>\n\n<div class=\"dd-hero\">\n<svg viewBox=\"0 0 1200 520\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"ddHeroTitle ddHeroDesc\">\n<title id=\"ddHeroTitle\">Docker Compose Tutorial<\/title>\n<desc id=\"ddHeroDesc\">Banner showing a Node.js API, PostgreSQL and Redis containers connected inside one Docker Compose network, with data dots moving between them.<\/desc>\n<defs>\n<linearGradient id=\"ddBg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#0b1020\"\/><stop offset=\"1\" stop-color=\"#151b3b\"\/><\/linearGradient>\n<linearGradient id=\"ddAcc\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#3178c6\"\/><stop offset=\"1\" stop-color=\"#7c5cff\"\/><\/linearGradient>\n<pattern id=\"ddGrid\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\"><path d=\"M40 0H0V40\" fill=\"none\" stroke=\"#ffffff\" stroke-opacity=\".04\"\/><\/pattern>\n<\/defs>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddBg)\"\/>\n<rect x=\"0\" y=\"-60\" width=\"1200\" height=\"640\" fill=\"url(#ddGrid)\"\/>\n<circle cx=\"1080\" cy=\"40\" r=\"180\" fill=\"#7c5cff\" opacity=\".10\"\/>\n<circle cx=\"120\" cy=\"500\" r=\"160\" fill=\"#3178c6\" opacity=\".10\"\/>\n<text x=\"70\" y=\"110\" fill=\"#8fa2ff\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"20\" font-weight=\"700\" letter-spacing=\"3\">DEVDOJO \u00b7 DEVOPS<\/text>\n<text x=\"70\" y=\"172\" fill=\"#ffffff\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"54\" font-weight=\"800\">Docker Compose Tutorial<\/text>\n<text x=\"70\" y=\"218\" fill=\"#c6cdf0\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"25\">Run Node.js, PostgreSQL &amp; Redis with one command<\/text>\n<rect x=\"70\" y=\"240\" width=\"250\" height=\"5\" rx=\"2.5\" fill=\"url(#ddAcc)\"\/>\n<g font-family=\"Consolas,Menlo,monospace\" font-size=\"20\">\n<rect x=\"70\" y=\"290\" width=\"440\" height=\"150\" rx=\"14\" fill=\"#0f1430\" stroke=\"#2a3366\"\/>\n<circle cx=\"96\" cy=\"314\" r=\"6\" fill=\"#ff5f57\"\/><circle cx=\"116\" cy=\"314\" r=\"6\" fill=\"#febc2e\"\/><circle cx=\"136\" cy=\"314\" r=\"6\" fill=\"#28c840\"\/>\n<text x=\"92\" y=\"358\" fill=\"#7c5cff\">$<\/text><text x=\"114\" y=\"358\" fill=\"#e6e9f5\">docker compose up -d<\/text>\n<text x=\"92\" y=\"392\" fill=\"#28c840\">\u2714<\/text><text x=\"114\" y=\"392\" fill=\"#9aa5cf\">db, cache  Healthy<\/text>\n<text x=\"92\" y=\"424\" fill=\"#28c840\">\u2714<\/text><text x=\"114\" y=\"424\" fill=\"#9aa5cf\">api        Started<\/text>\n<\/g>\n<rect x=\"600\" y=\"270\" width=\"540\" height=\"220\" rx=\"20\" fill=\"none\" stroke=\"#3178c6\" stroke-opacity=\".55\" stroke-dasharray=\"8 7\"\/>\n<text x=\"622\" y=\"298\" fill=\"#8fa2ff\" font-family=\"Consolas,Menlo,monospace\" font-size=\"16\">compose network<\/text>\n<path id=\"ddP1\" d=\"M770 380 C 830 330, 870 330, 930 330\" fill=\"none\" stroke=\"#3a4685\" stroke-width=\"2\"\/>\n<path id=\"ddP2\" d=\"M770 400 C 830 450, 870 450, 930 450\" fill=\"none\" stroke=\"#3a4685\" stroke-width=\"2\"\/>\n<circle r=\"6\" fill=\"#7c5cff\"><animateMotion dur=\"2.4s\" repeatCount=\"indefinite\"><mpath href=\"#ddP1\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#3178c6\"><animateMotion dur=\"2.9s\" repeatCount=\"indefinite\" keyPoints=\"1;0\" keyTimes=\"0;1\" calcMode=\"linear\"><mpath href=\"#ddP1\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#ff6b9a\"><animateMotion dur=\"2s\" repeatCount=\"indefinite\"><mpath href=\"#ddP2\"\/><\/animateMotion><\/circle>\n<circle r=\"6\" fill=\"#3178c6\"><animateMotion dur=\"2.6s\" repeatCount=\"indefinite\" keyPoints=\"1;0\" keyTimes=\"0;1\" calcMode=\"linear\"><mpath href=\"#ddP2\"\/><\/animateMotion><\/circle>\n<g class=\"node\"><rect x=\"630\" y=\"350\" width=\"140\" height=\"80\" rx=\"14\" fill=\"#16204a\" stroke=\"#3178c6\" stroke-width=\"2\"\/><text x=\"700\" y=\"385\" text-anchor=\"middle\" fill=\"#ffffff\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"21\" font-weight=\"700\">api<\/text><text x=\"700\" y=\"410\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-family=\"Consolas,Menlo,monospace\" font-size=\"15\">Node.js :3000<\/text><\/g>\n<g class=\"node\"><rect x=\"930\" y=\"295\" width=\"180\" height=\"70\" rx=\"14\" fill=\"#1b1a4a\" stroke=\"#7c5cff\" stroke-width=\"2\"\/><text x=\"1020\" y=\"326\" text-anchor=\"middle\" fill=\"#ffffff\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"21\" font-weight=\"700\">db<\/text><text x=\"1020\" y=\"350\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-family=\"Consolas,Menlo,monospace\" font-size=\"15\">PostgreSQL :5432<\/text><\/g>\n<g class=\"node\"><rect x=\"930\" y=\"415\" width=\"180\" height=\"70\" rx=\"14\" fill=\"#2a1736\" stroke=\"#ff6b9a\" stroke-width=\"2\"\/><text x=\"1020\" y=\"446\" text-anchor=\"middle\" fill=\"#ffffff\" font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"21\" font-weight=\"700\">cache<\/text><text x=\"1020\" y=\"470\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-family=\"Consolas,Menlo,monospace\" font-size=\"15\">Redis :6379<\/text><\/g>\n<\/svg>\n<\/div>\n\n<p>If you have ever followed a project&#8217;s README that says &#8220;install Node, then install PostgreSQL, then install Redis, then set ten environment variables&#8221;, you know how painful local setup can be. This <strong>Docker Compose tutorial<\/strong> fixes that. You will write one small file, run one command, and get a working Node.js API talking to a PostgreSQL database and a Redis cache \u2014 on any laptop, Windows, macOS or Linux.<\/p>\n\n<div class=\"dd-summary\">\n<strong class=\"dd-label\">Quick Summary<\/strong>\n<ul>\n<li><b>Docker Compose<\/b> lets you describe many containers (API, database, cache) in one <code>compose.yaml<\/code> file and start them all with <code>docker compose up<\/code>.<\/li>\n<li>Services talk to each other using their <b>service names<\/b> (like <code>db<\/code> and <code>cache<\/code>), not <code>localhost<\/code>.<\/li>\n<li>Use <b>health checks<\/b> with <code>depends_on: condition: service_healthy<\/code> so your API waits until the database is really ready.<\/li>\n<li>Use <b>named volumes<\/b> so your database data survives restarts. For PostgreSQL 18+, mount the volume at <code>\/var\/lib\/postgresql<\/code>.<\/li>\n<li>Use <b>Compose Watch<\/b> (<code>docker compose watch<\/code>) to see code changes live without rebuilding by hand.<\/li>\n<\/ul>\n<\/div>\n\n<nav class=\"dd-toc\" aria-label=\"Table of contents\">\n<strong>What you will learn<\/strong>\n<ol>\n<li><a href=\"#what-is-docker-compose\">What is Docker Compose?<\/a><\/li>\n<li><a href=\"#before-you-start\">Before you start<\/a><\/li>\n<li><a href=\"#project-structure\">Project structure<\/a><\/li>\n<li><a href=\"#node-api\">Step 1: The Node.js API<\/a><\/li>\n<li><a href=\"#dockerfile\">Step 2: The Dockerfile<\/a><\/li>\n<li><a href=\"#compose-file\">Step 3: The compose.yaml file<\/a><\/li>\n<li><a href=\"#run-it\">Step 4: Run and test it<\/a><\/li>\n<li><a href=\"#live-reload\">Step 5: Live reload with Compose Watch<\/a><\/li>\n<li><a href=\"#everyday-commands\">Everyday commands<\/a><\/li>\n<li><a href=\"#common-errors\">Common errors and fixes<\/a><\/li>\n<li><a href=\"#best-practices\">Best practices<\/a><\/li>\n<li><a href=\"#faq\">FAQ<\/a><\/li>\n<\/ol>\n<\/nav>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"what-is-docker-compose\">What is Docker Compose? (Docker Compose tutorial basics)<\/h2>\n\n<p>Docker runs one <b>container<\/b> at a time \u2014 a small, isolated box that has your app and everything it needs. Real apps usually need more than one box: an API, a database, maybe a cache or a queue. Starting each one with a long <code>docker run ...<\/code> command, creating networks by hand and remembering the right order gets old quickly.<\/p>\n\n<p><b>Docker Compose<\/b> is the tool that solves this. You write a YAML file that lists your <em>services<\/em> (containers), how they are built or which image they use, which ports they open, which environment variables they need, and which data should be saved. Then a single command starts the whole stack. Think of it as a <b>recipe card for your whole app<\/b>.<\/p>\n\n<div class=\"dd-cards\">\n<div class=\"dd-card\"><b>One file<\/b>Your entire local setup lives in <code>compose.yaml<\/code> and is committed to Git with your code.<\/div>\n<div class=\"dd-card\"><b>One command<\/b><code>docker compose up<\/code> builds, creates a network, and starts every service in the right order.<\/div>\n<div class=\"dd-card\"><b>Same everywhere<\/b>A new teammate gets the exact same PostgreSQL and Redis versions as you, in minutes.<\/div>\n<\/div>\n\n<figure>\n<svg viewBox=\"0 0 900 380\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"ddF1t ddF1d\">\n<title id=\"ddF1t\">How the services connect in Docker Compose<\/title>\n<desc id=\"ddF1d\">Your browser calls localhost port 3000, which maps to the api container. The api container reaches db on port 5432 and cache on port 6379 over the private compose network. The db container stores data in a named volume.<\/desc>\n<rect width=\"900\" height=\"380\" rx=\"14\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Arial,sans-serif\">\n<rect x=\"30\" y=\"150\" width=\"150\" height=\"80\" rx=\"12\" fill=\"#1a2148\" stroke=\"#5b6aa8\"\/>\n<text x=\"105\" y=\"185\" text-anchor=\"middle\" fill=\"#fff\" font-size=\"17\" font-weight=\"700\">Your browser<\/text>\n<text x=\"105\" y=\"208\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-size=\"13\">localhost:3000<\/text>\n<rect x=\"230\" y=\"40\" width=\"640\" height=\"300\" rx=\"16\" fill=\"none\" stroke=\"#3178c6\" stroke-dasharray=\"7 6\"\/>\n<text x=\"250\" y=\"66\" fill=\"#8fa2ff\" font-size=\"14\" font-family=\"Consolas,Menlo,monospace\">default network (created by Compose)<\/text>\n<rect x=\"280\" y=\"150\" width=\"160\" height=\"80\" rx=\"12\" fill=\"#16204a\" stroke=\"#3178c6\" stroke-width=\"2\"\/>\n<text x=\"360\" y=\"185\" text-anchor=\"middle\" fill=\"#fff\" font-size=\"18\" font-weight=\"700\">api<\/text>\n<text x=\"360\" y=\"208\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-size=\"13\">Node.js \u00b7 port 3000<\/text>\n<rect x=\"560\" y=\"90\" width=\"170\" height=\"80\" rx=\"12\" fill=\"#1b1a4a\" stroke=\"#7c5cff\" stroke-width=\"2\"\/>\n<text x=\"645\" y=\"125\" text-anchor=\"middle\" fill=\"#fff\" font-size=\"18\" font-weight=\"700\">db<\/text>\n<text x=\"645\" y=\"148\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-size=\"13\">PostgreSQL \u00b7 5432<\/text>\n<rect x=\"560\" y=\"220\" width=\"170\" height=\"80\" rx=\"12\" fill=\"#2a1736\" stroke=\"#ff6b9a\" stroke-width=\"2\"\/>\n<text x=\"645\" y=\"255\" text-anchor=\"middle\" fill=\"#fff\" font-size=\"18\" font-weight=\"700\">cache<\/text>\n<text x=\"645\" y=\"278\" text-anchor=\"middle\" fill=\"#9aa5cf\" font-size=\"13\">Redis \u00b7 6379<\/text>\n<rect x=\"760\" y=\"100\" width=\"95\" height=\"60\" rx=\"10\" fill=\"#132a24\" stroke=\"#28c840\"\/>\n<text x=\"807\" y=\"126\" text-anchor=\"middle\" fill=\"#bff5c9\" font-size=\"13\" font-weight=\"700\">db-data<\/text>\n<text x=\"807\" y=\"145\" text-anchor=\"middle\" fill=\"#8fd6a0\" font-size=\"12\">volume<\/text>\n<\/g>\n<g stroke=\"#9aa5cf\" stroke-width=\"2\" fill=\"none\">\n<path d=\"M180 190 H276\" marker-end=\"url(#ddArr)\"\/>\n<path d=\"M440 180 L556 135\" marker-end=\"url(#ddArr)\"\/>\n<path d=\"M440 200 L556 255\" marker-end=\"url(#ddArr)\"\/>\n<path d=\"M730 130 H756\" stroke=\"#28c840\" marker-end=\"url(#ddArr2)\"\/>\n<\/g>\n<defs>\n<marker id=\"ddArr\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#9aa5cf\"\/><\/marker>\n<marker id=\"ddArr2\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\"><path d=\"M0 0L10 5L0 10z\" fill=\"#28c840\"\/><\/marker>\n<\/defs>\n<g font-family=\"Consolas,Menlo,monospace\" font-size=\"12\" fill=\"#c6cdf0\">\n<text x=\"186\" y=\"180\">port 3000<\/text>\n<text x=\"452\" y=\"140\">db:5432<\/text>\n<text x=\"452\" y=\"262\">cache:6379<\/text>\n<\/g>\n<\/svg>\n<figcaption>Only the API is exposed to your computer. The database and cache stay private inside the Compose network and are reached by name.<\/figcaption>\n<\/figure>\n\n<h2 id=\"before-you-start\">Before you start<\/h2>\n\n<p>You need two things:<\/p>\n<ul>\n<li><b>Docker Desktop<\/b> (Windows\/macOS) or <b>Docker Engine with the Compose plugin<\/b> (Linux). Docker Desktop already includes Compose.<\/li>\n<li>A terminal and a code editor like VS Code.<\/li>\n<\/ul>\n\n<p>Check that both Docker and Compose are installed:<\/p>\n<pre><code>docker --version\ndocker compose version<\/code><\/pre>\n\n<div class=\"dd-note\"><b>Note:<\/b> The modern command is <code>docker compose<\/code> (with a space). The old <code>docker-compose<\/code> (with a hyphen) was the Python-based version 1, which is no longer maintained. Every command in this guide uses the new form.<\/div>\n\n<h2 id=\"project-structure\">Project structure<\/h2>\n\n<p>We will build a tiny &#8220;visit counter&#8221; API. Every time you open it, it saves a row in PostgreSQL and increases a counter in Redis. Simple, but it touches every important Compose feature. Create this folder layout:<\/p>\n\n<pre><code>my-app\/\n\u251c\u2500\u2500 compose.yaml\n\u251c\u2500\u2500 .env\n\u2514\u2500\u2500 api\/\n    \u251c\u2500\u2500 Dockerfile\n    \u251c\u2500\u2500 .dockerignore\n    \u251c\u2500\u2500 package.json\n    \u2514\u2500\u2500 src\/\n        \u2514\u2500\u2500 index.js<\/code><\/pre>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"node-api\">Step 1: The Node.js API<\/h2>\n\n<p>First, the <code>package.json<\/code>. We use ES modules (<code>\"type\": \"module\"<\/code>), Express 5, the <code>pg<\/code> driver for PostgreSQL and the official <code>redis<\/code> client.<\/p>\n\n<span class=\"dd-file\">api\/package.json<\/span>\n<pre><code>{\n  \"name\": \"visit-counter-api\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"node src\/index.js\",\n    \"dev\": \"node --watch src\/index.js\"\n  },\n  \"dependencies\": {\n    \"express\": \"^5.1.0\",\n    \"pg\": \"^8.13.0\",\n    \"redis\": \"^5.0.0\"\n  }\n}<\/code><\/pre>\n\n<p>Now the API itself. Notice that it does <b>not<\/b> hard-code any hostnames or passwords. It reads <code>DATABASE_URL<\/code> and <code>REDIS_URL<\/code> from environment variables, which Compose will provide.<\/p>\n\n<span class=\"dd-file\">api\/src\/index.js<\/span>\n<pre><code>import express from 'express';\nimport pg from 'pg';\nimport { createClient } from 'redis';\n\nconst app = express();\n\n\/\/ PostgreSQL connection pool (host comes from DATABASE_URL)\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\n\n\/\/ Redis client (host comes from REDIS_URL)\nconst cache = createClient({ url: process.env.REDIS_URL });\ncache.on('error', (err) =&gt; console.error('Redis error:', err.message));\nawait cache.connect();\n\n\/\/ Create the table once, if it does not exist yet\nawait pool.query(`\n  CREATE TABLE IF NOT EXISTS visits (\n    id SERIAL PRIMARY KEY,\n    visited_at TIMESTAMPTZ NOT NULL DEFAULT now()\n  )\n`);\n\napp.get('\/', async (req, res) =&gt; {\n  await pool.query('INSERT INTO visits DEFAULT VALUES');\n  const { rows } = await pool.query('SELECT COUNT(*)::int AS total FROM visits');\n  const hits = await cache.incr('hits');\n\n  res.json({\n    message: 'Hello from Docker Compose!',\n    totalVisits: rows[0].total, \/\/ stored in PostgreSQL\n    cacheHits: hits             \/\/ stored in Redis\n  });\n});\n\napp.get('\/health', (req, res) =&gt; res.json({ status: 'ok' }));\n\nconst port = process.env.PORT || 3000;\napp.listen(port, () =&gt; console.log(`API running on port ${port}`));<\/code><\/pre>\n\n<div class=\"dd-tip\"><b>Tip:<\/b> Top-level <code>await<\/code> works here because the file is an ES module. If the database is not ready, this code would crash on startup \u2014 that is exactly why we will add health checks in Step 3.<\/div>\n\n<h2 id=\"dockerfile\">Step 2: The Dockerfile<\/h2>\n\n<p>The Dockerfile tells Docker how to build an image for our API. We use the official <code>node:24-alpine<\/code> image (Node.js 24 is an LTS release, and Alpine keeps the image small).<\/p>\n\n<span class=\"dd-file\">api\/Dockerfile<\/span>\n<pre><code>FROM node:24-alpine\n\nWORKDIR \/app\n\n# Copy only package files first, so \"npm install\" is cached\nCOPY package*.json .\/\nRUN npm install --omit=dev\n\n# Now copy the rest of the source code\nCOPY . .\n\nEXPOSE 3000\nCMD [\"npm\", \"start\"]<\/code><\/pre>\n\n<p>And a <code>.dockerignore<\/code> so your local <code>node_modules<\/code> and secrets never get copied into the image:<\/p>\n\n<span class=\"dd-file\">api\/.dockerignore<\/span>\n<pre><code>node_modules\nnpm-debug.log\n.env<\/code><\/pre>\n\n<div class=\"dd-note\"><b>Why copy <code>package.json<\/code> first?<\/b> Docker caches each step. If you only change <code>index.js<\/code>, Docker reuses the cached <code>npm install<\/code> layer, so rebuilds take seconds instead of minutes.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"compose-file\">Step 3: The compose.yaml file<\/h2>\n\n<p>This is the heart of the Docker Compose tutorial. First, put your settings in a <code>.env<\/code> file next to <code>compose.yaml<\/code>. Compose reads this file automatically and replaces <code>${VARIABLE}<\/code> placeholders with these values.<\/p>\n\n<span class=\"dd-file\">.env<\/span>\n<pre><code>POSTGRES_USER=devdojo\nPOSTGRES_PASSWORD=change-me-please\nPOSTGRES_DB=appdb<\/code><\/pre>\n\n<div class=\"dd-warn\"><b>Important:<\/b> Add <code>.env<\/code> to your <code>.gitignore<\/code>. Commit a <code>.env.example<\/code> with dummy values instead, so teammates know which variables they need.<\/div>\n\n<p>Now the main file:<\/p>\n\n<span class=\"dd-file\">compose.yaml<\/span>\n<pre><code>services:\n  api:\n    build: .\/api\n    ports:\n      - \"3000:3000\"\n    environment:\n      DATABASE_URL: postgres:\/\/${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432\/${POSTGRES_DB}\n      REDIS_URL: redis:\/\/cache:6379\n    depends_on:\n      db:\n        condition: service_healthy\n      cache:\n        condition: service_healthy\n    restart: unless-stopped\n\n  db:\n    image: postgres:18-alpine\n    environment:\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: ${POSTGRES_DB}\n    volumes:\n      - db-data:\/var\/lib\/postgresql\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n      start_period: 10s\n\n  cache:\n    image: redis:8-alpine\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 3s\n      retries: 10\n\nvolumes:\n  db-data:<\/code><\/pre>\n\n<h3 id=\"compose-explained\">What each part means<\/h3>\n\n<div class=\"dd-table-wrap\">\n<table>\n<thead><tr><th>Key<\/th><th>What it does<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>services<\/code><\/td><td>The list of containers. Each name (<code>api<\/code>, <code>db<\/code>, <code>cache<\/code>) also becomes a <b>hostname<\/b> on the Compose network.<\/td><\/tr>\n<tr><td><code>build: .\/api<\/code><\/td><td>Build the image from the Dockerfile in the <code>api<\/code> folder.<\/td><\/tr>\n<tr><td><code>image<\/code><\/td><td>Use a ready-made image from Docker Hub instead of building one.<\/td><\/tr>\n<tr><td><code>ports: \"3000:3000\"<\/code><\/td><td><code>HOST:CONTAINER<\/code>. Opens container port 3000 on your computer&#8217;s port 3000. The database has no <code>ports<\/code>, so it stays private.<\/td><\/tr>\n<tr><td><code>environment<\/code><\/td><td>Environment variables passed into the container.<\/td><\/tr>\n<tr><td><code>depends_on<\/code> + <code>condition: service_healthy<\/code><\/td><td>Do not start <code>api<\/code> until <code>db<\/code> and <code>cache<\/code> pass their health checks.<\/td><\/tr>\n<tr><td><code>healthcheck<\/code><\/td><td>A command Docker runs again and again to decide if the service is &#8220;healthy&#8221;.<\/td><\/tr>\n<tr><td><code>volumes<\/code><\/td><td>A named volume (<code>db-data<\/code>) keeps the database files even when the container is removed.<\/td><\/tr>\n<tr><td><code>restart: unless-stopped<\/code><\/td><td>Restart the API automatically if it crashes, unless you stopped it yourself.<\/td><\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n\n<div class=\"dd-note\"><b>Why <code>$$<\/code> in the health check?<\/b> A single <code>${POSTGRES_USER}<\/code> is replaced by Compose when it reads the file. Writing <code>$${POSTGRES_USER}<\/code> tells Compose &#8220;leave this alone&#8221;, so the shell <em>inside<\/em> the container reads its own environment variable instead.<\/div>\n\n<div class=\"dd-warn\"><b>PostgreSQL 18+ volume path:<\/b> Starting with the PostgreSQL 18 Docker images, the data folder moved to a version-specific path, and the image&#8217;s volume is now <code>\/var\/lib\/postgresql<\/code>. Mount your volume there (as above), not at the old <code>\/var\/lib\/postgresql\/data<\/code>. See the <a href=\"https:\/\/hub.docker.com\/_\/postgres\" target=\"_blank\" rel=\"noopener\">official postgres image page<\/a> for details. Curious about what is coming next in Postgres? Read our post on <a href=\"https:\/\/devdojo.co.in\/postgresql-19-new-features\/\">PostgreSQL 19 new features<\/a>.<\/div>\n\n<figure>\n<svg viewBox=\"0 0 900 260\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-labelledby=\"ddF2t ddF2d\">\n<title id=\"ddF2t\">Startup order with health checks<\/title>\n<desc id=\"ddF2d\">Timeline: db and cache start first and run their health checks. Once both report healthy, Compose starts the api container, which connects successfully.<\/desc>\n<rect width=\"900\" height=\"260\" rx=\"14\" fill=\"#0f1430\"\/>\n<g font-family=\"Segoe UI,Arial,sans-serif\" font-size=\"15\">\n<line x1=\"140\" y1=\"215\" x2=\"860\" y2=\"215\" stroke=\"#3a4685\" stroke-width=\"2\"\/>\n<text x=\"140\" y=\"240\" fill=\"#9aa5cf\" font-size=\"13\">0s<\/text><text x=\"430\" y=\"240\" fill=\"#9aa5cf\" font-size=\"13\">~5\u201310s<\/text><text x=\"800\" y=\"240\" fill=\"#9aa5cf\" font-size=\"13\">ready<\/text>\n<text x=\"30\" y=\"62\" fill=\"#fff\" font-weight=\"700\">db<\/text>\n<rect x=\"140\" y=\"45\" width=\"300\" height=\"26\" rx=\"6\" fill=\"#3a2f7a\"\/><text x=\"152\" y=\"63\" fill=\"#e6e9f5\" font-size=\"13\">starting\u2026 pg_isready \u2717 \u2717 \u2717<\/text>\n<rect x=\"440\" y=\"45\" width=\"420\" height=\"26\" rx=\"6\" fill=\"#1f6f3f\"\/><text x=\"452\" y=\"63\" fill=\"#e6ffe9\" font-size=\"13\">healthy \u2714<\/text>\n<text x=\"30\" y=\"112\" fill=\"#fff\" font-weight=\"700\">cache<\/text>\n<rect x=\"140\" y=\"95\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#5a2340\"\/><text x=\"150\" y=\"113\" fill=\"#ffe6ef\" font-size=\"13\">starting\u2026<\/text>\n<rect x=\"260\" y=\"95\" width=\"600\" height=\"26\" rx=\"6\" fill=\"#1f6f3f\"\/><text x=\"272\" y=\"113\" fill=\"#e6ffe9\" font-size=\"13\">healthy \u2714 (PONG)<\/text>\n<text x=\"30\" y=\"162\" fill=\"#fff\" font-weight=\"700\">api<\/text>\n<rect x=\"140\" y=\"145\" width=\"300\" height=\"26\" rx=\"6\" fill=\"#262c4d\" stroke=\"#3a4685\" stroke-dasharray=\"5 4\"\/><text x=\"152\" y=\"163\" fill=\"#9aa5cf\" font-size=\"13\">waiting for db + cache\u2026<\/text>\n<rect x=\"440\" y=\"145\" width=\"420\" height=\"26\" rx=\"6\" fill=\"#1b4f8f\"\/><text x=\"452\" y=\"163\" fill=\"#e6f0ff\" font-size=\"13\">started \u2192 connects on first try \u2714<\/text>\n<line x1=\"440\" y1=\"35\" x2=\"440\" y2=\"215\" stroke=\"#7c5cff\" stroke-dasharray=\"4 4\"\/>\n<\/g>\n<\/svg>\n<figcaption>Without health checks, <code>depends_on<\/code> only waits for the container to <em>start<\/em>, not for PostgreSQL to accept connections.<\/figcaption>\n<\/figure>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"run-it\">Step 4: Run and test it<\/h2>\n\n<p>From the <code>my-app<\/code> folder, build and start everything in the background:<\/p>\n\n<pre><code>docker compose up -d --build<\/code><\/pre>\n\n<p>Check that all three services are running and healthy:<\/p>\n\n<pre><code>docker compose ps<\/code><\/pre>\n\n<p>You should see <code>db<\/code> and <code>cache<\/code> marked <b>healthy<\/b> and <code>api<\/code> marked <b>Up<\/b>. Now call the API (or open it in your browser):<\/p>\n\n<pre><code>curl http:\/\/localhost:3000\n# {\"message\":\"Hello from Docker Compose!\",\"totalVisits\":1,\"cacheHits\":1}\n\ncurl http:\/\/localhost:3000\n# {\"message\":\"Hello from Docker Compose!\",\"totalVisits\":2,\"cacheHits\":2}<\/code><\/pre>\n\n<p>Let&#8217;s peek inside the database and the cache directly, using <code>docker compose exec<\/code> to run commands inside running containers:<\/p>\n\n<pre><code># Count rows in PostgreSQL\ndocker compose exec db psql -U devdojo -d appdb -c \"SELECT COUNT(*) FROM visits;\"\n\n# Read the counter from Redis\ndocker compose exec cache redis-cli GET hits<\/code><\/pre>\n\n<p>Now stop everything and start it again:<\/p>\n\n<pre><code>docker compose down\ndocker compose up -d\ncurl http:\/\/localhost:3000<\/code><\/pre>\n\n<p>The <code>totalVisits<\/code> number keeps growing, because PostgreSQL data lives in the <code>db-data<\/code> volume. The Redis counter, however, starts over \u2014 we did not give Redis a volume. That is fine for a cache, and it is a good way to see the difference volumes make.<\/p>\n\n<h2 id=\"live-reload\">Step 5: Live reload with Compose Watch<\/h2>\n\n<p>Right now, if you edit <code>index.js<\/code> you must run <code>docker compose up -d --build<\/code> again. <b>Compose Watch<\/b> automates this: it watches files on your computer and updates the container for you. You add a <code>develop.watch<\/code> section to the service and pick an <b>action<\/b>. Click the tabs below to compare the three main actions:<\/p>\n\n<div class=\"dd-tabs\">\n<input type=\"radio\" name=\"dd-watch\" id=\"dd-t1\" checked>\n<input type=\"radio\" name=\"dd-watch\" id=\"dd-t2\">\n<input type=\"radio\" name=\"dd-watch\" id=\"dd-t3\">\n<div class=\"dd-tab-labels\">\n<label for=\"dd-t1\">sync<\/label>\n<label for=\"dd-t2\">sync+restart<\/label>\n<label for=\"dd-t3\">rebuild<\/label>\n<\/div>\n<div class=\"dd-tab-panel dd-p1\">\n<p><b>Copies changed files into the running container.<\/b> Best when the app already reloads itself (Node&#8217;s <code>--watch<\/code>, Vite, Next.js dev server, Nodemon). Fastest option.<\/p>\n<pre><code>  api:\n    build: .\/api\n    command: npm run dev   # runs \"node --watch\"\n    develop:\n      watch:\n        - action: sync\n          path: .\/api\/src\n          target: \/app\/src<\/code><\/pre>\n<\/div>\n<div class=\"dd-tab-panel dd-p2\">\n<p><b>Copies changed files, then restarts the container.<\/b> Good when your app does not reload by itself, or for config files such as <code>nginx.conf<\/code>.<\/p>\n<pre><code>  api:\n    build: .\/api\n    develop:\n      watch:\n        - action: sync+restart\n          path: .\/api\/src\n          target: \/app\/src<\/code><\/pre>\n<\/div>\n<div class=\"dd-tab-panel dd-p3\">\n<p><b>Builds a fresh image and replaces the container.<\/b> Use it for changes that need a new image, like adding a package to <code>package.json<\/code>.<\/p>\n<pre><code>  api:\n    build: .\/api\n    develop:\n      watch:\n        - action: rebuild\n          path: .\/api\/package.json<\/code><\/pre>\n<\/div>\n<\/div>\n\n<p>In practice you combine them. Here is the <code>api<\/code> service with the recommended setup \u2014 sync source code, rebuild when dependencies change:<\/p>\n\n<span class=\"dd-file\">compose.yaml (api service)<\/span>\n<pre><code>  api:\n    build: .\/api\n    command: npm run dev\n    ports:\n      - \"3000:3000\"\n    environment:\n      DATABASE_URL: postgres:\/\/${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432\/${POSTGRES_DB}\n      REDIS_URL: redis:\/\/cache:6379\n    depends_on:\n      db:\n        condition: service_healthy\n      cache:\n        condition: service_healthy\n    develop:\n      watch:\n        - action: sync\n          path: .\/api\/src\n          target: \/app\/src\n        - action: rebuild\n          path: .\/api\/package.json<\/code><\/pre>\n\n<p>Start it with watch mode turned on:<\/p>\n\n<pre><code>docker compose up --watch\n# or, to only see watch events:\ndocker compose watch<\/code><\/pre>\n\n<p>Change the <code>message<\/code> text in <code>index.js<\/code>, save, and refresh <code>localhost:3000<\/code>. You will see the new message within a second or two \u2014 no rebuild needed. Read more in the <a href=\"https:\/\/docs.docker.com\/compose\/how-tos\/file-watch\/\" target=\"_blank\" rel=\"noopener\">official Compose Watch docs<\/a>.<\/p>\n\n<div class=\"dd-tip\"><b>Tip:<\/b> Watch mode is for development only. In production you build a fresh image once and run it \u2014 ideally in a CI pipeline. Our guide to <a href=\"https:\/\/devdojo.co.in\/github-actions-ci-cd-nodejs\/\">GitHub Actions CI\/CD for Node.js<\/a> shows how to do that.<\/div>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"everyday-commands\">Everyday Docker Compose commands<\/h2>\n\n<p>You will use these commands almost every day. Bookmark this table:<\/p>\n\n<div class=\"dd-table-wrap\">\n<table>\n<thead><tr><th>Command<\/th><th>What it does<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>docker compose up -d<\/code><\/td><td>Start all services in the background (&#8220;detached&#8221;).<\/td><\/tr>\n<tr><td><code>docker compose up -d --build<\/code><\/td><td>Rebuild images first, then start. Use after changing a Dockerfile.<\/td><\/tr>\n<tr><td><code>docker compose ps<\/code><\/td><td>List services with their status and health.<\/td><\/tr>\n<tr><td><code>docker compose logs -f api<\/code><\/td><td>Follow live logs of one service. Leave out <code>api<\/code> to see all.<\/td><\/tr>\n<tr><td><code>docker compose exec db sh<\/code><\/td><td>Open a shell inside a running container.<\/td><\/tr>\n<tr><td><code>docker compose restart api<\/code><\/td><td>Restart just one service.<\/td><\/tr>\n<tr><td><code>docker compose stop<\/code><\/td><td>Stop containers but keep them (fast to start again).<\/td><\/tr>\n<tr><td><code>docker compose down<\/code><\/td><td>Stop and remove containers and the network. <b>Volumes are kept.<\/b><\/td><\/tr>\n<tr><td><code>docker compose down -v<\/code><\/td><td>Same, but also <b>deletes volumes<\/b> \u2014 your database data is gone.<\/td><\/tr>\n<tr><td><code>docker compose config<\/code><\/td><td>Print the final file with all <code>${VARIABLES}<\/code> filled in. Great for debugging.<\/td><\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n\n<h2 id=\"common-errors\">Common errors and fixes<\/h2>\n\n<p>These are the problems beginners (and plenty of seniors) hit most often:<\/p>\n\n<div class=\"dd-table-wrap\">\n<table>\n<thead><tr><th>Error \/ symptom<\/th><th>Why it happens<\/th><th>Fix<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>connect ECONNREFUSED 127.0.0.1:5432<\/code><\/td><td>Your app uses <code>localhost<\/code>. Inside a container, <code>localhost<\/code> means <em>that same container<\/em>, not the database.<\/td><td>Use the service name as host: <code>db:5432<\/code>, <code>cache:6379<\/code>.<\/td><\/tr>\n<tr><td>API crashes on first start, works after restart<\/td><td>The API started before PostgreSQL was ready to accept connections.<\/td><td>Add a <code>healthcheck<\/code> to <code>db<\/code> and use <code>depends_on<\/code> with <code>condition: service_healthy<\/code>.<\/td><\/tr>\n<tr><td><code>Bind for 0.0.0.0:5432 failed: port is already allocated<\/code><\/td><td>Another program (often a locally installed PostgreSQL) already uses that port on your computer.<\/td><td>Stop the other program, or change only the host side: <code>\"5433:5432\"<\/code>.<\/td><\/tr>\n<tr><td>PostgreSQL 18 container exits with an error about the data directory or <code>\/var\/lib\/postgresql\/data<\/code><\/td><td>The volume is mounted at the old path used by images before version 18.<\/td><td>Mount at <code>\/var\/lib\/postgresql<\/code>. To upgrade existing data from 17, use <code>pg_dump<\/code>\/restore or <code>pg_upgrade<\/code>.<\/td><\/tr>\n<tr><td><code>the attribute `version` is obsolete<\/code> warning<\/td><td>Old tutorials start files with <code>version: \"3.8\"<\/code>. Modern Compose ignores it.<\/td><td>Delete the <code>version:<\/code> line.<\/td><\/tr>\n<tr><td><code>The \"POSTGRES_USER\" variable is not set. Defaulting to a blank string.<\/code><\/td><td>Compose cannot find your <code>.env<\/code> file.<\/td><td>Put <code>.env<\/code> in the same folder as <code>compose.yaml<\/code>, or pass <code>--env-file path\/to\/.env<\/code>.<\/td><\/tr>\n<tr><td>Changed <code>POSTGRES_PASSWORD<\/code> but login still fails<\/td><td>The <code>POSTGRES_*<\/code> variables are only used the first time, when the volume is empty.<\/td><td>For local dev, reset with <code>docker compose down -v<\/code> (deletes data), or change the password with SQL.<\/td><\/tr>\n<tr><td>Code changes do not show up<\/td><td>The image still contains the old code.<\/td><td>Run <code>docker compose up -d --build<\/code>, or use Compose Watch.<\/td><\/tr>\n<tr><td><code>docker-compose: command not found<\/code><\/td><td>You are using the retired v1 command name.<\/td><td>Use <code>docker compose<\/code> (with a space).<\/td><\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n\n<h2 id=\"best-practices\">Best practices<\/h2>\n\n<ol>\n<li><b>Name the file <code>compose.yaml<\/code>.<\/b> It is the preferred name today. <code>docker-compose.yml<\/code> still works.<\/li>\n<li><b>Pin major versions of images<\/b> (<code>postgres:18-alpine<\/code>, <code>redis:8-alpine<\/code>) instead of <code>latest<\/code>, so a surprise upgrade never breaks your setup.<\/li>\n<li><b>Expose only what you need.<\/b> Leave out <code>ports<\/code> for databases unless you want to connect a GUI tool from your computer.<\/li>\n<li><b>Keep secrets out of Git.<\/b> Use <code>.env<\/code> locally and a real secret manager in production.<\/li>\n<li><b>Always add health checks<\/b> for databases, caches and message queues.<\/li>\n<li><b>Use named volumes<\/b> for data you care about, and remember that <code>down -v<\/code> deletes them.<\/li>\n<li><b>Use a <code>.dockerignore<\/code><\/b> to keep images small and builds fast.<\/li>\n<li><b>Use profiles for optional tools.<\/b> Add <code>profiles: [\"tools\"]<\/code> to something like pgAdmin, then start it only when needed with <code>docker compose --profile tools up<\/code>.<\/li>\n<li><b>Run <code>docker compose config<\/code><\/b> when something looks wrong \u2014 it shows exactly what Compose sees.<\/li>\n<\/ol>\n\n<p>Here is a quick example of an optional database admin tool using a profile:<\/p>\n\n<pre><code>  pgadmin:\n    image: dpage\/pgadmin4\n    profiles: [\"tools\"]\n    ports:\n      - \"5050:80\"\n    environment:\n      PGADMIN_DEFAULT_EMAIL: admin@example.com\n      PGADMIN_DEFAULT_PASSWORD: admin\n    depends_on:\n      - db<\/code><\/pre>\n\n<p>A normal <code>docker compose up<\/code> skips it. <code>docker compose --profile tools up -d<\/code> includes it, and you can open <code>http:\/\/localhost:5050<\/code> and connect to host <code>db<\/code>.<\/p>\n<\/div>\n\n\n\n<div class=\"dd-post\">\n<h2 id=\"faq\">FAQ<\/h2>\n\n<details>\n<summary>What is the difference between Docker and Docker Compose?<\/summary>\n<p>Docker builds and runs single containers. Docker Compose is a tool on top of Docker that runs <b>many<\/b> containers together from one YAML file, with a shared network, volumes and start-up order.<\/p>\n<\/details>\n\n<details>\n<summary>Should I name my file compose.yaml or docker-compose.yml?<\/summary>\n<p>Both work. <code>compose.yaml<\/code> is the preferred name in the official Compose specification, so use it for new projects.<\/p>\n<\/details>\n\n<details>\n<summary>Does <code>docker compose down<\/code> delete my database?<\/summary>\n<p>No. <code>down<\/code> removes containers and the network but keeps named volumes. Only <code>docker compose down -v<\/code> deletes volumes (and your data).<\/p>\n<\/details>\n\n<details>\n<summary>Can I use Docker Compose in production?<\/summary>\n<p>Yes, for small apps on a single server it works well. For many servers, auto-scaling and zero-downtime deploys, teams usually move to Kubernetes or a managed container service. Learning Compose first makes those much easier to understand.<\/p>\n<\/details>\n\n<details>\n<summary>Why can&#8217;t my container connect to <code>localhost<\/code>?<\/summary>\n<p>Each container has its own network &#8220;home&#8221;. Inside the <code>api<\/code> container, <code>localhost<\/code> is the API itself. Use the other service&#8217;s name (like <code>db<\/code>) as the hostname.<\/p>\n<\/details>\n\n<details>\n<summary>Does Docker Compose work on Windows?<\/summary>\n<p>Yes. Install Docker Desktop (it uses WSL 2 on Windows). For the best file-watching speed, keep your project inside the WSL file system rather than on the Windows <code>C:<\/code> drive.<\/p>\n<\/details>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>You now have a real multi-container setup: a Node.js API, a PostgreSQL database with saved data, and a Redis cache \u2014 all started with <code>docker compose up<\/code>. More importantly, you understand the ideas behind it: services talk by <b>name<\/b>, <b>health checks<\/b> control start-up order, <b>volumes<\/b> keep data safe, and <b>Compose Watch<\/b> gives you fast feedback while coding.<\/p>\n\n<p>Try extending this project next: add a <code>worker<\/code> service that reads jobs from Redis, or a frontend service with Vite. The pattern stays exactly the same \u2014 one more block in <code>compose.yaml<\/code>.<\/p>\n\n<div class=\"dd-cta\">\n<h3>New posts every day on DevDojo<\/h3>\n<p>Practical, beginner-friendly guides on AI, frontend, backend and DevOps. Bookmark <a href=\"https:\/\/devdojo.co.in\/\">devdojo.co.in<\/a> and leave a comment below if you got stuck anywhere in this tutorial \u2014 we read every one.<\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.<\/p>\n","protected":false},"author":1,"featured_media":187,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7,57,8],"tags":[61,60,74,43,70,75],"class_list":["post-188","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-backend","category-devops","category-nodejs","tag-devops","tag-docker","tag-docker-compose","tag-node-js","tag-postgresql","tag-redis"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.2.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Lokendra\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.2.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"DevDojo - Empowering Developers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Docker Compose Tutorial: Node.js, PostgreSQL &amp; Redis\" \/>\n\t\t<meta property=\"og:description\" content=\"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-10-10T16:00:50+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-10-10T16:00:50+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Docker Compose Tutorial: Node.js, PostgreSQL &amp; Redis\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BlogPosting\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#blogposting\",\"name\":\"Docker Compose Tutorial: Node.js, PostgreSQL & Redis\",\"headline\":\"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together\",\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/docker-compose-tutorial.jpg\",\"width\":1200,\"height\":630,\"caption\":\"Docker Compose tutorial banner showing Node.js API, PostgreSQL and Redis containers in one Compose network\"},\"datePublished\":\"2026-10-10T16:00:50+00:00\",\"dateModified\":\"2026-10-10T16:00:50+00:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#webpage\"},\"articleSection\":\"Backend, DevOps, NodeJS, DevOps, Docker, Docker Compose, Node.js, PostgreSQL, Redis\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"position\":2,\"name\":\"Backend\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"name\":\"NodeJS\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"position\":3,\"name\":\"NodeJS\",\"item\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#listItem\",\"name\":\"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/#listItem\",\"name\":\"Backend\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#listItem\",\"position\":4,\"name\":\"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/category\\\/backend\\\/nodejs\\\/#listItem\",\"name\":\"NodeJS\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/\",\"name\":\"Lokendra\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Lokendra\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#webpage\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/\",\"name\":\"Docker Compose Tutorial: Node.js, PostgreSQL & Redis\",\"description\":\"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/author\\\/lokendra\\\/#author\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/docker-compose-tutorial.jpg\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#mainImage\",\"width\":1200,\"height\":630,\"caption\":\"Docker Compose tutorial banner showing Node.js API, PostgreSQL and Redis containers in one Compose network\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/index.php\\\/2026\\\/10\\\/10\\\/docker-compose-tutorial\\\/#mainImage\"},\"datePublished\":\"2026-10-10T16:00:50+00:00\",\"dateModified\":\"2026-10-10T16:00:50+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#website\",\"url\":\"https:\\\/\\\/devdojo.co.in\\\/\",\"name\":\"DevDojo\",\"description\":\"Empowering Developers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/devdojo.co.in\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Docker Compose Tutorial: Node.js, PostgreSQL & Redis","description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.","canonical_url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BlogPosting","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#blogposting","name":"Docker Compose Tutorial: Node.js, PostgreSQL & Redis","headline":"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together","author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/docker-compose-tutorial.jpg","width":1200,"height":630,"caption":"Docker Compose tutorial banner showing Node.js API, PostgreSQL and Redis containers in one Compose network"},"datePublished":"2026-10-10T16:00:50+00:00","dateModified":"2026-10-10T16:00:50+00:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#webpage"},"isPartOf":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#webpage"},"articleSection":"Backend, DevOps, NodeJS, DevOps, Docker, Docker Compose, Node.js, PostgreSQL, Redis"},{"@type":"BreadcrumbList","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","position":1,"name":"Home","item":"https:\/\/devdojo.co.in\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","position":2,"name":"Backend","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","name":"NodeJS"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","position":3,"name":"NodeJS","item":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/","nextItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#listItem","name":"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together"},"previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/#listItem","name":"Backend"}},{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#listItem","position":4,"name":"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together","previousItem":{"@type":"ListItem","@id":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/#listItem","name":"NodeJS"}}]},{"@type":"Organization","@id":"https:\/\/devdojo.co.in\/#organization","name":"DevDojo","description":"Empowering Developers","url":"https:\/\/devdojo.co.in\/"},{"@type":"Person","@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author","url":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/","name":"Lokendra","image":{"@type":"ImageObject","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/99f102f0cc9080237ce54b91304fd1b3b6fa29925cbaeba93cd796872e58b45c?s=96&d=mm&r=g","width":96,"height":96,"caption":"Lokendra"}},{"@type":"WebPage","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#webpage","url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/","name":"Docker Compose Tutorial: Node.js, PostgreSQL & Redis","description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/devdojo.co.in\/#website"},"breadcrumb":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#breadcrumblist"},"author":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"creator":{"@id":"https:\/\/devdojo.co.in\/index.php\/author\/lokendra\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/devdojo.co.in\/wp-content\/uploads\/2026\/10\/docker-compose-tutorial.jpg","@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#mainImage","width":1200,"height":630,"caption":"Docker Compose tutorial banner showing Node.js API, PostgreSQL and Redis containers in one Compose network"},"primaryImageOfPage":{"@id":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/#mainImage"},"datePublished":"2026-10-10T16:00:50+00:00","dateModified":"2026-10-10T16:00:50+00:00"},{"@type":"WebSite","@id":"https:\/\/devdojo.co.in\/#website","url":"https:\/\/devdojo.co.in\/","name":"DevDojo","description":"Empowering Developers","inLanguage":"en-US","publisher":{"@id":"https:\/\/devdojo.co.in\/#organization"}}]},"og:locale":"en_US","og:site_name":"DevDojo - Empowering Developers","og:type":"article","og:title":"Docker Compose Tutorial: Node.js, PostgreSQL &amp; Redis","og:description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.","og:url":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/","article:published_time":"2026-10-10T16:00:50+00:00","article:modified_time":"2026-10-10T16:00:50+00:00","twitter:card":"summary_large_image","twitter:title":"Docker Compose Tutorial: Node.js, PostgreSQL &amp; Redis","twitter:description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload."},"aioseo_meta_data":{"post_id":"188","title":"Docker Compose Tutorial: Node.js, PostgreSQL & Redis","description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.","keywords":null,"keyphrases":{"focus":{"keyphrase":"Docker Compose tutorial","score":75},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":"Docker Compose Tutorial: Node.js, PostgreSQL & Redis","og_description":"Docker Compose tutorial for beginners: run a Node.js API, PostgreSQL and Redis together with one compose.yaml file, health checks, volumes and live reload.","og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"BlogPosting","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":0,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-10-10 13:28:00","updated":"2026-10-10 16:19:37","focus_keyword":"Docker Compose tutorial","additional_keywords":null,"truseo_locale":null,"seo_analyzer_scan_date":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/\" title=\"Backend\">Backend<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/\" title=\"NodeJS\">NodeJS<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">\u00bb<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tDocker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/devdojo.co.in\/"},{"label":"Backend","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/"},{"label":"NodeJS","link":"https:\/\/devdojo.co.in\/index.php\/category\/backend\/nodejs\/"},{"label":"Docker Compose Tutorial: Run Node.js, PostgreSQL and Redis Together","link":"https:\/\/devdojo.co.in\/index.php\/2026\/10\/10\/docker-compose-tutorial\/"}],"_links":{"self":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/188","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/comments?post=188"}],"version-history":[{"count":1,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/188\/revisions"}],"predecessor-version":[{"id":189,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/posts\/188\/revisions\/189"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media\/187"}],"wp:attachment":[{"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/media?parent=188"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/categories?post=188"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devdojo.co.in\/index.php\/wp-json\/wp\/v2\/tags?post=188"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}