<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Docker compose - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Docker compose - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 18 Jul 2026 16:27:21 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/docker-compose/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Containerize a Node.js Application with Docker and Deploy with GitHub Actions ]]>
                </title>
                <description>
                    <![CDATA[ If you've been building Node.js projects, you've probably had an experience like this. The project runs fine on your machine, but when you push it to a server, something breaks. Maybe it's a different ]]>
                </description>
                <link>https://www.freecodecamp.org/news/containerize-a-node-js-app-with-docker-and-deploy-with-github-actions/</link>
                <guid isPermaLink="false">6a569b9cbd138d774dee2042</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub Actions ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ci-cd ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker-compose.yml ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containerization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Backend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Tue, 14 Jul 2026 20:27:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/343864e6-5319-4378-a2b1-4955e38ad6d8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've been building <a href="https://www.freecodecamp.org/news/role-based-access-control-nodejs-rest-api-jwt/">Node.js projects</a>, you've probably had an experience like this. The project runs fine on your machine, but when you push it to a server, something breaks.</p>
<p>Maybe it's a different Node version, maybe an environment variable is missing, or maybe a system dependency doesn't match. You spend an hour debugging something that was never actually a code problem.</p>
<p>Docker fixes this at the root. With Docker, you stop shipping just code. The Node version, dependencies, and config all travel inside the container. Your laptop, a CI server, a production VM — it behaves the same on all of them. No more environment surprises.</p>
<p>In this tutorial, we'll go through all this step by step: a multi-stage Dockerfile, using Docker Compose with PostgreSQL for local development, and a GitHub Actions workflow that pushes a fresh image to Docker Hub on every merge to <code>main</code>.</p>
<p>The complete code for this tutorial is available on <a href="https://github.com/ziaongit/nodejs-docker-cicd">GitHub</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-sample-application">The Sample Application</a></p>
</li>
<li><p><a href="#heading-writing-the-dockerfile">Writing the Dockerfile</a></p>
</li>
<li><p><a href="#heading-the-dockerignore-file">The .dockerignore File</a></p>
</li>
<li><p><a href="#heading-the-gitignore-file">The .gitignore File</a></p>
</li>
<li><p><a href="#heading-build-and-test-the-image-locally">Build and Test the Image Locally</a></p>
</li>
<li><p><a href="#heading-docker-compose-for-local-development">Docker Compose for Local Development</a></p>
</li>
<li><p><a href="#heading-automate-the-build-with-github-actions">Automate the Build with GitHub Actions</a></p>
</li>
<li><p><a href="#heading-deploying-the-image">Deploying the Image</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js 18+</p>
</li>
<li><p>Docker Desktop, which you can download at <a href="https://docs.docker.com/get-docker/">docs.docker.com/get-docker</a>. Windows users need WSL 2 before Docker starts. Open PowerShell as Administrator and run <code>wsl --install</code>. After the restart, Docker Desktop will install without issues.</p>
</li>
<li><p>A GitHub account</p>
</li>
<li><p>A Docker Hub account (free at <a href="https://hub.docker.com">hub.docker.com</a>)</p>
</li>
<li><p>Some Express.js experience helps, but isn't required</p>
</li>
</ul>
<h2 id="heading-the-sample-application">The Sample Application</h2>
<p>We're building a task management API with Express and PostgreSQL. Keep in mind the app is just a vehicle to teach you how this works. The Dockerfile and pipeline we set up here work the same way for any Node.js project.</p>
<p>Create the project:</p>
<pre><code class="language-bash">mkdir nodejs-docker-cicd &amp;&amp; cd nodejs-docker-cicd
npm init -y
npm install express pg dotenv
npm install --save-dev nodemon
</code></pre>
<p>Create <code>src/index.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { Pool } = require('pg');
require('dotenv').config();

const app = express();
app.use(express.json());

const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});

// Create table on startup
pool.query(`
  CREATE TABLE IF NOT EXISTS tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW()
  )
`).catch(console.error);

// Health check — required for Docker HEALTHCHECK and load balancers
app.get('/health', (req, res) =&gt; {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.get('/tasks', async (req, res) =&gt; {
  try {
    const result = await pool.query('SELECT * FROM tasks ORDER BY created_at DESC');
    res.json(result.rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post('/tasks', async (req, res) =&gt; {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });
  try {
    const result = await pool.query(
      'INSERT INTO tasks (title) VALUES ($1) RETURNING *',
      [title]
    );
    res.status(201).json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.patch('/tasks/:id', async (req, res) =&gt; {
  const { id } = req.params;
  const { completed } = req.body;
  try {
    const result = await pool.query(
      'UPDATE tasks SET completed = $1 WHERE id = $2 RETURNING *',
      [completed, id]
    );
    if (result.rows.length === 0) return res.status(404).json({ error: 'Task not found' });
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; console.log(`Server running on port ${PORT}`));
</code></pre>
<p>Open <code>package.json</code> and update the <code>"scripts"</code> section:</p>
<pre><code class="language-json">"scripts": {
  "start": "node src/index.js",
  "dev": "nodemon src/index.js"
}
</code></pre>
<p><code>npm start</code> runs the app directly with Node. <code>npm run dev</code> uses nodemon so the server restarts automatically when you edit a file.</p>
<p>For running without Docker, create a <code>.env</code> file:</p>
<pre><code class="language-plaintext">DB_HOST=localhost
DB_PORT=5432
DB_NAME=tasksdb
DB_USER=postgres
DB_PASSWORD=yourpassword
PORT=3000
</code></pre>
<p>Notice that all database credentials come from environment variables rather than being hardcoded. Swap the variables, and the same image runs against your local database or a production one — no code changes needed. The <code>/health</code> endpoint is what Docker pings to know the app is actually handling requests.</p>
<h2 id="heading-writing-the-dockerfile">Writing the Dockerfile</h2>
<p>Before touching the Dockerfile, there are two terms you'll keep seeing. An <strong>image</strong> is a packaged, immutable version of your app — Node runtime, code, dependencies, everything together in one artifact. A <strong>container</strong> is a running instance of that image. One image, many containers, any machine.</p>
<p>Here's the Dockerfile we'll use:</p>
<pre><code class="language-dockerfile"># ── Stage 1: Install dependencies ──────────────────────────────────────────
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files first — Docker caches this layer separately.
# If you only change src code (not package.json), Docker skips npm ci on rebuild.
COPY package*.json ./
RUN npm ci

COPY . .


# ── Stage 2: Production image ───────────────────────────────────────────────
FROM node:18-alpine AS production

# Create a non-root user — running as root inside a container is a security risk
RUN addgroup -g 1001 -S nodejs &amp;&amp; \
    adduser -S nodeuser -u 1001

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

# Copy only the source code from the builder stage (not node_modules or dev files)
COPY --from=builder /app/src ./src

RUN chown -R nodeuser:nodejs /app
USER nodeuser

EXPOSE 3000

# Docker will ping /health every 30s. If it fails 3 times, the container is marked unhealthy.
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "src/index.js"]
</code></pre>
<p>This is a multi-stage build. The first stage (<code>builder</code>) installs everything, including dev dependencies. The second stage (<code>production</code>) starts fresh and only copies what the app needs to run. Nodemon, test frameworks, and anything else dev-only never make it into the final image.</p>
<p>The size difference is real. A <code>node:18</code> Debian image is over 950MB. Switch to <code>node:18-alpine</code> and cut out the dev dependencies, and the final image lands around 150–200MB instead. A smaller image means faster pushes and faster deploys.</p>
<p><code>npm ci</code> instead of <code>npm install</code> is a deliberate choice for CI/CD. It reads exact versions from <code>package-lock.json</code> and fails hard if the lockfile doesn't match <code>package.json</code>. Every build on every machine installs the exact same versions — no surprises from a dependency that quietly updated overnight.</p>
<p>The <code>nodeuser</code> account exists because containers run as root by default. That's fine until something goes wrong. A non-root user means that an attacker who gets inside the container can't just do whatever they want.</p>
<h2 id="heading-the-dockerignore-file">The <code>.dockerignore</code> File</h2>
<p>Create <code>.dockerignore</code> before building:</p>
<pre><code class="language-plaintext">node_modules
npm-debug.log
.env
.git
.gitignore
README.md
Dockerfile
.dockerignore
</code></pre>
<p>The <code>node_modules</code> exclusion is the critical one. Your local modules were compiled for your operating system — macOS or Windows binaries won't work inside a Linux container. Excluding them means Docker installs fresh modules during the build, compiled for the correct platform. Without this exclusion, you'd either copy broken binaries into the image or waste time uploading hundreds of megabytes to the build context.</p>
<p>Never put <code>.env</code> in an image. Passwords, API keys, anything sensitive — those go in at runtime as environment variables, never inside the image itself.</p>
<h2 id="heading-the-gitignore-file">The <code>.gitignore</code> File</h2>
<p>One more thing before the first commit: a <code>.gitignore</code>. You don't want <code>node_modules</code> or <code>.env</code> tracked:</p>
<pre><code class="language-plaintext">node_modules/
.env
.env.local
npm-debug.log*
logs/
.DS_Store
Thumbs.db
.vscode/
.idea/
dist/
build/
</code></pre>
<h2 id="heading-build-and-test-the-image-locally">Build and Test the Image Locally</h2>
<p>Open Docker Desktop first and give it a moment. On Windows, you'll see a whale icon in the taskbar that animates while the engine is starting up. Once it goes still, you're good to run Docker commands. If you try to run Docker before the engine is up, you'll hit this:</p>
<pre><code class="language-plaintext">ERROR: Error response from daemon: Docker Desktop is unable to start
</code></pre>
<p>If that happens, quit Docker Desktop. Open PowerShell as Administrator, run <code>wsl --update</code>, and restart. Then go to Control Panel → Programs → Turn Windows features on or off. Both Hyper-V and Virtual Machine Platform need to be checked. After the restart, Docker Desktop should come up fine.</p>
<p>It's worth knowing about this error too:</p>
<pre><code class="language-plaintext">docker : The term 'docker' is not recognized as the name of a cmdlet, function,
script file, or operable program.
</code></pre>
<p>This means that Docker Desktop isn't running or isn't installed. Open it from the Start menu and wait.</p>
<p>Run the build:</p>
<pre><code class="language-bash">docker build -t nodejs-docker-cicd:latest .
</code></pre>
<p>The first time takes roughly 30 seconds since Docker has to pull <code>node:18-alpine</code> from the internet. Once that's cached, subsequent builds are much quicker. Both stages will scroll by:</p>
<pre><code class="language-plaintext">[+] Building 33.1s (17/17) FINISHED
 =&gt; [builder 1/5] FROM docker.io/library/node:18-alpine       20.9s
 =&gt; [builder 4/5] RUN npm ci                                   3.5s
 =&gt; [production 5/7] RUN npm ci --only=production              3.2s
 =&gt; [production 7/7] RUN chown -R nodeuser:nodejs /app         3.2s
 =&gt; exporting to image                                         1.5s
 =&gt; =&gt; naming to docker.io/library/nodejs-docker-cicd:latest     0.0s
</code></pre>
<p>When you see <code>(17/17) FINISHED</code> the image is built. Check the size:</p>
<pre><code class="language-bash">docker images nodejs-docker-cicd
</code></pre>
<pre><code class="language-plaintext">IMAGE                     ID             DISK USAGE   CONTENT SIZE
nodejs-docker-cicd:latest   c9eed311d999        198MB         47.5MB
</code></pre>
<p><strong>CONTENT SIZE</strong> (47.5MB) is the compressed size that gets pushed to Docker Hub. <strong>DISK USAGE</strong> (198MB) is what it takes up on disk locally. Compare that to a <code>node:18</code> Debian image at 950MB+, and you can see why the Alpine base and multi-stage approach matter.</p>
<p>On subsequent builds, Docker reuses cached layers. Edit only your source files without touching <code>package.json</code> and the <code>npm ci</code> step gets skipped completely. That 33-second first build becomes 3 seconds.</p>
<h2 id="heading-docker-compose-for-local-development">Docker Compose for Local Development</h2>
<p>The app needs a database. Setting up PostgreSQL locally means every developer who clones the repo has to do it, too. Docker Compose handles this: one file defines both services, and one command starts them.</p>
<p>Create <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">services:
  app:
    build:
      context: .
      target: production
    ports:
      - '3000:3000'
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: tasksdb
      DB_USER: postgres
      DB_PASSWORD: postgres
      PORT: 3000
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: tasksdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
</code></pre>
<p>A few things worth pointing out. <code>DB_HOST</code> is set to <code>postgres</code>. That's the service name, not <code>localhost</code>. Containers on the same Docker network reach each other by service name. Put <code>localhost</code> there and the app tries to connect to itself.</p>
<p><code>depends_on</code> with <code>condition: service_healthy</code> holds the app back until Postgres actually passes its health check. Skip this and the app starts, tries to connect to a database that isn't ready yet, and crashes. The health check pings <code>pg_isready</code> every 5 seconds. Once it gets a green response, the app container starts.</p>
<p>The named volume <code>postgres_data</code> keeps your data alive between restarts. Run <code>docker compose down</code> and the data is still there next time. Add <code>--volumes</code> to wipe it clean.</p>
<p>Start both services:</p>
<pre><code class="language-bash">docker compose up --build
</code></pre>
<p>You'll see PostgreSQL initialize and then the app start. Once you see <code>Server running on port 3000</code> in the logs, the stack is up.</p>
<p>Open a second terminal to test — leave the compose logs running in the first one.</p>
<p><strong>Linux/macOS:</strong></p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn Docker"}'

curl http://localhost:3000/tasks

curl http://localhost:3000/health
</code></pre>
<p><strong>Windows PowerShell:</strong> Typing <code>curl</code> in PowerShell runs <code>Invoke-WebRequest</code>, not actual curl. Run <code>curl.exe</code> instead. For JSON bodies, write to a file first:</p>
<pre><code class="language-powershell">'{"title": "Learn Docker"}' | Set-Content body.json
curl.exe -X POST http://localhost:3000/tasks -H "Content-Type: application/json" --data `@body.json

curl.exe http://localhost:3000/tasks

curl.exe http://localhost:3000/health
</code></pre>
<p>The backtick before <code>@body.json</code> is necessary. PowerShell would otherwise try to interpret <code>@</code> as a splatting operator rather than passing it to curl as a filename prefix.</p>
<p>You should see responses like these:</p>
<pre><code class="language-json"># POST /tasks
{"id":1,"title":"Learn Docker","completed":false,"created_at":"2026-07-09T22:21:17.073Z"}

# GET /tasks
[{"id":1,"title":"Learn Docker","completed":false,"created_at":"2026-07-09T22:21:17.073Z"}]

# GET /health
{"status":"ok","timestamp":"2026-07-09T22:11:44.700Z"}
</code></pre>
<p>The task hit PostgreSQL in one container and came back through the app. <code>Ctrl+C</code> in the compose terminal stops both.</p>
<h2 id="heading-automate-the-build-with-github-actions">Automate the Build with GitHub Actions</h2>
<p>The image works locally, so it's time to stop doing this by hand.</p>
<h3 id="heading-step-1-create-a-docker-hub-access-token">Step 1: Create a Docker Hub Access Token</h3>
<p>Go to <a href="https://hub.docker.com">hub.docker.com</a> and then Account Settings → Security → New Access Token. Set permission to Read &amp; Write, as read-only breaks the push. The token appears once, so copy it before closing the page.</p>
<p><strong>Security warning:</strong> Don't paste this token into a chat, email, or commit. If you expose it by accident, delete it immediately, then make a new one.</p>
<h3 id="heading-step-2-add-secrets-to-your-github-repository">Step 2: Add Secrets to Your GitHub Repository</h3>
<p>Head to Settings → Secrets and variables → Actions in your repo and add:</p>
<ul>
<li><p><code>DOCKERHUB_USERNAME</code> — your Docker Hub username</p>
</li>
<li><p><code>DOCKERHUB_TOKEN</code> — paste the token here, nowhere else</p>
</li>
</ul>
<p>If you ran into <code>Error: Username and password required</code>, the secrets either aren't saved yet or the names are typed wrong. Both are case-sensitive.</p>
<p>A Node 20 deprecation warning in the logs is normal. It comes from the Docker actions internally, not your code.</p>
<h3 id="heading-step-3-create-the-workflow-file">Step 3: Create the Workflow File</h3>
<p>Create <code>.github/workflows/docker-publish.yml</code>:</p>
<pre><code class="language-yaml">name: Build and Push Docker Image

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/nodejs-docker-cicd

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          target: production
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
</code></pre>
<p>The login step has <code>if: github.event_name != 'pull_request'</code>. This skips authentication on pull requests. PRs from forks don't have access to your secrets, so trying to log in would just fail. The build still runs on PRs to validate your Dockerfile, but the image isn't pushed.</p>
<p>The metadata action generates two tags on every merge to <code>main</code>: <code>latest</code> and a short commit SHA like <code>sha-a1b2c3d</code>. The SHA tag is what makes rollbacks practical. If <code>latest</code> breaks in production, you can pull any previous <code>sha-</code> tag and you're back to a known-good state in seconds.</p>
<p>The <code>cache-from/cache-to: type=gha</code> lines store Docker's layer cache in GitHub Actions' built-in cache. The first run builds everything from scratch. After that, unchanged layers are pulled from cache rather than rebuilt. On a typical Node.js app this brings build time from 2–3 minutes down to under 30 seconds.</p>
<h3 id="heading-push-and-watch-it-run">Push and Watch it Run</h3>
<pre><code class="language-bash">git add .
git commit -m "Add Docker configuration and GitHub Actions workflow"
git push origin main
</code></pre>
<p>Go to your repo's <strong>Actions</strong> tab. You'll see the workflow running in real time. Each step turns green as it completes:</p>
<pre><code class="language-plaintext">✅ Checkout code
✅ Set up Docker Buildx
✅ Log in to Docker Hub
✅ Extract metadata
✅ Build and push
</code></pre>
<p>Green across the board means your image is live on Docker Hub — two tags, <code>latest</code> and a commit SHA like <code>sha-a1b2c3d</code>. Every push to <code>main</code> from here builds and ships automatically.</p>
<h2 id="heading-deploying-the-image">Deploying the Image</h2>
<p>With your image on Docker Hub, you can deploy it to any infrastructure:</p>
<p><strong>Any VPS or server:</strong></p>
<pre><code class="language-bash">docker pull yourusername/nodejs-docker-cicd:latest
docker run -d -p 3000:3000 \
  -e DB_HOST=your-db-host \
  -e DB_NAME=tasksdb \
  -e DB_USER=postgres \
  -e DB_PASSWORD=yourpassword \
  yourusername/nodejs-docker-cicd:latest
</code></pre>
<p><strong>Railway</strong> — Connect your Docker Hub image in the Railway dashboard and it deploys on the next push.</p>
<p><strong>Fly.io</strong> — Run <code>fly launch</code> pointing at your Dockerfile and Fly handles the rest.</p>
<p><strong>Render</strong> — Paste your Docker Hub image URL into the Render service settings.</p>
<p>Each push to <code>main</code> runs the workflow. New image goes to Docker Hub, platform picks it up — that's your deployment handled.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>What started as a local Node.js app now runs in a container. You get the same behavior on any machine, real PostgreSQL in development, and a pipeline that builds and ships to Docker Hub without you doing anything after the push.</p>
<p>The multi-stage build keeps the image lean — dev tools stay out, non-root user, health check baked in. Compose gets the full stack up with one command for anyone who clones the repo. The SHA tag on every GitHub Actions build means rolling back is just a matter of pulling an older tag.</p>
<p>These same patterns (multi-stage builds, Compose for local development, automated image publishing) are used across the industry for production Node.js deployments. Pick up these patterns once and they follow you to every project.</p>
<p>From here, you can extend the pipeline: drop a test step in before the build, or add multi-platform support if you're targeting ARM. Once Docker Compose starts feeling limiting in production, that's usually when Kubernetes enters the picture.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Dockerize a Go Application – Full Step-by-Step Walkthrough ]]>
                </title>
                <description>
                    <![CDATA[ Imagine that you want to share your source code with someone who doesn’t have Go installed on their computer. Unfortunately, this person won’t be able to run your application. Even if they do have Go  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-dockerize-a-go-application-full-step-by-step-walkthrough/</link>
                <guid isPermaLink="false">69f248846e0124c05e445b7a</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ golang ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Njong Emy ]]>
                </dc:creator>
                <pubDate>Wed, 29 Apr 2026 18:05:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e49dda12-fd5e-4474-aa18-b72624640bf3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine that you want to share your source code with someone who doesn’t have Go installed on their computer. Unfortunately, this person won’t be able to run your application. Even if they do have Go installed, application behaviour may differ because your local development environment is different from theirs.</p>
<p>So how do you bundle up your application so that it can run the same way in every local environment? That’s where Docker comes in.</p>
<p>For beginners, Docker isn't always a very easy concept to grasp. But once you get it, I promise that it’s very interesting. So interesting that you’ll want to dockerize every application you lay your hands on.</p>
<p>For this article, a Go application will be our case study. The fundamental concept of containerization as explained here is transferable, so don’t worry too much about how dockerizing applications in another language will look like.</p>
<p>We’ll go through the basics of dockerizing a Go app with just Docker, images and containers, setting up multiple containers in one application with Docker Compose, and the constituent of a Docker Compose file.</p>
<p>By the end of this article, you'll have a basic understanding of what Docker is, what an image or container is, and how to orchestrate multiple, dependent containers with Docker Compose.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-docker">What is Docker</a>?</p>
</li>
<li><p><a href="#heading-how-to-install-docker">How to Install Docker</a></p>
</li>
<li><p><a href="#heading-what-is-a-dockerfile">What is a Dockerfile</a>?</p>
</li>
<li><p><a href="#heading-what-is-docker-compose">What is Docker Compose</a>?</p>
</li>
<li><p><a href="#heading-the-app-container">The app Container</a></p>
</li>
<li><p><a href="#heading-the-database-container">The database Container</a></p>
</li>
<li><p><a href="#heading-the-phpmyadmin-container">The phpMyAdmin Container</a></p>
</li>
<li><p><a href="#heading-running-everything-together">Running Everything Together</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You don't need any prior knowledge of Docker to follow this tutorial. This article is written with a beginner POV in mind, so it's okay if the concept is new to you.</p>
<p>In order to be fully engaged and understand the Go coding examples used here, it'll be helpful if you have basic knowledge of Golang. If you already understand how to set up a Go application on your local computer, you're good to go. If not, you can check this article on <a href="https://www.freecodecamp.org/news/how-to-get-started-coding-in-golang/">how to get started coding in Go</a>.</p>
<h2 id="heading-what-is-docker">What is Docker?</h2>
<p>Imagine that you have a box. In that box, you put your code and everything that it needs to run. That is, the programming language it uses and any other external packages you need to install.</p>
<p>If someone needs your application, you can just hand them the box. You can also hand this box to as many people as you want. They don’t need to install the language or any other thing on their computer because everything they need is already inside the box. So, when they run the application, what they're actually doing is running an instance of that box.</p>
<p>The app is running within the box which is the standard environment. This means for everyone who got the box and “opened it”, the application is going to run the exact same way.</p>
<p>With the help of Docker, apps can run under the same conditions across different systems, and you avoid the problem of “it works on my machine”.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3b2b169d-d882-48a8-88bf-233e4acec611.png" alt="A box containing dependencies, runtime, and source code that has arrows pointing to multiple developers" style="display:block;margin:0 auto" width="800" height="332" loading="lazy">

<p>In technical Docker terms, this box is called an <strong>image</strong> and the running instance is called a <strong>container</strong>.</p>
<p>An image is a lightweight, standalone, executable package that includes everything needed to run a piece of software. That is, code, runtime, libraries, system tools, and even the operating system.</p>
<p>A container is simply a runnable instance of an image. This represents the execution environment for a specific application.</p>
<p>If all this seems to abstract, don’t worry. We’ll get our hands dirty in a little bit.</p>
<h2 id="heading-how-to-install-docker">How to Install Docker</h2>
<p>In order to install Docker, we're going to install Docker Desktop which comes bundled up with the Docker Engine. Docker Destop is a GUI for managing containers, and you'll see how useful it is in subsequent sections.</p>
<p>At the time of writing, I'm using WSL (Windows Sub-system for Linux). If you're doing the same, you'll need to take that into consideration before installing because Docker requires different installation prerequisites and steps for different operating systems.</p>
<p>To install Docker Desktop on WSL,</p>
<ol>
<li><p>Download and install the <a href="https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe?utm_source=docker&amp;utm_medium=webreferral&amp;utm_campaign=docs-driven-download-windows&amp;_gl=1*6mcgze*_gcl_au*MTg5NDEzMjg4NS4xNzc0ODU5MzQ3*_ga*MTkwMzQzNjIyLjE3NzQ4NTkzNDc.*_ga_XJWPQMJYHQ*czE3NzY2MzUyMzgkbzMkZzEkdDE3NzY2MzY3MDkkajYwJGwwJGgw">windows</a> <code>.exe</code> file</p>
</li>
<li><p>Start Docker Desktop from the Start Menu and navigate to settings</p>
</li>
<li><p>Select <strong>Use WSL 2 based engine</strong> from the <strong>General</strong> tab</p>
</li>
<li><p>Click on apply.</p>
</li>
</ol>
<p>That’s it for the WSL installation. If you are running another operating system, the <a href="https://docs.docker.com/get-started/introduction/get-docker-desktop/">official docs</a> have a list of installation options for you.</p>
<h2 id="heading-what-is-a-dockerfile">What is a Dockerfile?</h2>
<p>In order to build your box in the first place, Docker needs to follow a couple of outlined steps. It needs to know the dependencies, the run time, and it also needs to have the source code. All these steps we list in a Dockerfile.</p>
<p>Before we get down to cracking anything, let’s create a working directory and navigate into it.</p>
<pre><code class="language-bash">mkdir go_book_api &amp;&amp; cd go_book_api
</code></pre>
<p>To intialise the Go module in your application, run the following command:</p>
<pre><code class="language-bash">go mod init go_book_api
</code></pre>
<p>This creates a <code>go.mod</code> file to keep track of your project dependencies. In the root of the project, create a <code>cmd</code> directory, and a <code>main.go</code> file in it. This will serve as the entry point of your application. In the <code>main.go</code> file, you can have a simple print statement:</p>
<pre><code class="language-go">// cmd/main.go
package main

import "fmt"

func main() {
	fmt.Println("Look at me gooo!")
}
</code></pre>
<p>Now, go ahead and create a file in the root of your project and call it <code>Dockerfile</code>. This file has no extensions, but your system automatically knows that it's a file for Docker commands.</p>
<p>Go ahead and paste the following in that file, and then we'll go through each of them one by one:</p>
<pre><code class="language-bash"># base image
FROM golang:1.24

# define the working directory
WORKDIR /app

# copy the go.mod and go.sum so that the packages to be installed
# are known in the container. ./ here is the WORKDIR, /app
COPY go.mod ./

# command to install modules
RUN go mod download

# copy source code into working dir
COPY . .

# build
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping ./cmd/main.go

# run the compiled binary when the container starts
CMD ["/docker-gs-ping"]
</code></pre>
<p>Most Dockerfiles begin with a base image, which is specified by the <code>FROM</code> keyword. A base image is a foundational template that provides minimal operating system environment, libraries, or dependencies required to build and run an application within a container.</p>
<p>In this case, your base image is <code>golang:1.24</code> . Your base image could have been an operating system like Linux. In that case. when you ship your code to someone who isn’t running a Linux operating system, they wouldn’t have to worry because they will be running the application in an environment that already has a minimal Linux OS. In the same light, someone who doesn’t have Go installed locally can run your application.</p>
<p>To figure out what base image to use when setting up your Dockerfile, you can always peruse the official Docker Hub repository for published images. For this case, you can check out base images that are officially published by Golang <a href="https://hub.docker.com/hardened-images/catalog/dhi/golang/images">here</a>.</p>
<p>The next step is to define a working directory. Inside your box, you have a filesystem that is almost identical to the ones you’d see on a Linux system. You have folders like <code>/app</code>, <code>/bin</code> , <code>/usr</code> , and <code>/var</code> , and so on. The working directory you've defined in this case is <code>/app</code>, and it's done with the <code>WORKDIR</code> command.</p>
<p>After setting a working directory, you want to copy the <code>go.mod</code> and <code>go.sum</code> file into it, so that Docker knows what dependencies to add into your box.</p>
<p>The <code>COPY</code> command in Docker takes at least two arguments: the source directory(ies), and then the destination directory. In this case, you want to copy <code>go.mod</code> and <code>go.sum</code> into the working directory of your box, <code>/app</code>.</p>
<p>In the box, you'll run a command that downloads and installs all the modules defined in the <code>go.mod</code> file. To run a command in Docker environment, use <code>RUN</code> and then the command, which is <code>go mod download</code> in this case.</p>
<p>The next step is to copy any source code you have into the working directory.</p>
<p>At this point, you have the dependencies and the source code. The last step is to build the Go application into a single executable file which can be run inside your environment (inside the container).</p>
<p>Within the container, you’ll have a compiled binary at <code>/docker-gs-ping</code>, which is as a result of the compilation of the code in your <code>main.go</code> file. The last step is a <code>RUN</code> command that just tells Docker to run the executable binary after building it. It’s a way of saying “once the container starts running, execute this binary file”.</p>
<p>With these steps, Docker will build an image (a box per our analogy) that you can run. To build the image, you can run this command in your terminal:</p>
<pre><code class="language-go">docker build -t go_book_api .
</code></pre>
<p>The <code>docker build</code> command tells Docker to build an image based on the steps in the Dockerfile. <code>-t</code> is the flag for a tag, and this helps you refer to the image later when running the container.</p>
<p>To accompany your tag, you'll provide a name to the image which is <code>go_book_api</code> in this case. The <code>.</code> at the end is important because it tells Docker where the Dockerfile in question is, and the files that you need to copy into your image.</p>
<p>This is what the building looks like in my IDE:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/361a805e-153d-4034-9d9a-d34c9015738a.png" alt="screenshot of IDE terminal showing a Docker image being built" style="display:block;margin:0 auto" width="1910" height="992" loading="lazy">

<p>If you check the Images tab on Docker Compose, you'll see that an image is built:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/b569277e-295b-4a3d-8e51-fb91dd7e3d91.png" alt="screenshot of a built container image on Docker Desktop" style="display:block;margin:0 auto" width="1881" height="363" loading="lazy">

<p>You can host this image on a public image repository platform like <a href="https://www.docker.com/products/docker-hub/">Docker Hub</a>, and share it with your friends. They can pull your image, set it up, and run your application even if they don’t have Go installed. All they need to do is get the container running.</p>
<p>If you click on the little play button to the far-right, you can spin up an instance of the image (a container).</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/09726294-be22-458d-b660-5f6d32102205.png" alt="screenshot of Docker Compose modal for running a new container" style="display:block;margin:0 auto" width="825" height="758" loading="lazy">

<p>You can give a descriptive name to the container (Docker will generate a random one if you don’t), and click on the Run button. Once the container starts running, you're redirected to its log page.</p>
<p>Your container is up and running! You can see that this is a running instance of your application.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3133c16c-0950-4f03-9502-ae6495535c13.png" alt="screenshot of a running docker container on Docker Compose" style="display:block;margin:0 auto" width="1918" height="663" loading="lazy">

<h2 id="heading-what-is-docker-compose">What is Docker Compose?</h2>
<p>If you were building a simple Go application that needed no external dependencies, the above set-up would be more than sufficient.</p>
<p>In our example here, the application is supposed to be for a book API, so you’d expect that we'd have some service like a database and a database administrator client like phpMyAdmin to visualize or tables.</p>
<p>To set all this up in one file would be a little complicated using just Docker. This is because Docker doesn't allow you to have one base image for Go, another base image for a database, and so on, in one file.</p>
<p>You could use the base image of a small operating system, and then run commands to manually install these other services as dependencies, but this method makes your application hard to maintain and scale. This method isn't advisable because if one dependency crashes, the whole application will collapse instantly.</p>
<p>To remedy this situation, Docker compose allows you to have multiple containers for your application that are connected together. Docker compose handles running the containers in the right order, allows one container to use a folder from another container, or even keep its data in another container – and so on.</p>
<p>Our previous analogy of boxes is the same, except with Docker Compose, we don’t necessarily have only one box anymore:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/2c890de4-8d5d-4457-a27a-fc441f58d794.png" alt="image of a box containing multiple containers that have arrows pointing to different developers" style="display:block;margin:0 auto" width="823" height="609" loading="lazy">

<p>The point of Docker Compose is to help you orchestrate multiple images needed to run your application. You can think of it as connecting several boxes together.</p>
<p>Following the explanation from before, your application would be running in the <code>Go book api</code> container, the book data we'll create with your application would be stored in the <code>mysql</code> container which is the database, and you can visualize your database with phpMyadmin, which is in the <code>phpMyadmin</code> container.</p>
<p>To see this technically, create a <code>docker-compose.yml</code> file in the root of the project. The name of this file is important, and Docker Compose only accepts filenames such as <code>compose.yml</code> , <code>docker-compose.yml</code> , or <code>docker-compose.yaml</code>. The file extension hints that the commands are written in <code>yaml</code> which is a language mostly used for file configurations.</p>
<pre><code class="language-bash">services:
  app:
    depends_on:
      - database
    build: 
      context: .
    container_name: go_book_api
    hostname: go_book_api
    networks:
      - go_book_api_net
    ports:
      - 8080:8080
    env_file:
      - .env
    
  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USER}
    volumes:
      - mysql-go:/var/lib/mysql
    ports:
      - 3356:3306
    networks:
      - go_book_api_net

  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 9000:80
    environment:
      PMA_HOST: database
      PMA_ARBITRARY: 1
    depends_on:
      - database
    networks:
      - go_book_api_net

volumes:
  mysql-go:

networks:
  go_book_api_net:
    driver: bridge
</code></pre>
<p>At the root level of the docker-compose file, you have <code>services</code> . These are all the containers that are your application needs to run, and in the context of Docker Compose, they're each regarded as a service.</p>
<h3 id="heading-the-app-container">The <code>app</code> Container</h3>
<pre><code class="language-bash"> app:
    depends_on:
      - database
    build: 
      context: .
    container_name: go_book_api
    hostname: go_book_api
    networks:
      - go_book_api_net
    ports:
      - 8080:8080
    env_file:
      - .env
</code></pre>
<p>The very first container is the <code>app</code> container, which is your Go application. Under the <code>app</code> container, you'll need to define a few parameters that this container also needs to run.</p>
<p>The <code>depends_on</code> attribute controls the start-up and shut-down order of services within a container. This ensures that if container A depends on container B to start, the container B should be started first so that container A can use it. In this case, the <code>database</code> container must be started before the <code>app</code> container. Note that this doesn't mean <code>app</code> will always wait for the <code>database</code> to be ready.</p>
<p>The next attribute which is <code>build</code> tells Docker Compose to build the Docker image from the local project. Since the Dockerfile for your application is in the root of your app, you'll specify the root path with the <code>context</code> attribute as <code>.</code> .</p>
<p>To give a specific name to your container, you'll use <code>container_name</code>. <code>hostname</code> is what other containers will use for communication.</p>
<p>Recall that the point of Docker Compose is to have multiple containers communicating with each other. They do this with the help of networks. So you'll create another attribute, <code>networks</code>, and give it a name, <code>go_book_api_net</code> . To every other container that you want to associate with this <code>app</code>, you're going to specify the same network.</p>
<p>The next attribute is <code>ports</code> . Your application is an API, which means it's running on a backend Go server. To access the API, you'll need to map a local port to a port on the container. You're mapping port <code>8080</code> on your computer to port <code>8080</code> in the container.</p>
<p>The <code>env_file</code> attribute just tells Docker Compose where to read environment variables from. In this case, you can create a <code>.env</code> file in the root of your project to store important variables that your container will need.</p>
<h3 id="heading-the-database-container">The <code>database</code> Container</h3>
<pre><code class="language-bash">  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USER}
    volumes:
      - mysql-go:/var/lib/mysql
    ports:
      - 3356:3306
    networks:
      - go_book_api_net
</code></pre>
<p>The second container is the <code>database</code> container. Note, that you can give whatever name you choose to your listed services, but giving your containers descriptive names is always a good convention to follow.</p>
<p>For your Go application database, you'll be working with a MySQL database in this case. Your application needs MySQL to run, so you must set it up as one of the services.</p>
<p>Remember that to build a container, you need a base image. Your base image in this case is <code>mysql:8.0</code> , as you've specified with the <code>image</code> property above. When trying to set up this container, Docker Compose knows to build your database container from this already existing official image.</p>
<p>If you’ve set up a database locally before, you know that configuration is a step you can’t skip. Every database you create needs a user, a password, and the database name. You can set these variables up in the <code>environment</code> property. Instead of hardcoding these values, you can set them up in a <code>.env</code> file, and reference the environmental variables as you've done here.</p>
<p>Database servers usually listen on specific ports for incoming connections, whether the database is running locally or remotely. Just as you specified for your <code>app</code> container, you can set a port for your database and map it to a corresponding port in the container. If you want to access the database locally, you'd do that on port <code>3356</code>, and all requests are forwarded to port <code>3306</code> in the database container.</p>
<p>Once your containers go functional and your application starts running, creating, and storing data in the database, you’ll realise that every time you stop and then restart your containers, you lose the data stored in the database.</p>
<p>To avoid this, you'll need to store your data outside the container. That way, you won't lose the contents of your database every time you stop running your containers.</p>
<p>This is what volumes are for. You can allocate a specific location outside the database container to store all that content. For your <code>volume</code> in this case, the storage location you specified is <code>mysql-go:/var/lib/mysql</code> .</p>
<p>Just as you set the network in your <code>app</code> container above to <code>go_book_api_net</code>, you'll specify the same network for this database container. Since you want the containers to communicate with each other, it makes sense that they're within the same network.</p>
<h3 id="heading-the-phpmyadmin-container">The <code>phpMyAdmin</code> Container</h3>
<p>The last container or last service you need (but that is optional) to configure in this case is the phpMyAdmin container. I find it easier having a database client because it lets me easily see the structure and content of my database.</p>
<pre><code class="language-bash"> phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 9000:80
    environment:
      PMA_HOST: database
      PMA_ARBITRARY: 1
    depends_on:
      - database
    networks:
      - go_book_api_net
</code></pre>
<p>The process is almost the same as the previous containers you've configured. You'll start by pulling the official <code>phpmyadmin</code> image from Docker so that your container is built on it.</p>
<p>The <code>restart</code> option here is just so that if you stop and restart the container, phpMyAdmin automatically reloads again.</p>
<p>On the host machine, which is your local environment, you can have access to this service via port <code>9000</code> and it maps to port <code>80</code> in the container.</p>
<p>As for the <code>environment</code> , <code>PMA_HOST</code> tells phpMyAdmin to connect to a host called <code>database</code> (which is your database container). This works because both containers are on the same network, as you can see in the <code>networks</code> attribute. <code>PMA_ARBITRARY</code> is used so that if you decide to connect to another host (say, you set up a another database in future and still wish to connect via phpMyAdmin), you can do that via the UI.</p>
<p>Your database client depends on the <code>database</code> container, and so you need to specify that in <code>depends_on</code>:</p>
<pre><code class="language-bash">volumes:
  mysql-go:

networks:
  go_book_api_net:
    driver: bridge
</code></pre>
<p>The final section of your Docker Compose file is where you declared named values for the volume and network you've used in setting up your containers.</p>
<p>For the <code>volumes</code>, you'll declare a value called <code>mysql-go</code>. To the container where you want to attach this volume, you'll assign a specific storage location. You can see this in use in the database container.</p>
<pre><code class="language-bash"> volumes:
      - mysql-go:/var/lib/mysql
</code></pre>
<p>The same concept follows for the network. You have a named network called <code>go_book_api_net</code> that every container within this same network can use. The <code>driver</code> option is used here to specify the network type, and <code>bridge</code> is used for private internal networks.</p>
<h3 id="heading-running-everything-together">Running Everything Together</h3>
<p>Before Docker Compose, you had one Dockerfile that built a single container for your Go application. With Docker Compose, You’re gonna be building three containers (your application container, the database, and phpMyAdmin), and orchestrating them to work together as one single application.</p>
<p>You can push all this to a platform like GitHub, and someone can clone, start, and run the application without having any of these services (MySQL or PhpMyAdmin) installed locally on their computer. But they do need to have Docker installed.</p>
<p>To build your containers all together, you can use the command <code>docker compose build</code>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/0040fbdc-c541-494f-af9b-664d6a00bc17.png" alt="screenshot of IDE terminal showing build for an image" style="display:block;margin:0 auto" width="1871" height="959" loading="lazy">

<p>If you check your Docker Compose UI again, we see that a new image has been built, and it corresponds to the app service</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/736be9be-feb1-4888-8d15-c818e4683f4b.png" alt="screenshot of a built image on Docker Desktop" style="display:block;margin:0 auto" width="1913" height="363" loading="lazy">

<p>To start running the containers, you can use the command <code>docker compose up</code>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/8ba14bb9-77d5-48a1-b574-54a848f54b1e.png" alt="a screenshot of running containers in terminal IDE" style="display:block;margin:0 auto" width="1912" height="993" loading="lazy">

<p>If you navigate to the container tab of Docker Compose, you can see that your containers are up and running:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/82e3d54d-bfec-4cea-806a-c52846a3e077.png" alt="A screenshot of running containers on Docker Desktop" style="display:block;margin:0 auto" width="1910" height="202" loading="lazy">

<p>The main app service, <code>go_book_api</code>, isn’t running because when you run your image, your binary runs and exits almost immediately.</p>
<p>In your <code>main.go</code>, let’s rewrite the code to set up a minimal HTTP handler function that listens on port <code>8080</code>:</p>
<pre><code class="language-go">// cmd/main.go
package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	log.Println("listening on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}
</code></pre>
<p>If you’re new to Go, don’t let the code above bother you too much. All it does it set up a <code>health</code> endpoint with an associated handler function that listens on a port (<code>8080</code> in this case) and prints “ok”.</p>
<p>In your <code>Dockerfile</code>, let’s add a command to execute the created binary when the container starts:</p>
<pre><code class="language-go"># run the compiled binary when the container starts
CMD ["/docker-gs-ping"]
</code></pre>
<p>After adding this, you'll need to rebuild the containers and start them again. You can see that all containers are running now:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/3ddf3e15-87b8-4978-851f-d6179e323166.png" alt="A screenshot of running containers on Docker Desktop" style="display:block;margin:0 auto" width="1555" height="202" loading="lazy">

<p>If you click on the <code>go_book_api</code> container, you can see that your server is running on port <code>8080</code> as configured:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/ddd07614-eb53-4bfc-b088-e824f651ef6c.png" alt="A screenshot of a running container on Docker Desktop" style="display:block;margin:0 auto" width="1919" height="522" loading="lazy">

<p>Since your app is running on port <code>8080</code> and you have a <code>/health</code> endpoint set up for it, you can actually visit that endpoint in a browser to see the output “ok”.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/39a1ea3e-7cbf-4d46-9bbe-bf8053d48586.png" alt="an image of health endpoint showing ok response on the browser" style="display:block;margin:0 auto" width="1086" height="163" loading="lazy">

<p>Also, if you click on the exposed <code>phpmyadmin</code> port, you can access the database client locally on port <code>9000</code>. Based on the environment variables set up in the <code>.env</code> file, you can log in.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/8d7de244-7268-4d17-a779-785feae389c4.png" alt="screenshot of browser with phpMyAdmin login form" style="display:block;margin:0 auto" width="1915" height="962" loading="lazy">

<p>Another interesting thing to look for on Docker desktop is volumes. There is a volumes tab where you can see your configured <code>mysql-go</code> volume.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61d7e29f8d56921d07b9014e/66d1dde3-2fc1-48aa-b701-7504dba2007f.png" alt="a screenshot of the volumes tab on Docker Desktop" style="display:block;margin:0 auto" width="1910" height="254" loading="lazy">

<p>You can always open these volumes/containers on the docker GUI, go through the files and logs, experiment with putting one container down and seeing how the others respond, and so on.</p>
<p>After this entire setup, what do you notice? You didn’t have to install Go, MySQL, or phpMyAdmin locally. You only used officially published base images to orchestrate a full application. That's the magic of Docker.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Docker can be very abstract at the beginning, but understanding the fundamental purpose behind it makes everything much clearer.</p>
<p>In this article, you've learned what Docker is, how to containerize a basic Go application, and how to manage multiple containers with Docker Compose.</p>
<p>If you have trouble wrapping your head around why or how the Dockerfile is set up in the order that it is, my advice is not to get too stuck figuring it out on your own. As a Docker beginner, I realised that it’s easier if you imagine it as creating a recipe. If you try to build an image and it fails, you know there’s a step that you’re skipping.</p>
<p>The <a href="https://www.docker.com/">official docker documentation</a> has amazing resources if you want to understand Docker further than this tutorial. I encourage you to do so because this article only scratches the surface of the amazing things you can achieve with containerization.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Docker Compose for Production Workloads — with Profiles, Watch Mode, and GPU Support ]]>
                </title>
                <description>
                    <![CDATA[ There's a perception problem with Docker Compose. Ask a room full of platform engineers what they think of it, and you'll hear some version of: "It's great for local dev, but we use Kubernetes for rea ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-docker-compose-for-production-workloads/</link>
                <guid isPermaLink="false">69aadee178c5adcd0e18ddd3</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balajee Asish Brahmandam ]]>
                </dc:creator>
                <pubDate>Fri, 06 Mar 2026 14:04:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/73c5f43a-321c-4ce1-8eb4-872b532cc8dd.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>There's a perception problem with Docker Compose. Ask a room full of platform engineers what they think of it, and you'll hear some version of: "It's great for local dev, but we use Kubernetes for real work."</p>
<p>I get it. I held that same opinion for years. Compose was the thing I used to spin up a Postgres database on my laptop, not something I'd trust with a staging environment, let alone a workload that needed GPU access.</p>
<p>Then 2024 and 2025 happened. Docker shipped a set of features that quietly transformed Compose from a developer convenience tool into something that can handle complex deployment scenarios. Profiles let you manage multiple environments from a single file. Watch mode killed the painful rebuild cycle that made container-based development feel sluggish. GPU support opened the door to ML inference workloads. And a bunch of smaller improvements (better health checks, Bake integration, structured logging) filled in the gaps that used to make Compose feel like a toy.</p>
<p>Here's what I'll cover: using Docker Compose profiles to manage multiple environments from one file, setting up watch mode for instant code syncing during development, configuring GPU passthrough for machine learning workloads, implementing proper health checks and startup ordering so your services stop crashing on cold starts, and using Bake to bridge the gap between your local Compose workflow and production image builds. I'll also tell you where Compose still falls short and where you should reach for something else.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>You should be comfortable with Docker basics and have written a <code>compose.yaml</code> file before. You'll need Docker Compose v2 installed. The minimum version depends on which features you want: <code>service_healthy</code> dependency conditions require v2.20.0+, watch mode requires v2.22.0+, and the <code>gpus:</code> shorthand requires v2.30.0+. Run <code>docker compose version</code> to check what you have.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-modern-compose-file-whats-changed">The Modern Compose File: What's Changed</a></p>
</li>
<li><p><a href="#heading-how-to-use-profiles-to-manage-multiple-environments">How to Use Profiles to Manage Multiple Environments</a></p>
<ul>
<li><a href="#heading-real-world-profile-patterns-ive-used">Real-World Profile Patterns I've Used</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-use-watch-mode-to-end-the-rebuild-cycle">How to Use Watch Mode to End the Rebuild Cycle</a></p>
<ul>
<li><a href="#heading-watch-mode-vs-bind-mounts">Watch Mode vs. Bind Mounts</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-set-up-gpu-support-for-machine-learning-workloads">How to Set Up GPU Support for Machine Learning Workloads</a></p>
<ul>
<li><a href="#heading-how-to-combine-multi-gpu-workloads-with-profiles">How to Combine Multi-GPU Workloads with Profiles</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-configure-health-checks-dependencies-and-startup-ordering">How to Configure Health Checks, Dependencies, and Startup Ordering</a></p>
</li>
<li><p><a href="#heading-how-to-use-bake-for-production-image-builds">How to Use Bake for Production Image Builds</a></p>
</li>
<li><p><a href="#heading-what-compose-is-not-an-honest-assessment">What Compose Is Not (An Honest Assessment)</a></p>
</li>
<li><p><a href="#heading-a-practical-adoption-path">A Practical Adoption Path</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-the-modern-compose-file-whats-changed">The Modern Compose File: What's Changed</h2>
<p>If you haven't looked at a Compose file recently, the first thing you'll notice is that the <code>version</code> field is gone. Docker Compose v2 ignores it entirely, and including it actually triggers a deprecation warning. A modern <code>compose.yaml</code> starts cleanly with your services, no preamble needed.</p>
<p>But the structural changes go deeper than that. Here's what a modern, production-aware Compose file looks like for a typical web application stack:</p>
<pre><code class="language-yaml">services:
  api:
    image: ghcr.io/myorg/api:${TAG:-latest}
    env_file: [configs/common.env]
    environment:
      - NODE_ENV=${NODE_ENV:-production}
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

volumes:
  db-data:
</code></pre>
<p>Look at what's in there: resource limits, health checks with dependency conditions, proper volume management. These aren't nice-to-haves. They're the features that make Compose viable beyond your laptop.</p>
<p>Health checks in particular solve one of Compose's oldest and most annoying pain points: the race condition where your web server starts before the database is actually ready to accept connections. If you've ever added <code>sleep 10</code> to a startup script and crossed your fingers, you know what I'm talking about.</p>
<h2 id="heading-how-to-use-profiles-to-manage-multiple-environments">How to Use Profiles to Manage Multiple Environments</h2>
<p>This is the feature that changed my relationship with Compose. Before profiles, managing different environments meant choosing between two painful approaches. Either you maintained multiple Compose files (<code>docker-compose.yml</code>, <code>docker-compose.dev.yml</code>, <code>docker-compose.test.yml</code>, <code>docker-compose.prod.yml</code>) and dealt with the inevitable drift between them. Or you used one big bloated file where you commented out services depending on the context. Both approaches were fragile, and both led to those fun "works on my machine" conversations.</p>
<p>Profiles give you a much cleaner path. You assign services to named groups. Services without a profile always start. Services with a profile only start when you explicitly activate that profile. You can also activate profiles with the <code>COMPOSE_PROFILES</code> environment variable instead of the CLI flag, which is handy for CI (see the <a href="https://docs.docker.com/compose/how-tos/profiles/">official profiles docs</a> for the full syntax).</p>
<p>Here's what that looks like:</p>
<pre><code class="language-yaml">services:
  api:
    image: myapp:latest
    # No profiles = always starts

  db:
    image: postgres:16
    # No profiles = always starts

  debug-tools:
    image: busybox
    profiles: [debug]
    # Only starts with --profile debug

  prometheus:
    image: prom/prometheus
    profiles: [monitoring]
    # Only starts with --profile monitoring

  grafana:
    image: grafana/grafana
    profiles: [monitoring]
    depends_on: [prometheus]
</code></pre>
<p>Now your team operates with simple, memorable commands:</p>
<pre><code class="language-bash"># Development: just the core stack
docker compose up -d

# Development with observability
docker compose --profile monitoring up -d

# CI: core stack only (no monitoring overhead)
docker compose up -d

# Full stack with debugging
docker compose --profile debug --profile monitoring up
</code></pre>
<p>One Compose file. No drift. No guesswork about which override file to pass.</p>
<h3 id="heading-real-world-profile-patterns-ive-used">Real-World Profile Patterns I've Used</h3>
<p>Four patterns I keep coming back to:</p>
<p><strong>The "infra-only" pattern.</strong> This is for developers who run application code natively on their host machine but need infrastructure services like databases, message queues, and caches in containers. You leave infrastructure services without a profile and put application services behind one. Your backend developer runs <code>docker compose up</code> to get Postgres and Redis, then starts the API directly on their host with their favorite debugger attached.</p>
<p><strong>The "mock vs. real" pattern.</strong> You put a <code>payments-mock</code> service in the <code>dev</code> profile and a real payments gateway service in the <code>prod</code> profile. Same Compose file, totally different behavior depending on context. This one saved my team from accidentally hitting a live payment API during development more than once.</p>
<p><strong>The "CI optimization" pattern.</strong> Heavy services like Selenium browsers and monitoring stacks go behind profiles so your CI pipeline skips them. Your test suite runs faster without that overhead, and you only pull those services in when you actually need end-to-end integration tests.</p>
<p><strong>The "AI/ML workloads" pattern.</strong> GPU-dependent services (inference servers, model training containers) go into a <code>gpu</code> profile. Developers without GPUs can still work on the rest of the stack without anything breaking.</p>
<p>One practical tip that's saved me a lot of headaches: document your profiles in the project's README. It sounds obvious, but when a new team member runs <code>docker compose up</code> and wonders why the monitoring dashboard isn't starting, they need a single place to find the answer. A quick table listing each profile and what it includes will save you from answering the same Slack question every onboarding cycle.</p>
<h2 id="heading-how-to-use-watch-mode-to-end-the-rebuild-cycle">How to Use Watch Mode to End the Rebuild Cycle</h2>
<p>If profiles solved the environment management problem, watch mode solved the developer experience problem.</p>
<p>You probably know the old workflow for container-based development. It went like this: edit code, run <code>docker compose build</code>, run <code>docker compose up</code>, test your change, find a bug, edit again, rebuild, restart, test. Each iteration costs you thirty seconds to a minute of waiting. Over a full day of active development, you're losing an hour or more just sitting there watching build logs scroll by.</p>
<p>Watch mode (introduced in Compose v2.22.0 and significantly improved in later releases) monitors your local files and automatically takes action when something changes. It supports three synchronization strategies, and picking the right one for each situation is the key to making it work well. The <a href="https://docs.docker.com/compose/how-tos/file-watch/">official watch mode docs</a> cover the full spec if you want to dig deeper.</p>
<p><code>sync</code> copies changed files directly into the running container. This works best for interpreted languages like Python, JavaScript, and Ruby, and for frameworks with hot module reloading like React, Vue, or Next.js. The file lands in the container, the framework picks up the change, and your browser updates. No rebuild, no restart. If you're working with a compiled language like Go, Rust, or Java, <code>sync</code> won't help you since the code needs to be recompiled. Use <code>rebuild</code> for those instead.</p>
<p><code>rebuild</code> triggers a full image rebuild and container replacement. You want this for dependency changes, like when you update <code>package.json</code> or <code>requirements.txt</code>, or when you modify the Dockerfile itself. In those cases, syncing files isn't enough. You need a fresh image.</p>
<p><code>sync+restart</code> syncs files into the container, then restarts the main process. This is ideal for configuration file changes like <code>nginx.conf</code> or database configs, where the application needs to reload to pick up the new settings but the image itself is fine.</p>
<p>Here's what a real-world watch configuration looks like for a Node.js application:</p>
<pre><code class="language-yaml">services:
  api:
    build: .
    ports: ["3000:3000"]
    command: npx nodemon server.js
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
        - action: sync+restart
          path: ./config
          target: /app/config
</code></pre>
<p>You start it with <code>docker compose up --watch</code>, or you can run <code>docker compose watch</code> as a standalone command if you'd rather keep the file sync events separate from your application logs.</p>
<p>A few things to know before you set this up. Watch mode only works with services that have a local <code>build:</code> context. If you're pulling a prebuilt image from a registry, there's nothing for Compose to sync or rebuild, so watch will ignore that service. Your container also needs basic file utilities (<code>stat</code>, <code>mkdir</code>) installed, and the container <code>USER</code> must have write access to the target path. If you're using a minimal base image like <code>scratch</code> or <code>distroless</code>, the <code>sync</code> action won't work. And if you're on an older Compose version, check which actions are supported: <code>sync+restart</code> and <code>sync+exec</code> were added in later minor releases after the initial v2.22.0 launch.</p>
<p>It's a massive improvement. Edit a source file, save it, and the change is live in under a second for frameworks with hot reload. No context switching to run build commands. No waiting. Just code.</p>
<h3 id="heading-watch-mode-vs-bind-mounts">Watch Mode vs. Bind Mounts</h3>
<p>A fair question you might be asking: bind mounts have provided a form of live-reload for years. Why does watch mode need to exist?</p>
<p>Bind mounts work, but they come with platform-specific issues that have plagued Docker Desktop for a long time. On macOS and Windows, bind mounts go through a filesystem sharing layer between the host OS and the Linux VM running Docker. This introduces permission quirks, performance problems on large directories (ever watched a <code>node_modules</code> folder choke a bind mount on macOS?), and inconsistent file notification behavior that makes hot reload unreliable.</p>
<p>Watch mode sidesteps these issues by explicitly syncing files at the application level. It's more predictable, works consistently across platforms, and gives you more control over what happens when a file changes.</p>
<p>That said, bind mounts still work well for many use cases, especially if you're on native Linux where the performance overhead doesn't exist. Watch mode is the better choice for teams that have run into cross-platform issues, or for anyone who wants the automatic rebuild and restart triggers that bind mounts can't provide.</p>
<h2 id="heading-how-to-set-up-gpu-support-for-machine-learning-workloads">How to Set Up GPU Support for Machine Learning Workloads</h2>
<p>This is the feature that made me rethink what Compose can do.</p>
<p>Docker has supported GPU passthrough for individual containers for years through the NVIDIA Container Toolkit and the <code>--gpus</code> flag. But configuring GPU access in Compose files used to require clunky runtime declarations that were poorly documented and changed between Compose versions. It was the kind of thing where you'd find a Stack Overflow answer from 2021, try it, and discover it didn't work anymore.</p>
<p>The modern Compose spec handles it cleanly through the <code>deploy.resources.reservations.devices</code> block:</p>
<pre><code class="language-yaml">services:
  inference:
    image: myorg/model-server:latest
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
</code></pre>
<p>If you're on Compose v2.30.0 or later, there's also a shorter syntax using the <code>gpus:</code> field:</p>
<pre><code class="language-yaml">services:
  inference:
    image: myorg/model-server:latest
    gpus:
      - driver: nvidia
        count: 1
</code></pre>
<p>Both approaches do the same thing. The <code>deploy.resources</code> syntax works on older Compose versions and gives you more control (like setting <code>device_ids</code> to pin specific GPUs). The <code>gpus:</code> shorthand is cleaner when you just need basic access.</p>
<p><strong>One thing that will trip you up if you skip it:</strong> your host machine needs the right GPU drivers and <code>nvidia-container-toolkit</code> installed before any of this works. Run <code>nvidia-smi</code> on the host first. If that command doesn't show your GPUs, Compose won't see them either. For CUDA workloads, use official GPU base images like <code>nvidia/cuda</code> or the PyTorch/TensorFlow GPU images. The <a href="https://docs.docker.com/compose/how-tos/gpu-support/">Compose GPU access docs</a> walk through the full setup.</p>
<p>That's the whole thing. When you run <code>docker compose up</code>, the inference service gets access to one NVIDIA GPU. You can set <code>count</code> to <code>"all"</code> if you want every available GPU, or use <code>device_ids</code> to assign specific GPUs to specific services.</p>
<h3 id="heading-how-to-combine-multi-gpu-workloads-with-profiles">How to Combine Multi-GPU Workloads with Profiles</h3>
<p>Here's where profiles and GPU support work really well together. Consider an ML workload where you need an LLM for text generation, an embedding model for vector search, and a vector database:</p>
<pre><code class="language-yaml">services:
  vectordb:
    image: milvus/milvus:latest
    # Runs on CPU, no profile needed

  llm-server:
    image: ollama/ollama:latest
    profiles: [gpu]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["1"]
              capabilities: [gpu]
    volumes:
      - model-cache:/root/.ollama

  embedding-server:
    image: myorg/embeddings:latest
    profiles: [gpu]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["0"]
              capabilities: [gpu]
</code></pre>
<p>Developers without GPUs work on the application logic with just <code>docker compose up</code>. The vector database starts, they can write code against its API, and everything runs fine. When it's time to test the full ML pipeline, someone with a multi-GPU workstation runs <code>docker compose --profile gpu up</code> and gets the complete stack with specific GPU assignments.</p>
<p>This pattern has become central to our AIOps platform development. The team building alerting logic doesn't need GPUs. The team training anomaly detection models does. One Compose file serves both teams.</p>
<h2 id="heading-how-to-configure-health-checks-dependencies-and-startup-ordering">How to Configure Health Checks, Dependencies, and Startup Ordering</h2>
<p>One of Compose's most underappreciated improvements is how it handles service dependencies. The <code>depends_on</code> directive now supports conditions that actually mean something (this requires Compose v2.20.0+, see the <a href="https://docs.docker.com/compose/how-tos/startup-order/">startup ordering docs</a> for the full picture):</p>
<pre><code class="language-yaml">depends_on:
  db:
    condition: service_healthy
  redis:
    condition: service_started
</code></pre>
<p>When you combine this with proper health checks, you eliminate the "sleep 10 and hope" pattern that plagues so many Compose setups. Your API service actually waits until PostgreSQL is accepting connections before it tries to start. Not just until the container is running, but until the database process inside it has passed its health check.</p>
<p>One detail that catches people: tune your <code>start_period</code>. Databases like PostgreSQL need time to initialize on first boot, especially if they're running migrations. Without a <code>start_period</code>, the health check starts counting retries immediately and can declare the service unhealthy before it even had a chance to finish starting up. A config like this works well for most database services:</p>
<pre><code class="language-yaml">healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 5s
  timeout: 2s
  retries: 10
  start_period: 30s
</code></pre>
<p>The <code>start_period</code> gives the container 30 seconds of grace time where failed health checks don't count against the retry limit.</p>
<p>This might seem like a small detail, but if you've ever worked on a stack with eight or ten interconnected services, you know how much time you can waste debugging cascading failures during cold starts. Proper startup ordering prevents all of that and makes your local environment behave much more like production.</p>
<h2 id="heading-how-to-use-bake-for-production-image-builds">How to Use Bake for Production Image Builds</h2>
<p>I mentioned Bake integration earlier, and it's worth its own section because it solves a problem you'll hit as soon as you start using Compose for anything beyond local dev: your development Compose file and your production build process have different needs.</p>
<p>During development, you want fast builds, local caches, and single-platform images. For production, you want tagged images pushed to a registry, multi-platform builds, and build attestations. Trying to cram both into your <code>compose.yaml</code> gets messy fast.</p>
<p>Docker Bake (<code>docker buildx bake</code>) can read your <code>compose.yaml</code> and generate build targets from it, but you can override and extend those targets with a separate <code>docker-bake.hcl</code> file. This keeps your development workflow clean while giving CI the knobs it needs. The <a href="https://docs.docker.com/build/bake/">Bake documentation</a> covers the full HCL syntax and Compose integration.</p>
<p>Here's a minimal <code>docker-bake.hcl</code>:</p>
<pre><code class="language-hcl">group "default" {
  targets = ["api", "worker"]
}

target "api" {
  context    = "api"
  dockerfile = "Dockerfile"
  tags       = ["registry.example.com/team/api:release"]
  platforms  = ["linux/amd64"]
}

target "worker" {
  context    = "worker"
  dockerfile = "Dockerfile"
  tags       = ["registry.example.com/team/worker:release"]
}
</code></pre>
<p>Then your CI pipeline runs <code>docker buildx bake</code> to produce release images, while developers keep using <code>docker compose up --build</code> locally. The two workflows share the same Dockerfiles but have separate build configurations where they need them.</p>
<p>The pattern I've landed on: use Compose for local development and CI test environments, use Bake in CI to produce the release images, and push those images into whatever deployment target your team uses (staging server, Kubernetes cluster, edge node). Compose gets you from code to running containers fast. Bake gets you from code to production-ready images with proper tags and attestations.</p>
<h2 id="heading-what-compose-is-not-an-honest-assessment">What Compose Is Not (An Honest Assessment)</h2>
<p>I've spent this entire article making the case that Compose has grown up. But I should also tell you where it falls short. I'd rather you hear it from me now than discover it the hard way in production.</p>
<p><strong>Compose is not a container orchestrator.</strong> It doesn't schedule work across multiple hosts. It doesn't do automatic failover. It won't give you rolling updates with zero downtime, and it has no concept of service mesh networking. If you need any of those things, you need Kubernetes, Nomad, or Docker Swarm (if you're still using it).</p>
<p><strong>Compose doesn't replace Helm or Kustomize.</strong> If you're deploying to Kubernetes, Compose files don't translate directly. Docker offers Compose Bridge to convert Compose files into Kubernetes manifests, but it's still experimental and won't handle complex Kubernetes-specific configurations like custom resource definitions or ingress rules.</p>
<p><strong>Compose doesn't handle secrets well in production.</strong> The secrets support exists, but it's limited compared to HashiCorp Vault, AWS Secrets Manager, or Kubernetes secrets. For anything beyond a staging environment, you'll want an external secrets management solution.</p>
<p>The sweet spot for modern Compose is clear: local development, CI/CD testing environments, single-node staging environments, and workloads where a single powerful machine (particularly for GPU work) is the right deployment target. Within that scope, Compose is excellent. Outside of it, you'll hit walls fast.</p>
<p>If you do run Compose in a staging or single-node production setup, a few more things are worth adding that I haven't covered here: <code>restart: unless-stopped</code> on every service so containers come back after a host reboot, a logging driver config so your logs go somewhere searchable instead of disappearing into <code>docker logs</code>, and a backup strategy for your named volumes. These aren't Compose-specific problems, but Compose won't solve them for you either.</p>
<h2 id="heading-a-practical-adoption-path">A Practical Adoption Path</h2>
<p>If you're currently working with a basic Compose setup and want to start using these features, here's the order I'd recommend. Each step is incremental, backward-compatible, and valuable on its own. You don't have to do all of this at once.</p>
<p><strong>Week 1: Add health checks and proper</strong> <code>depends_on</code> <strong>conditions.</strong> This alone will eliminate the most common frustration: services crashing on startup because their dependencies aren't ready yet. Start with your database and your main application service. Once those two are wired up with <code>condition: service_healthy</code>, you'll notice the difference immediately.</p>
<pre><code class="language-yaml">healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 5s
  timeout: 2s
  retries: 10
  start_period: 30s
</code></pre>
<p><strong>Week 2: Introduce profiles.</strong> Start by putting your monitoring stack behind a <code>monitoring</code> profile and your debug tools behind a <code>debug</code> profile. Then delete whatever extra Compose files you've been maintaining. Having one source of truth instead of four files that are almost-but-not-quite the same makes everything simpler.</p>
<p><strong>Week 3: Set up watch mode for your most-edited service.</strong> Pick the service where your developers spend the most time iterating. Get watch mode working there first. Once the team sees the difference (saving a file and seeing the change reflected in under a second) they'll ask for it on everything else.</p>
<p><strong>Week 4: Add resource limits.</strong> Define memory and CPU limits for every service. This prevents one runaway container from starving the rest and gives you a realistic preview of how your services behave under production constraints. It's also useful for catching memory leaks early.</p>
<pre><code class="language-yaml">deploy:
  resources:
    limits:
      memory: 512M
      cpus: "1.0"
</code></pre>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Docker Compose in 2026 is not the same tool it was a few years ago. Profiles, watch mode, GPU support, proper dependency management, and Bake integration have turned it into something that can handle real, complex workloads, as long as those workloads fit on a single node.</p>
<p>It's not Kubernetes, and it shouldn't try to be. But for local development, CI pipelines, staging environments, and single-machine GPU workloads, it's become hard to argue against. If you've been dismissing Compose because of what it used to be, the current version deserves a second look.</p>
<p>If you found this useful, you can find me writing about DevOps, containers, and AIOps best practices on my blog.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ What is Docker Compose? How to Use it with an Example ]]>
                </title>
                <description>
                    <![CDATA[ Docker helps you setup a development environment on your machine quickly. It only takes a couple of minutes to go through the entire process.  But let's assume you were assigned on a project which requires at least 10 different services in a running ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/what-is-docker-compose-how-to-use-it/</link>
                <guid isPermaLink="false">66ba10ff79b7f411df58deb3</guid>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Arunachalam B ]]>
                </dc:creator>
                <pubDate>Fri, 07 Apr 2023 19:29:48 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/04/What-is-Docker-compose-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Docker helps you setup a development environment on your machine quickly. It only takes a couple of minutes to go through the entire process. </p>
<p>But let's assume you were assigned on a project which requires at least 10 different services in a running state to run your project. For example, let's say your project requires Java 8, Node 14, MySQL, MongoDB, Ruby on rails, RabbitMQ, and others. </p>
<p>In such case, you have to pull all those images individually from Docker and start all of them in their containers. At some point, one process may depend on another to run. So, you have to order them. </p>
<p>It would be good if it's a one time process. But, not just once – every day, every time you start working on your project – you have to start all these services.</p>
<p>That's a tedious process right?</p>
<p>To overcome this, Docker introduced a concept called Multi Containers (Docker Compose). Before learning about Docker Compose, let's quickly learn how to start a database host in Docker. </p>
<p>In the example part of this tutorial, we'll spin up a NodeJS container and MongoDB container together. Learning about MongoDB at the beginning will give you a good understanding for when we move to Docker Compose. </p>
<p>We'll split this tutorial into 2 sections:</p>
<ol>
<li>How to use docker as a Database host (MongoDB)</li>
<li>How Docker Compose works with an example (NodeJS and MongoDB)</li>
</ol>
<h2 id="heading-how-to-use-docker-as-a-database-host">How to Use Docker as a Database Host</h2>
<p>If you've had experience with backend development, you might have had a chance to handle multiple databases. For example, databases like MySQL/Postgres to handle relational data and Cassandra/MongoDB to handle unstructured data.</p>
<p>Want to know a secret? You can work on back-end development without installing the database on your machine locally. Yes, you can use Docker as a database host. It has all the dependencies by default in the particular image file.</p>
<h2 id="heading-why-do-we-need-docker-database">Why Do We Need Docker Database?</h2>
<p>Docker helps us maintain consistent versions across platforms and environments. Let's say that there are a group of people working on your team on MongoDB version 5.0. If a new member joins your team, they'll need to set up the same version with the exact configuration manually. What if they install the latest version of MongoDB (6.0)? That will lead to some conflicts. This will be nightmare if it spreads to everyone's else's devices.</p>
<p>To get around this, you can use MongoDB in Docker with a custom configuration and push the MongoDB image to Docker Hub internally. If a new person comes in they can pull the image and start the implementation without any manual configuration.</p>
<p>Let's look at the advantages of using a database in Docker.</p>
<ol>
<li>By using this implementation we can ensure that everyone on a team uses the exact runtimes and configuration without any external resources.</li>
<li>It's very easy to set up and we can start/stop the server quickly using Docker desktop</li>
</ol>
<h2 id="heading-how-to-setup-mongodb-using-docker">How to Setup MongoDB using Docker</h2>
<p>If you're not familiar with Docker Hub, here's a short intro. Docker hub is a platform where you can find and share Docker images that are public or private. It is pretty similar to GitHub / GitLab. In a nutshell, it's a repository for Docker images.</p>
<p>The first step is to pull the official Docker image from Docker Hub.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-43.png" alt="Image" width="600" height="400" loading="lazy">
<em>MongoDB Image in Docker Hub</em></p>
<pre><code class="lang-bash">docker pull mongo:latest
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-44.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output for pulling Mongo image from Docker Hub</em></p>
<p>Once you're done pulling the Mongo image, open your Docker Desktop and you'll be able to see it there.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-45.png" alt="Image" width="600" height="400" loading="lazy">
<em>Mongo Image available in Docker desktop</em></p>
<p>Let's run our MongoDB image using the <code>docker run</code> command.</p>
<pre><code class="lang-bash">docker run -d -p 27017:27017 --name mongo-server-local mongo: latest
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-46.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output to run MongoDB in Docker</em></p>
<p>We've successfully run the Docker image. Now we can see the container running on Docker Desktop.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-48.png" alt="Image" width="600" height="400" loading="lazy">
<em>Mongo container running in Docker Desktop</em></p>
<p>So, the MongoDB server is running on your machine. Let's confirm this in the browser. Go to <a target="_blank" href="http://localhost:27017">http://localhost:27017</a> on your browser and you should be able to see the message as shown in the screenshot below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-49.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output "It looks like you are trying to access MongoDB over HTTP on the native driver port" for running MongoDB server using Docker</em></p>
<p>Interesting right?</p>
<p>We can stop/start the MongoDB server using Docker whenever we need.</p>
<h3 id="heading-important-note">Important Note</h3>
<ol>
<li>It is not recommended to use Docker as a database for production</li>
<li>Do not use Docker database for large scale applications</li>
</ol>
<h2 id="heading-what-is-docker-compose">What is docker-compose?</h2>
<p>Let's come back to docker-compose.</p>
<p>Docker Compose is a tool you can use to define and share multi-container applications. This means you can run a project with multiple containers using a single source.</p>
<p>For example, assume you're building a project with NodeJS and MongoDB together. You can create a single image that starts both containers as a service – you don't need to start each separately.</p>
<p>Interesting right? And this solves the problem which I called out at the very beginning of this article.</p>
<p>To achieve this we need to define a <code>docker-compose.yml</code>.</p>
<h3 id="heading-docker-composeyml-file">docker-compose.yml file</h3>
<p>The compose file is a YML file defining services, networks, and volumes for a Docker container. There are several versions of the compose file format available – 1, 2, 2.x, and 3.x.</p>
<p>Before proceeding further, here's a important note to us from the <a target="_blank" href="https://docs.docker.com/compose/">Docker Compose team</a>.</p>
<blockquote>
<p>From the end of June 2023 Compose V1 won’t be supported anymore and will be removed from all Docker Desktop versions. </p>
</blockquote>
<p>We are using version 3 in this article.</p>
<pre><code class="lang-docker-compose">version: '3'
services:
  app:
    image: node:latest
    container_name: app_main
    restart: always
    command: sh -c "yarn install &amp;&amp; yarn start"
    ports:
      - 8000:8000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: localhost
      MYSQL_USER: root
      MYSQL_PASSWORD: 
      MYSQL_DB: test
  mongo:
    image: mongo
    container_name: app_mongo
    restart: always
    ports:
      - 27017:27017
    volumes:
      - ~/mongo:/data/db
volumes:
  mongodb:
</code></pre>
<p>Let's dismantle the above code and understand it piece by piece:</p>
<ul>
<li><code>version</code> refers to the docker-compose version (Latest 3)</li>
<li><code>services</code> defines the services that we need to run</li>
<li><code>app</code> is a custom name for one of your containers</li>
<li><code>image</code> the image which we have to pull. Here we are using <code>node:latest</code> and <code>mongo</code>.</li>
<li><code>container_name</code> is the name for each container</li>
<li><code>restart</code> starts/restarts a service container</li>
<li><code>port</code> defines the custom port to run the container</li>
<li><code>working_dir</code> is the current working directory for the service container</li>
<li><code>environment</code> defines the environment variables, such as DB credentials, and so on.</li>
<li><code>command</code> is the command to run the service</li>
</ul>
<h3 id="heading-how-to-run-the-multi-container">How to run the multi-container</h3>
<p>We need to build our multi-container using docker build.</p>
<pre><code class="lang-bash">docker compose build
</code></pre>
<p>After successfully building, we can run the containers using the <code>up</code> command.</p>
<pre><code class="lang-bash">docker compose up
</code></pre>
<p>If you want to run the container in detached mode, just use the <code>-d</code> flag.</p>
<pre><code class="lang-bash">docker compose up -d
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-50.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output to run multiple containers using docker-compose in detached mode</em></p>
<p>Fine, We are good to go. The containers are up and running. Let's check the container list.</p>
<pre><code class="lang-bash">docker compose ps
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-51.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output to list the running container services</em></p>
<p>Hurray, we can see that there are two containers running at the same time.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-54.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output for running nodejs service using docker</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-55.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sample output for running mongodb service using docker</em></p>
<p>To have a look at the data in your MongoDB, you have to install MongoDB Compass.</p>
<p>Here's the screenshot of it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/image-56.png" alt="Image" width="600" height="400" loading="lazy">
<em>MongoDB server view in mongodb compass</em></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, you have learned how Docker Compose works with an example. Using multiple containers you can spin up any type of service such as RabbitMQ or Apache Kafka and run in a single source of service. Hope you enjoyed reading this article.</p>
<p>If you wish to learn more about Docker, subscribe to my article on my <a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_docker_compose">site</a> (<a target="_blank" href="https://5minslearn.gogosoon.com/?ref=fcc_docker_compose">https://5minslearn.gogosoon.com</a>) which has a consolidated list of all my blogs. </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Docker Handbook – Learn Docker for Beginners ]]>
                </title>
                <description>
                    <![CDATA[ The concept of containerization itself is pretty old. But the emergence of the Docker Engine in 2013 has made it much easier to containerize your applications. According to the Stack Overflow Developer Survey - 2020, Docker is the #1 most wanted plat... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-docker-handbook/</link>
                <guid isPermaLink="false">66b0ab4d8d675d0da5f1ab81</guid>
                
                    <category>
                        <![CDATA[ containerization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker Containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Farhan Hasin Chowdhury ]]>
                </dc:creator>
                <pubDate>Mon, 01 Feb 2021 16:35:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/The-Docker-Handbook-Mockup.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The concept of containerization itself is pretty old. But the emergence of the <a target="_blank" href="https://docs.docker.com/get-started/overview/#docker-engine">Docker Engine</a> in 2013 has made it much easier to containerize your applications.</p>
<p>According to the <a target="_blank" href="https://insights.stackoverflow.com/survey/2020#overview">Stack Overflow Developer Survey - 2020</a>, <a target="_blank" href="https://docker.com/">Docker</a> is the <a target="_blank" href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5">#1 most wanted platform</a>, <a target="_blank" href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5">#2 most loved platform</a>, and also the <a target="_blank" href="https://insights.stackoverflow.com/survey/2020#technology-platforms">#3 most popular platform</a>.</p>
<p>As in-demand as it may be, getting started can seem a bit intimidating at first. So in this book, we'll be learning everything from the basics to a more intermediate level of containerization. After going through the entire book, you should be able to:</p>
<ul>
<li>Containerize (almost) any application</li>
<li>Upload custom Docker Images to online registries</li>
<li>Work with multiple containers using Docker Compose</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li>Familiarity with the Linux Terminal</li>
<li>Familiarity with JavaScript (some later projects use JavaScript)</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-introduction-to-containerization-and-docker">Introduction to Containerization and Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-install-docker">How to Install Docker</a><ul>
<li><a class="post-section-overview" href="#heading-how-to-install-docker-on-macos">How to Install Docker on macOS</a></li>
<li><a class="post-section-overview" href="#heading-how-to-install-docker-on-windows">How to Install Docker on Windows</a></li>
<li><a class="post-section-overview" href="#heading-how-to-install-docker-on-linux">How to Install Docker on Linux</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-hello-world-in-docker-intro-to-docker-basics">Hello World in Docker - Intro to Docker Basics</a><ul>
<li><a class="post-section-overview" href="#heading-what-is-a-container">What is a Container?</a></li>
<li><a class="post-section-overview" href="#heading-what-is-a-docker-image">What is a Docker Image?</a></li>
<li><a class="post-section-overview" href="#heading-what-is-a-docker-registry">What is a Docker Registry?</a></li>
<li><a class="post-section-overview" href="#heading-docker-architecture-overview">Docker Architecture Overview</a></li>
<li><a class="post-section-overview" href="#heading-the-full-picture">The Full Picture</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-docker-container-manipulation-basics">Docker Container Manipulation Basics</a><ul>
<li><a class="post-section-overview" href="#heading-how-to-run-a-container">How to Run a Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-publish-a-port">How to Publish a Port</a></li>
<li><a class="post-section-overview" href="#heading-how-to-use-detached-mode">How to Use Detached Mode</a></li>
<li><a class="post-section-overview" href="#heading-how-to-list-containers">How to List Containers</a></li>
<li><a class="post-section-overview" href="#heading-how-to-name-or-rename-a-container">How to Name or Rename a Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-stop-or-kill-a-running-container">How to Stop or Kill a Running Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-restart-a-container">How to Restart a Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-container-without-running">How to Create a Container Without Running</a></li>
<li><a class="post-section-overview" href="#heading-how-to-remove-dangling-containers">How to Remove Dangling Containers</a></li>
<li><a class="post-section-overview" href="#heading-how-to-run-a-container-in-interactive-mode">How to Run a Container in Interactive Mode</a></li>
<li><a class="post-section-overview" href="#heading-how-to-execute-commands-inside-a-container">How to Execute Commands Inside a Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-work-with-executable-images">How to Work With Executable Images</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-docker-image-manipulation-basics">Docker Image Manipulation Basics</a><ul>
<li><a class="post-section-overview" href="#heading-how-to-create-a-docker-image">How to Create a Docker Image</a></li>
<li><a class="post-section-overview" href="#heading-how-to-tag-docker-images">How to Tag Docker Images</a></li>
<li><a class="post-section-overview" href="#heading-how-to-list-and-remove-docker-images">How to List and Remove Docker Images</a></li>
<li><a class="post-section-overview" href="#heading-how-to-understand-the-many-layers-of-a-docker-image">How to Understand the Many Layers of a Docker Image</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-nginx-from-source">How to Build NGINX from Source</a></li>
<li><a class="post-section-overview" href="#heading-how-to-optimize-docker-images">How to Optimize Docker Images</a></li>
<li><a class="post-section-overview" href="#heading-embracing-alpine-linux">Embracing Alpine Linux</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-executable-docker-images">How to Create Executable Docker Images</a></li>
<li><a class="post-section-overview" href="#heading-how-to-share-your-docker-images-online">How to Share Your Docker Images Online</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-how-to-containerize-a-javascript-application">How to Containerize a JavaScript Application</a><ul>
<li><a class="post-section-overview" href="#heading-how-to-write-the-development-dockerfile">How to Write the Development Dockerfile</a></li>
<li><a class="post-section-overview" href="#heading-how-to-work-with-bind-mounts-in-docker">How to Work With Bind Mounts in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-work-with-anonymous-volumes-in-docker">How to Work With Anonymous Volumes in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-perform-multi-staged-builds-in-docker">How to Perform Multi-Staged Builds in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-ignore-unnecessary-files">How to Ignore Unnecessary Files</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-network-manipulation-basics-in-docker">Network Manipulation Basics in Docker</a><ul>
<li><a class="post-section-overview" href="#heading-docker-network-basics">Docker Network Basics</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-user-defined-bridge-in-docker">How to Create a User-Defined Bridge in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-attach-a-container-to-a-network-in-docker">How to Attach a Container to a Network in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-detach-containers-from-a-network-in-docker">How to Detach Containers from a Network in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-get-rid-of-networks-in-docker">How to Get Rid of Networks in Docker</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-how-to-containerize-a-multi-container-javascript-application">How to Containerize a Multi-Container JavaScript Application</a><ul>
<li><a class="post-section-overview" href="#heading-how-to-run-the-database-server">How to Run the Database Server</a></li>
<li><a class="post-section-overview" href="#heading-how-to-work-with-named-volumes-in-docker">How to Work with Named Volumes in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-access-logs-from-a-container-in-docker">How to Access Logs from a Container in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-create-a-network-and-attaching-the-database-server-in-docker">How to Create a Network and Attaching the Database Server in Docker</a></li>
<li><a class="post-section-overview" href="#heading-how-to-write-the-dockerfile">How to Write the Dockerfile</a></li>
<li><a class="post-section-overview" href="#heading-how-to-execute-commands-in-a-running-container">How to Execute Commands in a Running Container</a></li>
<li><a class="post-section-overview" href="#heading-how-to-write-management-scripts-in-docker">How to Write Management Scripts in Docker</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-how-to-compose-projects-using-docker-compose">How to Compose Projects Using Docker-Compose</a><ul>
<li><a class="post-section-overview" href="#heading-docker-compose-basics">Docker Compose Basics</a></li>
<li><a class="post-section-overview" href="#heading-how-to-start-services-in-docker-compose">How to Start Services in Docker Compose</a></li>
<li><a class="post-section-overview" href="#heading-how-to-list-services-in-docker-compose">How to List Services in Docker Compose</a></li>
<li><a class="post-section-overview" href="#heading-how-to-execute-commands-inside-a-running-service-in-docker-compose">How to Execute Commands Inside a Running Service in Docker Compose</a></li>
<li><a class="post-section-overview" href="#heading-how-to-access-logs-from-a-running-service-in-docker-compose">How to Access Logs from a Running Service in Docker Compose</a></li>
<li><a class="post-section-overview" href="#heading-how-to-stop-services-in-docker-compose">How to Stop Services in Docker Compose</a></li>
<li><a class="post-section-overview" href="#heading-how-to-compose-a-full-stack-application-in-docker-compose">How to Compose a Full-stack Application in Docker Compose</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-project-code">Project Code</h2>
<p>Code for the example projects can be found in the following repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/fhsinchy/docker-handbook-projects/">https://github.com/fhsinchy/docker-handbook-projects/</a></div>
<p>You can find the complete code in the <code>completed</code> branch.</p>
<h2 id="heading-contributions">Contributions</h2>
<p>This book is completely open-source and quality contributions are more than welcome. You can find the full content in the following repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/fhsinchy/the-docker-handbook">https://github.com/fhsinchy/the-docker-handbook</a></div>
<p>I usually do my changes and updates on the GitBook version of the book first and then publish them on freeCodeCamp. You can find the always updated and often unstable version of the book at the following link:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://docker-handbook.farhan.dev/">https://docker-handbook.farhan.dev/</a></div>
<p>If you're looking for a frozen but stable version of the book, then freeCodeCamp will be the best place to go:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.freecodecamp.org/news/the-docker-handbook/">https://www.freecodecamp.org/news/the-docker-handbook/</a></div>
<p>Whichever version of the book you end up reading though, don't forget to let me know your opinion. Constructive criticism is always welcomed.</p>
<h2 id="heading-introduction-to-containerization-and-docker">Introduction to Containerization and Docker</h2>
<p>According to <a target="_blank" href="https://www.ibm.com/cloud/learn/containerization#toc-what-is-co-r25Smlqq">IBM</a>, </p>
<blockquote>
<p>Containerization involves encapsulating or packaging up software code and all its dependencies so that it can run uniformly and consistently on any infrastructure.</p>
</blockquote>
<p>‌In other words, containerization lets you bundle up your software along with all its dependencies in a self-contained package so that it can be run without going through a troublesome setup process.</p>
<p>‌Let's consider a real life scenario here. Assume you have developed an awesome book management application that can store information regarding all the books you own, and can also serve the purpose of a book lending system for your friends.</p>
<p>‌If you make a list of the dependencies, that list may look as follows:</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
</ul>
<p>Well, theoretically this should be it. But practically there are some other things as well. Turns out <a target="_blank" href="https://nodejs.org/">Node.js</a> uses a build tool known as <code>node-gyp</code> for building native add-ons. And according to the <a target="_blank" href="https://github.com/nodejs/node-gyp#installation">installation instruction</a> in the <a target="_blank" href="https://github.com/nodejs/node-gyp">official repository</a>, this build tool requires Python 2 or 3 and a proper C/C++ compiler tool-chain. </p>
<p>Taking all these into account, the final list of dependencies is as follows:</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
<li>Python 2 or 3</li>
<li>C/C++ tool-chain</li>
</ul>
<p>Installing Python 2 or 3 is pretty straightforward regardless of the platform you're on. Setting up the C/C++ tool-chain is pretty easy on Linux, but on Windows and Mac it's a painful task. </p>
<p>On Windows, the C++ build tools package measures at gigabytes and takes quite some time to install. On a Mac, you can either install the gigantic <a target="_blank" href="https://developer.apple.com/xcode/">Xcode</a> application or the much smaller <a target="_blank" href="https://developer.apple.com/downloads/">Command Line Tools for Xcode</a> package. </p>
<p>Regardless of the one you install, it still may break on OS updates. In fact, the problem is so prevalent that there are <a target="_blank" href="https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md">Installation notes for macOS Catalina</a> available on the official repository.</p>
<p>Let's assume that you've gone through all the hassle of setting up the dependencies and have started working on the project. Does that mean you're out of danger now? Of course not.</p>
<p>What if you have a teammate who uses Windows while you're using Linux. Now you have to consider the inconsistencies of how these two different operating systems handle paths. Or the fact that popular technologies like <a target="_blank" href="https://nginx.org/">nginx</a> are not well optimized to run on Windows. Some technologies like <a target="_blank" href="https://redis.io/">Redis</a> don't even come pre-built for Windows.</p>
<p>Even if you get through the entire development phase, what if the person responsible for managing the servers follows the wrong deployment procedure?</p>
<p>All these issues can be solved if only you could somehow:</p>
<ul>
<li>Develop and run the application inside an isolated environment (known as a container) that matches your final deployment environment.</li>
<li>Put your application inside a single file (known as an image) along with all its dependencies and necessary deployment configurations.</li>
<li>And share that image through a central server (known as a registry) that is accessible by anyone with proper authorization.</li>
</ul>
<p>Your teammates will then be able to download the image from the registry, run the application as it is within an isolated environment free from the platform specific inconsistencies, or even deploy directly on a server, since the image comes with all the proper production configurations.</p>
<p>That is the idea behind containerization: putting your applications inside a self-contained package, making it portable and reproducible across various environments.</p>
<p><strong>Now the question is "What role does Docker play here?"</strong></p>
<p>As I've already explained, containerization is an idea that solves a myriad of problems in software development by putting things into boxes. </p>
<p>This very idea has quite a few implementations. <a target="_blank" href="https://www.docker.com/">Docker</a> is such an implementation. It's an open-source containerization platform that allows you to containerize your applications, share them using public or private registries, and also to <a target="_blank" href="https://docs.docker.com/get-started/orchestration/">orchestrate</a> them.</p>
<p>Now, Docker is not the only containerization tool on the market, it's just the most popular one. Another containerization engine that I love is called <a target="_blank" href="https://podman.io/">Podman</a> developed by Red Hat. Other tools like <a target="_blank" href="https://github.com/GoogleContainerTools/kaniko">Kaniko</a> by Google, <a target="_blank" href="https://coreos.com/rkt/">rkt</a> by CoreOS are amazing, but they're not ready to be a drop-in replacement for Docker just yet.</p>
<p>Also, if you want a history lesson, you may read the amazing <a target="_blank" href="https://blog.aquasec.com/a-brief-history-of-containers-from-1970s-chroot-to-docker-2016">A Brief History of Containers: From the 1970s Till Now</a> which covers most of the major turning points for the technology.</p>
<h2 id="heading-how-to-install-docker">How to Install Docker</h2>
<p>Installation of Docker varies greatly depending on the operating system you’re using. But it's universally simple across the board. </p>
<p>Docker runs flawlessly on all three major platforms, Mac, Windows, and Linux. Among the three, the installation process on Mac is the easiest, so we'll start there.</p>
<h3 id="heading-how-to-install-docker-on-macos">How to Install Docker on macOS</h3>
<p>On a mac, all you have to do is navigate to the official <a target="_blank" href="https://www.docker.com/products/docker-desktop">download page</a> and click the <em>Download for Mac (stable)</em> button. </p>
<p>You’ll get a regular looking <em>Apple Disk Image</em> file and inside the file, there will be the application. All you have to do is drag the file and drop it in your Applications directory.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/drag-docker-in-applications-directory.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You can start Docker by simply double-clicking the application icon. Once the application starts, you'll see the Docker icon appear on your menu-bar.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-menubar.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now, open up the terminal and execute <code>docker --version</code> and <code>docker-compose --version</code> to ensure the success of the installation.</p>
<h3 id="heading-how-to-install-docker-on-windows">How to Install Docker on Windows</h3>
<p>On Windows, the procedure is almost the same, except there are a few extra steps that you’ll need to go through. The installation steps are as follows:</p>
<ol>
<li>Navigate to <a target="_blank" href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">this site</a> and follow the instructions for installing WSL2 on Windows 10.</li>
<li>Then navigate to the official <a target="_blank" href="https://www.docker.com/products/docker-desktop">download page</a> and click the <em>Download for Windows (stable)</em> button.</li>
<li>Double-click the downloaded installer and go through the installation with the defaults.</li>
</ol>
<p>Once the installation is done, start <em>Docker Desktop</em> either from the start menu or your desktop. The docker icon should show up on your taskbar.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-taskbar.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now, open up Ubuntu or whatever distribution you've installed from Microsoft Store. Execute the <code>docker --version</code> and <code>docker-compose --version</code> commands to make sure that the installation was successful.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-windows.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You can access Docker from your regular Command Prompt or PowerShell as well. It's just that I prefer using WSL2 over any other command line on Windows.</p>
<h3 id="heading-how-to-install-docker-on-linux">How to Install Docker on Linux</h3>
<p>Installing Docker on Linux is a bit of a different process, and depending on the distribution you’re on, it may vary even more. But to be honest, the installation is just as easy (if not easier) as the other two platforms.</p>
<p>The Docker Desktop package on Windows or Mac is a collection of tools like <code>Docker Engine</code>, <code>Docker Compose</code>, <code>Docker Dashboard</code>, <code>Kubernetes</code> and a few other goodies. </p>
<p>On Linux however, you don’t get such a bundle. Instead you install all the necessary tools you need manually. Installation procedures for different distributions are as follows:</p>
<ul>
<li>If you’re on Ubuntu, you may follow the <a target="_blank" href="https://docs.docker.com/engine/install/ubuntu/">Install Docker Engine on Ubuntu</a> section from the official docs.</li>
<li>For other distributions, <em>installation per distro</em> guides are available on the official docs.<ul>
<li><a target="_blank" href="https://docs.docker.com/engine/install/debian/">Install Docker Engine on Debian</a></li>
<li><a target="_blank" href="https://docs.docker.com/engine/install/fedora/">Install Docker Engine on Fedora</a></li>
<li><a target="_blank" href="https://docs.docker.com/engine/install/centos/">Install Docker Engine on CentOS</a></li>
</ul>
</li>
<li>If you’re on a distribution that is not listed in the docs, you may follow the <a target="_blank" href="https://docs.docker.com/engine/install/binaries/">Install Docker Engine from binaries</a> guide instead.</li>
<li>Regardless of the procedure you follow, you’ll have to go through some <a target="_blank" href="https://docs.docker.com/engine/install/linux-postinstall/">Post-installation steps for Linux</a> which are very important.</li>
<li>Once you’re done with the docker installation, you’ll have to install another tool named Docker Compose. You may follow the <a target="_blank" href="https://docs.docker.com/compose/install/">Install Docker Compose</a> guide from the official docs. </li>
</ul>
<p>Once the installation is done, open up the terminal and execute <code>docker --version</code> and <code>docker-compose --version</code> to ensure the success of the installation.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-linux.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Although Docker performs quite well regardless of the platform you’re on, I prefer Linux over the others. Throughout the book, I’ll be switching between my <a target="_blank" href="https://releases.ubuntu.com/20.10/">Ubuntu 20.10</a> and <a target="_blank" href="https://fedoramagazine.org/announcing-fedora-33/">Fedora 33</a> workstations.</p>
<p>Another thing that I would like to clarify right from the get go, is that I won't be using any GUI tool for working with Docker throughout the entire book.</p>
<p>I'm aware of the nice GUI tools available for different platforms, but learning the common docker commands is one of the primary goals of this book.</p>
<h2 id="heading-hello-world-in-docker-intro-to-docker-basics">Hello World in Docker – Intro to Docker Basics</h2>
<p>Now that you have Docker up and running on your machine, it's time for you to run your first container. Open up the terminal and run the following command:</p>
<pre><code>docker run hello-world

# Unable to find image <span class="hljs-string">'hello-world:latest'</span> locally
# latest: Pulling <span class="hljs-keyword">from</span> library/hello-world
# <span class="hljs-number">0e03</span>bdcc26d7: Pull complete 
# Digest: sha256:<span class="hljs-number">4</span>cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image <span class="hljs-keyword">for</span> hello-world:latest
# 
# Hello <span class="hljs-keyword">from</span> Docker!
# This message shows that your installation appears to be working correctly.
# 
# To generate <span class="hljs-built_in">this</span> message, Docker took the following steps:
#  <span class="hljs-number">1.</span> The Docker client contacted the Docker daemon.
#  <span class="hljs-number">2.</span> The Docker daemon pulled the <span class="hljs-string">"hello-world"</span> image <span class="hljs-keyword">from</span> the Docker Hub.
#     (amd64)
#  <span class="hljs-number">3.</span> The Docker daemon created a <span class="hljs-keyword">new</span> container <span class="hljs-keyword">from</span> that image which runs the
#     executable that produces the output you are currently reading.
#  <span class="hljs-number">4.</span> The Docker daemon streamed that output to the Docker client, which sent it
#     to your terminal.
#
# To <span class="hljs-keyword">try</span> something more ambitious, you can run an Ubuntu container <span class="hljs-keyword">with</span>:
#  $ docker run -it ubuntu bash
# 
# Share images, automate workflows, and more <span class="hljs-keyword">with</span> a free Docker ID:
#  https:<span class="hljs-comment">//hub.docker.com/</span>
#
# For more examples and ideas, <span class="hljs-attr">visit</span>:
#  https:<span class="hljs-comment">//docs.docker.com/get-started/</span>
</code></pre><p>The <a target="_blank" href="https://hub.docker.com/_/hello-world">hello-world</a> image is an example of minimal containerization with Docker. It has a single program compiled from a <a target="_blank" href="https://github.com/docker-library/hello-world/blob/master/hello.c">hello.c</a> file responsible for printing out the message you're seeing on your terminal.</p>
<p>Now in your terminal, you can use the <code>docker ps -a</code> command to have a look at all the containers that are currently running or have run in the past:</p>
<pre><code>docker ps -a

# CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
# <span class="hljs-number">128</span>ec8ceab71        hello-world         <span class="hljs-string">"/hello"</span>            <span class="hljs-number">14</span> seconds ago      Exited (<span class="hljs-number">0</span>) <span class="hljs-number">13</span> seconds ago                      exciting_chebyshev
</code></pre><p>In the output, a container named <code>exciting_chebyshev</code> was run with the container id of <code>128ec8ceab71</code> using the <code>hello-world</code> image. It has <code>Exited (0) 13 seconds ago</code> where the <code>(0)</code> exit code means no error was produced during the runtime of the container.</p>
<p>Now in order to understand what just happened behind the scenes, you'll have to get familiar with the Docker Architecture and three very fundamental concepts of containerization in general, which are as follows:</p>
<ul>
<li>Container</li>
<li>Image</li>
<li>Registry</li>
</ul>
<p>I've listed the three concepts in alphabetical order and will begin my explanations with the first one on the list.</p>
<h3 id="heading-what-is-a-container">What is a Container?</h3>
<p>In the world of containerization, there can not be anything more fundamental than the concept of a container.</p>
<p>The official Docker <a target="_blank" href="https://www.docker.com/resources/what-container">resources</a> site says - </p>
<blockquote>
<p>A container is an abstraction at the application layer that packages code and dependencies together. Instead of virtualizing the entire physical machine, containers virtualize the host operating system only.</p>
</blockquote>
<p>You may consider containers to be the next generation of virtual machines. </p>
<p>Just like virtual machines, containers are completely isolated environments from the host system as well as from each other. They are also a lot lighter than the traditional virtual machine, so a large number of containers can be run simultaneously without affecting the performance of the host system.‌</p>
<p>Containers and virtual machines are actually different ways of virtualizing your physical hardware. The main difference between these two is the method of virtualization.</p>
<p>Virtual machines are usually created and managed by a program known as a hypervisor, like <a target="_blank" href="https://www.virtualbox.org/">Oracle VM VirtualBox</a>, <a target="_blank" href="https://www.vmware.com/">VMware Workstation</a>, <a target="_blank" href="https://www.linux-kvm.org/">KVM</a>, <a target="_blank" href="https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/about/">Microsoft Hyper-V</a> and so on. This hypervisor program usually sits between the host operating system and the virtual machines to act as a medium of communication.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/virtual-machines.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Each virtual machine comes with its own guest operating system which is just as heavy as the host operating system. </p>
<p>The application running inside a virtual machine communicates with the guest operating system, which talks to the hypervisor, which then in turn talks to the host operating system to allocate necessary resources from the physical infrastructure to the running application.</p>
<p>As you can see, there is a long chain of communication between applications running inside virtual machines and the physical infrastructure. The application running inside the virtual machine may take only a small amount of resources, but the guest operating system adds a noticeable overhead.</p>
<p>Unlike a virtual machine, a container does the job of virtualization in a smarter way. Instead of having a complete guest operating system inside a container, it just utilizes the host operating system via the container runtime while maintaining isolation – just like a traditional virtual machine.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/containers.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The container runtime, that is Docker, sits between the containers and the host operating system instead of a hypervisor. The containers then communicate with the container runtime which then communicates with the host operating system to get necessary resources from the physical infrastructure. </p>
<p>As a result of eliminating the entire guest operating system layer, containers are much lighter and less resource-hogging than traditional virtual machines.</p>
<p>As a demonstration of the point, look at the following code block:</p>
<pre><code>uname -a
# Linux alpha-centauri <span class="hljs-number">5.8</span><span class="hljs-number">.0</span><span class="hljs-number">-22</span>-generic #<span class="hljs-number">23</span>-Ubuntu SMP Fri Oct <span class="hljs-number">9</span> <span class="hljs-number">00</span>:<span class="hljs-number">34</span>:<span class="hljs-number">40</span> UTC <span class="hljs-number">2020</span> x86_64 x86_64 x86_64 GNU/Linux

docker run alpine uname -a
# Linux f08dbbe9199b <span class="hljs-number">5.8</span><span class="hljs-number">.0</span><span class="hljs-number">-22</span>-generic #<span class="hljs-number">23</span>-Ubuntu SMP Fri Oct <span class="hljs-number">9</span> <span class="hljs-number">00</span>:<span class="hljs-number">34</span>:<span class="hljs-number">40</span> UTC <span class="hljs-number">2020</span> x86_64 Linux
</code></pre><p>In the code block above, I have executed the <code>uname -a</code> command on my host operating system to print out the kernel details. Then on the next line I've executed the same command inside a container running <a target="_blank" href="https://alpinelinux.org/">Alpine Linux</a>. </p>
<p>As you can see in the output, the container is indeed using the kernel from my host operating system. This goes to prove the point that containers virtualize the host operating system instead of having an operating system of their own.</p>
<p>If you're on a Windows machine, you'll find out that all the containers use the WSL2 kernel. It happens because WSL2 acts as the back-end for Docker on Windows. On macOS the default back-end is a VM running on <a target="_blank" href="https://github.com/moby/hyperkit">HyperKit</a> hypervisor.</p>
<h3 id="heading-what-is-a-docker-image">What is a Docker Image?</h3>
<p>Images are multi-layered self-contained files that act as the template for creating containers. They are like a frozen, read-only copy of a container. Images can be exchanged through registries.</p>
<p>In the past, different container engines had different image formats. But later on, the <a target="_blank" href="https://opencontainers.org/">Open Container Initiative (OCI)</a> defined a standard specification for container images which is complied by the major containerization engines out there. This means that an image built with Docker can be used with another runtime like Podman without any additional hassle.</p>
<p>Containers are just images in running state. When you obtain an image from the internet and run a container using that image, you essentially create another temporary writable layer on top of the previous read-only ones. </p>
<p>This concept will become a lot clearer in upcoming sections of this book. But for now, just keep in mind that images are multi-layered read-only files carrying your application in a desired state inside them.</p>
<h3 id="heading-what-is-a-docker-registry">What is a Docker Registry?</h3>
<p>You've already learned about two very important pieces of the puzzle, <em>Containers</em> and <em>Images</em>. The final piece is the <em>Registry</em>. </p>
<p>An image registry is a centralized place where you can upload your images and can also download images created by others. <a target="_blank" href="https://hub.docker.com/">Docker Hub</a> is the default public registry for Docker. Another very popular image registry is <a target="_blank" href="https://quay.io/">Quay</a> by Red Hat. </p>
<p>Throughout this book I'll be using Docker Hub as my registry of choice.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-hub.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You can share any number of public images on Docker Hub for free. People around the world will be able to download them and use them freely. Images that I've uploaded are available on my profile (<a target="_blank" href="https://hub.docker.com/u/fhsinchy">fhsinchy</a>) page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/my-images-on-docker-hub.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Apart from Docker Hub or Quay, you can also create your own image registry for hosting private images. There is also a local registry that runs within your computer that caches images pulled from remote registries.</p>
<h3 id="heading-docker-architecture-overview">Docker Architecture Overview</h3>
<p>Now that you've become familiar with most of the fundamental concepts regarding containerization and Docker, it's time for you to understand how Docker as a software was designed.</p>
<p>The engine consists of three major components:</p>
<ol>
<li><strong>Docker Daemon:</strong> The daemon (<code>dockerd</code>) is a process that keeps running in the background and waits for commands from the client. The daemon is capable of managing various Docker objects.</li>
<li><strong>Docker Client:</strong> The client  (<code>docker</code>) is a command-line interface program mostly responsible for transporting commands issued by users.</li>
<li><strong>REST API:</strong> The REST API acts as a bridge between the daemon and the client. Any command issued using the client passes through the API to finally reach the daemon.</li>
</ol>
<p>According to the official <a target="_blank" href="https://docs.docker.com/get-started/overview/#docker-architecture">docs</a>, </p>
<blockquote>
<p>"Docker uses a client-server architecture. The Docker <em>client</em> talks to the Docker <em>daemon</em>, which does the heavy lifting of building, running, and distributing your Docker containers".</p>
</blockquote>
<p>You as a user will usually execute commands using the client component. The client then use the REST API to reach out to the long running daemon and get your work done.</p>
<h3 id="heading-the-full-picture">The Full Picture</h3>
<p>Okay, enough talking. Now it's time for you to understand how all these pieces of the puzzle you just learned about work in harmony. Before I dive into the explanation of what really happens when you run the <code>docker run hello-world</code> command, let me show you a little diagram I've made:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-run-hello-world.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>This image is a slightly modified version of the one found in the official <a target="_blank" href="https://docs.docker.com/engine/images/architecture.svg">docs</a>. The events that occur when you execute the command are as follows:</p>
<ol>
<li>You execute <code>docker run hello-world</code> command where <code>hello-world</code> is the name of an image.</li>
<li>Docker client reaches out to the daemon, tells it to get the <code>hello-world</code> image and run a container from that.</li>
<li>Docker daemon looks for the image within your local repository and realizes that it's not there, resulting in the <code>Unable to find image 'hello-world:latest' locally</code> that's printed on your terminal.</li>
<li>The daemon then reaches out to the default public registry which is Docker Hub and pulls in the latest copy of the <code>hello-world</code> image, indicated by the <code>latest: Pulling from library/hello-world</code> line in your terminal.</li>
<li>Docker daemon then creates a new container from the freshly pulled image.</li>
<li>Finally Docker daemon runs the container created using the <code>hello-world</code> image outputting the wall of text on your terminal.</li>
</ol>
<p>It's the default behavior of Docker daemon to look for images in the hub that are not present locally. But once an image has been fetched, it'll stay in the local cache. So if you execute the command again, you won't see the following lines in the output:</p>
<pre><code>Unable to find image <span class="hljs-string">'hello-world:latest'</span> locally
<span class="hljs-attr">latest</span>: Pulling <span class="hljs-keyword">from</span> library/hello-world
<span class="hljs-number">0e03</span>bdcc26d7: Pull complete
<span class="hljs-attr">Digest</span>: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
<span class="hljs-attr">Status</span>: Downloaded newer image <span class="hljs-keyword">for</span> hello-world:latest
</code></pre><p>If there is a newer version of the image available on the public registry, the daemon will fetch the image again. That <code>:latest</code> is a tag. Images usually have meaningful tags to indicate versions or builds. You'll learn about this in greater detail later on.</p>
<h2 id="heading-docker-container-manipulation-basics">Docker Container Manipulation Basics</h2>
<p>In the previous sections, you've learned about the building blocks of Docker and have also run a container using the <code>docker run</code> command. </p>
<p>In this section, you'll be learning about container manipulation in a lot more detail. Container manipulation is one of the most common task you'll be performing every single day, so having a proper understanding of the various commands is crucial.</p>
<p>Keep in mind, though, that this is not an exhaustive list of all the commands you can execute on Docker. I'll be talking only about the most common ones. Anytime you want to learn more about the available commands, just visit the official <a target="_blank" href="https://docs.docker.com/engine/reference/commandline/container/">reference</a> for the Docker command-line.</p>
<h3 id="heading-how-to-run-a-container">How to Run a Container</h3>
<p>Previously you've used <code>docker run</code> to create and start a container using the <code>hello-world</code> image. The generic syntax for this command is as follows:</p>
<pre><code>docker run &lt;image name&gt;
</code></pre><p>Although this is a perfectly valid command, there is a better way of dispatching commands to the <code>docker</code> daemon. </p>
<p>Prior to version <code>1.13</code>, Docker had only the previously mentioned command syntax. Later on, the command-line was <a target="_blank" href="https://www.docker.com/blog/whats-new-in-docker-1-13/">restructured</a> to have the following syntax:</p>
<pre><code>docker &lt;object&gt; &lt;command&gt; &lt;options&gt;
</code></pre><p>In this syntax:</p>
<ul>
<li><code>object</code> indicates the type of Docker object you'll be manipulating. This can be a <code>container</code>, <code>image</code>, <code>network</code> or <code>volume</code> object.</li>
<li><code>command</code> indicates the task to be carried out by the daemon, that is the <code>run</code> command.</li>
<li><code>options</code> can be any valid parameter that can override the default behavior of the command, like the <code>--publish</code> option for port mapping.</li>
</ul>
<p>Now, following this syntax, the <code>run</code> command can be written as follows:</p>
<pre><code>docker container run &lt;image name&gt;
</code></pre><p>The <code>image name</code> can be of any image from an online registry or your local system. As an example, you can try to run a container using the <a target="_blank" href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> image. This image contains a simple <a target="_blank" href="https://vuejs.org/">Vue.js</a> application that runs on port 80 inside the container. </p>
<p>To run a container using this image, execute following command on your terminal:</p>
<pre><code>docker container run --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> fhsinchy/hello-dock

# /docker-entrypoint.sh: <span class="hljs-regexp">/docker-entrypoint.d/</span> is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking <span class="hljs-keyword">for</span> shell scripts <span class="hljs-keyword">in</span> /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/<span class="hljs-number">10</span>-listen-on-ipv6-by-<span class="hljs-keyword">default</span>.sh
# <span class="hljs-number">10</span>-listen-on-ipv6-by-<span class="hljs-keyword">default</span>.sh: Getting the checksum <span class="hljs-keyword">of</span> /etc/nginx/conf.d/<span class="hljs-keyword">default</span>.conf
# <span class="hljs-number">10</span>-listen-on-ipv6-by-<span class="hljs-keyword">default</span>.sh: Enabled listen on IPv6 <span class="hljs-keyword">in</span> /etc/nginx/conf.d/<span class="hljs-keyword">default</span>.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/<span class="hljs-number">20</span>-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready <span class="hljs-keyword">for</span> start up
</code></pre><p>The command is pretty self-explanatory. The only portion that may require some explanation is the <code>--publish 8080:80</code> portion which will be explained in the next sub-section.</p>
<h3 id="heading-how-to-publish-a-port">How to Publish a Port</h3>
<p>Containers are isolated environments. Your host system doesn't know anything about what's going on inside a container. Hence, applications running inside a container remain inaccessible from the outside.</p>
<p>To allow access from outside of a container, you must publish the appropriate port inside the container to a port on your local network. The common syntax for the <code>--publish</code> or <code>-p</code> option is as follows:</p>
<pre><code>--publish &lt;host port&gt;:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">container</span> <span class="hljs-attr">port</span>&gt;</span></span>
</code></pre><p>When you wrote <code>--publish 8080:80</code> in the previous sub-section, it meant any request sent to port 8080 of your host system will be forwarded to port 80 inside the container‌.</p>
<p>Now to access the application on your browser, visit <code>http://127.0.0.1:8080</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>You can stop the container by simply hitting the <code>ctrl + c</code> key combination while the terminal window is in focus or closing off the terminal window completely.</p>
<h3 id="heading-how-to-use-detached-mode">How to Use Detached Mode</h3>
<p>Another very popular option of the <code>run</code> command is the <code>--detach</code> or <code>-d</code> option. In the example above, in order for the container to keep running, you had to keep the terminal window open. Closing the terminal window also stopped the running container.</p>
<p>This is because, by default, containers run in the foreground and attach themselves to the terminal like any other normal program invoked from the terminal. </p>
<p>In order to override this behavior and keep a container running in background, you can include the <code>--detach</code> option with the <code>run</code> command as follows:</p>
<pre><code>docker container run --detach --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> fhsinchy/hello-dock

# <span class="hljs-number">9</span>f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
</code></pre><p>Unlike the previous example, you won't get a wall of text thrown at you this time. Instead what you'll get is the ID of the newly created container.</p>
<p>The order of the options you provide doesn't really matter. If you put the <code>--publish</code> option before the <code>--detach</code> option, it'll work just the same. One thing that you have to keep in mind in case of the <code>run</code> command is that the image name must come last. If you put anything after the image name then that'll be passed as an argument to the container entry-point (explained in the <a class="post-section-overview" href="#executing-commands-inside-a-container">Executing Commands Inside a Container</a> sub-section) and may result in unexpected situations.</p>
<h3 id="heading-how-to-list-containers">How to List Containers</h3>
<p>The <code>container ls</code> command can be used to list out containers that are currently running. To do so execute following command:</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">5</span> seconds ago       Up <span class="hljs-number">5</span> seconds        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   gifted_sammet
</code></pre><p>A container named <code>gifted_sammet</code> is running. It was created <code>5 seconds ago</code> and the status is <code>Up 5 seconds,</code> which indicates that the container has been running fine since its creation.</p>
<p>The <code>CONTAINER ID</code> is <code>9f21cb777058</code> which is the first 12 characters of the full container ID. The full container ID is <code>9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc</code> which is 64 characters long. This full container ID was printed as the output of the <code>docker container run</code> command in the previous section.</p>
<p>Listed under the <code>PORTS</code> column, port 8080 from your local network is pointing towards port 80 inside the container. The name <code>gifted_sammet</code> is generated by Docker and can be something completely different in your computer.</p>
<p>The <code>container ls</code> command only lists the containers that are currently running on your system. In order to list out the containers that have run in the past you can use the <code>--all</code> or <code>-a</code> option.</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                     PORTS                  NAMES
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">2</span> minutes ago       Up <span class="hljs-number">2</span> minutes               <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   gifted_sammet
# <span class="hljs-number">6</span>cf52771dde1        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">3</span> minutes ago       Exited (<span class="hljs-number">0</span>) <span class="hljs-number">3</span> minutes ago                          reverent_torvalds
# <span class="hljs-number">128</span>ec8ceab71        hello-world           <span class="hljs-string">"/hello"</span>                 <span class="hljs-number">4</span> minutes ago       Exited (<span class="hljs-number">0</span>) <span class="hljs-number">4</span> minutes ago                          exciting_chebyshev
</code></pre><p>As you can see, the second container in the list <code>reverent_torvalds</code> was created earlier and has exited with the status code 0, which indicates that no error was produced during the runtime of the container.</p>
<h3 id="heading-how-to-name-or-rename-a-container">How to Name or Rename a Container</h3>
<p>By default, every container has two identifiers. They are as follows:</p>
<ul>
<li><code>CONTAINER ID</code> - a random 64 character-long string.</li>
<li><code>NAME</code> - combination of two random words, joined with an underscore.</li>
</ul>
<p>Referring to a container based on these two random identifiers is kind of inconvenient. It would be great if the containers could be referred to using a name defined by you.</p>
<p>Naming a container can be achieved using the <code>--name</code> option. To run another container using the <code>fhsinchy/hello-dock</code> image with the name <code>hello-dock-container</code> you can execute the following command:</p>
<pre><code>docker container run --detach --publish <span class="hljs-number">8888</span>:<span class="hljs-number">80</span> --name hello-dock-container fhsinchy/hello-dock

# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb
</code></pre><p>The 8080 port on local network is occupied by the <code>gifted_sammet</code> container (the container created in the previous sub-section). That's why you'll have to use a different port number, like 8888. Now to verify, run the <code>container ls</code> command:</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">28</span> seconds ago      Up <span class="hljs-number">26</span> seconds       <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8888</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-container
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">4</span> minutes ago       Up <span class="hljs-number">4</span> minutes        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   gifted_sammet
</code></pre><p>A new container with the name of <code>hello-dock-container</code> has been started.</p>
<p>You can even rename old containers using the <code>container rename</code> command. Syntax for the command is as follows:</p>
<pre><code>docker container rename &lt;container identifier&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">new</span> <span class="hljs-attr">name</span>&gt;</span></span>
</code></pre><p>To rename the <code>gifted_sammet</code> container to <code>hello-dock-container-2</code>, execute following command:</p>
<pre><code>docker container rename gifted_sammet hello-dock-container<span class="hljs-number">-2</span>
</code></pre><p>The command doesn't yield any output but you can verify that the changes have taken place using the <code>container ls</code> command. The <code>rename</code> command works for containers both in running state and stopped state.</p>
<h3 id="heading-how-to-stop-or-kill-a-running-container">How to Stop or Kill a Running Container</h3>
<p>Containers running in the foreground can be stopped by simply closing the terminal window or hitting <code>ctrl + c</code>. Containers running in the background, however, can not be stopped in the same way.</p>
<p>There are two commands that deal with this task. The first one is the <code>container stop</code> command. Generic syntax for the command is as follows:</p>
<pre><code>docker container stop &lt;container identifier&gt;
</code></pre><p>Where <code>container identifier</code> can either be the id or the name of the container. </p>
<p>I hope that you remember the container you started in the previous section. It's still running in the background. Get the identifier for that container using <code>docker container ls</code> (I'll be using <code>hello-dock-container</code> container for this demo). Now execute the following command to stop the container:</p>
<pre><code>docker container stop hello-dock-container

# hello-dock-container
</code></pre><p>If you use the name as identifier, you'll get the name thrown back to you as output. The <code>stop</code> command shuts down a container gracefully by sending a <code>SIGTERM</code> signal. If the container doesn't stop within a certain period, a <code>SIGKILL</code> signal is sent which shuts down the container immediately.</p>
<p>In cases where you want to send a <code>SIGKILL</code> signal instead of a <code>SIGTERM</code> signal, you may use the <code>container kill</code> command instead. The <code>container kill</code> command follows the same syntax as the <code>stop</code> command.</p>
<pre><code>docker container kill hello-dock-container<span class="hljs-number">-2</span>

# hello-dock-container<span class="hljs-number">-2</span>
</code></pre><h3 id="heading-how-to-restart-a-container">How to Restart a Container</h3>
<p>When I say restart I mean two scenarios specifically. They are as follows:</p>
<ul>
<li>Restarting a container that has been previously stopped or killed.</li>
<li>Rebooting a running container.</li>
</ul>
<p>As you've already learned from a previous sub-section, stopped containers remain in your system. If you want you can restart them. The <code>container start</code> command can be used to start any stopped or killed container. The syntax of the command is as follows:</p>
<pre><code>docker container start &lt;container identifier&gt;
</code></pre><p>You can get the list of all containers by executing the <code>container ls --all</code> command. Then look for the containers with <code>Exited</code> status.</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                        PORTS               NAMES
# b1db06e400c4        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">3</span> minutes ago       Exited (<span class="hljs-number">0</span>) <span class="hljs-number">47</span> seconds ago                         hello-dock-container
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">7</span> minutes ago       Exited (<span class="hljs-number">137</span>) <span class="hljs-number">17</span> seconds ago                       hello-dock-container<span class="hljs-number">-2</span>
# <span class="hljs-number">6</span>cf52771dde1        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">7</span> minutes ago       Exited (<span class="hljs-number">0</span>) <span class="hljs-number">7</span> minutes ago                          reverent_torvalds
# <span class="hljs-number">128</span>ec8ceab71        hello-world           <span class="hljs-string">"/hello"</span>                 <span class="hljs-number">9</span> minutes ago       Exited (<span class="hljs-number">0</span>) <span class="hljs-number">9</span> minutes ago                          exciting_chebyshev
</code></pre><p>Now to restart the <code>hello-dock-container</code> container, you may execute the following command:</p>
<pre><code>docker container start hello-dock-container

# hello-dock-container
</code></pre><p>Now you can ensure that the container is running by looking at the list of running containers using the <code>container ls</code> command. </p>
<p>The <code>container start</code> command starts any container in detached mode by default and retains any port configurations made previously. So if you visit <code>http://127.0.0.1:8080</code> now, you should be able to access the <code>hello-dock</code> application just like before.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now, in scenarios where you would like to reboot a running container you may use the <code>container restart</code> command. The <code>container restart</code> command follows the exact syntax as the <code>container start</code> command.</p>
<pre><code>docker container restart hello-dock-container<span class="hljs-number">-2</span>

# hello-dock-container<span class="hljs-number">-2</span>
</code></pre><p>The main difference between the two commands is that the <code>container restart</code> command attempts to stop the target container and then starts it back up again, whereas the start command just starts an already stopped container.</p>
<p>In case of a stopped container, both commands are exactly the same. But in case of a running container, you must use the <code>container restart</code> command.</p>
<h3 id="heading-how-to-create-a-container-without-running">How to Create a Container Without Running</h3>
<p>So far in this section, you've started containers using the <code>container run</code> command which is in reality a combination of two separate commands. These commands are as follows:</p>
<ul>
<li><code>container create</code> command creates a container from a given image.</li>
<li><code>container start</code> command starts a container that has been already created.</li>
</ul>
<p>Now, to perform the demonstration shown in the "How to Run a Container" section using these two commands, you can do something like the following:</p>
<pre><code>docker container create --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> fhsinchy/hello-dock

# <span class="hljs-number">2e7</span>ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
# <span class="hljs-number">2e7</span>ef5098bab        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">30</span> seconds ago      Created                                 hello-dock
</code></pre><p>Evident by the output of the <code>container ls --all</code> command, a container with the name of <code>hello-dock</code> has been created using the <code>fhsinchy/hello-dock</code> image. The <code>STATUS</code> of the container is <code>Created</code> at the moment, and, given that it's not running, it won't be listed without the use of the <code>--all</code> option. </p>
<p>Once the container has been created, it can be started using the <code>container start</code> command.</p>
<pre><code>docker container start hello-dock

# hello-dock

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# <span class="hljs-number">2e7</span>ef5098bab        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   About a minute ago   Up <span class="hljs-number">29</span> seconds       <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock
</code></pre><p>The container <code>STATUS</code> has changed from <code>Created</code> to <code>Up 29 seconds</code> which indicates that the container is now in running state. The port configuration has also shown up in the <code>PORTS</code> column which was previously empty.‌</p>
<p>Although you can get away with the <code>container run</code> command for the majority of the scenarios, there will be some situations later on in the book that require you to use this <code>container create</code> command.</p>
<h3 id="heading-how-to-remove-dangling-containers">How to Remove Dangling Containers</h3>
<p>As you've already seen, containers that have been stopped or killed remain in the system. These dangling containers can take up space or can conflict with newer containers.</p>
<p>In order to remove a stopped container you can use the <code>container rm</code> command. The generic syntax is as follows:</p>
<pre><code>docker container rm &lt;container identifier&gt;
</code></pre><p>To find out which containers are not running, use the <code>container ls --all</code> command and look for containers with <code>Exited</code> status.</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                      PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">6</span> minutes ago       Up About a minute           <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8888</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-container
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">10</span> minutes ago      Up About a minute           <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-container<span class="hljs-number">-2</span>
# <span class="hljs-number">6</span>cf52771dde1        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">10</span> minutes ago      Exited (<span class="hljs-number">0</span>) <span class="hljs-number">10</span> minutes ago                          reverent_torvalds
# <span class="hljs-number">128</span>ec8ceab71        hello-world           <span class="hljs-string">"/hello"</span>                 <span class="hljs-number">12</span> minutes ago      Exited (<span class="hljs-number">0</span>) <span class="hljs-number">12</span> minutes ago                          exciting_chebyshev
</code></pre><p>As can be seen in the output, the containers with ID <code>6cf52771dde1</code> and <code>128ec8ceab71</code> are not running. To remove the <code>6cf52771dde1</code> you can execute the following command:</p>
<pre><code>docker container rm <span class="hljs-number">6</span>cf52771dde1

# <span class="hljs-number">6</span>cf52771dde1
</code></pre><p>You can check if the container was deleted or not by using the <code>container ls</code> command. You can also remove multiple containers at once by passing their identifiers one after another separated by spaces.</p>
<p>Or, instead of removing individual containers, if you want to remove all dangling containers at one go, you can use the <code>container prune</code> command.</p>
<p>You can check the container list using the <code>container ls --all</code> command to make sure that the dangling containers have been removed:</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">8</span> minutes ago       Up <span class="hljs-number">3</span> minutes        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8888</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-container
# <span class="hljs-number">9</span>f21cb777058        fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">12</span> minutes ago      Up <span class="hljs-number">3</span> minutes        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-container<span class="hljs-number">-2</span>
</code></pre><p>If you are following the book exactly as written so far, you should only see the <code>hello-dock-container</code> and <code>hello-dock-container-2</code> in the list. I would suggest stopping and removing both containers before going on to the next section.</p>
<p>There is also the <code>--rm</code> option for the <code>container run</code>  and <code>container start</code> commands which indicates that you want the containers removed as soon as they're stopped. To start another <code>hello-dock</code> container with the <code>--rm</code> option, execute the following command:</p>
<pre><code>docker container run --rm --detach --publish <span class="hljs-number">8888</span>:<span class="hljs-number">80</span> --name hello-dock-volatile fhsinchy/hello-dock

# <span class="hljs-number">0</span>d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3
</code></pre><p>You can use the <code>container ls</code> command to verify that the container is running:</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# <span class="hljs-number">0</span>d74e14091dc   fhsinchy/hello-dock   <span class="hljs-string">"/docker-entrypoint.…"</span>   About a minute ago   Up About a minute   <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8888</span>-&gt;<span class="hljs-number">80</span>/tcp   hello-dock-volatile
</code></pre><p>Now if you stop the container and then check again with the <code>container ls --all</code> command:</p>
<pre><code>docker container stop hello-dock-volatile

# hello-dock-volatile

docker container ls --all

# CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
</code></pre><p>The container has been removed automatically. From now on I'll use the <code>--rm</code> option for most of the containers. I'll explicitly mention where it's not needed.</p>
<h3 id="heading-how-to-run-a-container-in-interactive-mode">How to Run a Container in Interactive Mode</h3>
<p>So far you've only run containers created from either the <a target="_blank" href="https://hub.docker.com/_/hello-world">hello-world</a> image or the <a target="_blank" href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> image. These images are made for executing simple programs that are not interactive.</p>
<p>Well, all images are not that simple. Images can encapsulate an entire Linux distribution inside them. </p>
<p>Popular distributions such as <a target="_blank" href="https://ubuntu.com/">Ubuntu</a>, <a target="_blank" href="https://fedora.org/">Fedora</a>, and <a target="_blank" href="https://debian.org/">Debian</a> all have official Docker images available in the hub. Programming languages such as <a target="_blank" href="https://hub.docker.com/_/python">python</a>, <a target="_blank" href="https://hub.docker.com/_/php">php</a>, <a target="_blank" href="https://hub.docker.com/_/golang">go</a> or run-times like <a target="_blank" href="https://hub.docker.com/_/node">node</a> and <a target="_blank" href="https://hub.docker.com/r/hayd/deno">deno</a> all have their official images.</p>
<p>These images do not just run some pre-configured program. These are instead configured to run a shell by default. In case of the operating system images it can be something like <code>sh</code> or <code>bash</code> and in case of the programming languages or run-times, it is usually their default language shell.</p>
<p>As you may have already learned from your previous experiences with computers, shells are interactive programs. An image configured to run such a program is an interactive image. These images require a special <code>-it</code> option to be passed in the <code>container run</code> command.</p>
<p>As an example, if you run a container using the <code>ubuntu</code> image by executing <code>docker container run ubuntu</code> you'll see nothing happens. But if you execute the same command with the <code>-it</code> option, you should land directly on bash inside the Ubuntu container.</p>
<pre><code>docker container run --rm -it ubuntu

# root@dbb1f56b9563:<span class="hljs-regexp">/# cat /</span>etc/os-release
# NAME=<span class="hljs-string">"Ubuntu"</span>
# VERSION=<span class="hljs-string">"20.04.1 LTS (Focal Fossa)"</span>
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME=<span class="hljs-string">"Ubuntu 20.04.1 LTS"</span>
# VERSION_ID=<span class="hljs-string">"20.04"</span>
# HOME_URL=<span class="hljs-string">"https://www.ubuntu.com/"</span>
# SUPPORT_URL=<span class="hljs-string">"https://help.ubuntu.com/"</span>
# BUG_REPORT_URL=<span class="hljs-string">"https://bugs.launchpad.net/ubuntu/"</span>
# PRIVACY_POLICY_URL=<span class="hljs-string">"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"</span>
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal
</code></pre><p>As you can see from the output of the <code>cat /etc/os-release</code> command, I am indeed interacting with the bash running inside the Ubuntu container.</p>
<p>The <code>-it</code> option sets the stage for you to interact with any interactive program inside a container. This option is actually two separate options mashed together.</p>
<ul>
<li>The <code>-i</code> or <code>--interactive</code> option connects you to the input stream of the container, so that you can send inputs to bash.</li>
<li>The <code>-t</code> or <code>--tty</code> option makes sure that you get some good formatting and a native terminal-like experience by allocating a pseudo-tty.</li>
</ul>
<p>You need to use the <code>-it</code> option whenever you want to run a container in interactive mode. Another example can be running the <code>node</code> image as follows:</p>
<pre><code>docker container run -it node

# Welcome to Node.js v15<span class="hljs-number">.0</span><span class="hljs-number">.0</span>.
# Type <span class="hljs-string">".help"</span> <span class="hljs-keyword">for</span> more information.
# &gt; [<span class="hljs-string">'farhan'</span>, <span class="hljs-string">'hasin'</span>, <span class="hljs-string">'chowdhury'</span>].map(<span class="hljs-function"><span class="hljs-params">name</span> =&gt;</span> name.toUpperCase())
# [ <span class="hljs-string">'FARHAN'</span>, <span class="hljs-string">'HASIN'</span>, <span class="hljs-string">'CHOWDHURY'</span> ]
</code></pre><p>Any valid JavaScript code can be executed in the node shell. Instead of writing <code>-it</code> you can be more verbose by writing <code>--interactive --tty</code> separately.</p>
<h3 id="heading-how-to-execute-commands-inside-a-container">How to Execute Commands Inside a Container</h3>
<p>In the <a target="_blank" href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker">Hello World in Docker</a> section of this book, you've seen me executing a command inside an Alpine Linux container. It went something like this:</p>
<pre><code>docker run alpine uname -a
# Linux f08dbbe9199b <span class="hljs-number">5.8</span><span class="hljs-number">.0</span><span class="hljs-number">-22</span>-generic #<span class="hljs-number">23</span>-Ubuntu SMP Fri Oct <span class="hljs-number">9</span> <span class="hljs-number">00</span>:<span class="hljs-number">34</span>:<span class="hljs-number">40</span> UTC <span class="hljs-number">2020</span> x86_64 Linux
</code></pre><p>In this command, I've executed the <code>uname -a</code> command inside an Alpine Linux container. Scenarios like this (where all you want to do is to execute a certain command inside a certain container) are pretty common.</p>
<p>Assume that you want encode a string using the <code>base64</code> program. This is something that's available in almost any Linux or Unix based operating system (but not on Windows). </p>
<p>In this situation you can quickly spin up a container using images like <a target="_blank" href="https://hub.docker.com/_/busybox">busybox</a> and let it do the job.</p>
<p>The generic syntax for encoding a string using <code>base64</code> is as follows:</p>
<pre><code>echo -n my-secret | base64

# bXktc2VjcmV0
</code></pre><p>And the generic syntax for passing a command to a container that is not running is as follows:</p>
<pre><code>docker container run &lt;image name&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">command</span>&gt;</span></span>
</code></pre><p>To perform the base64 encoding using the busybox image, you can execute the following command:</p>
<pre><code>docker container run --rm busybox sh -c <span class="hljs-string">"echo -n my-secret | base64

# bXktc2VjcmV0</span>
</code></pre><p>What happens here is that, in a <code>container run</code> command, whatever you pass after the image name gets passed to the default entry point of the image. </p>
<p>An entry point is like a gateway to the image. Most of the images except the executable images (explained in the <a target="_blank" href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/container-manipulation-basics#working-with-executable-images">Working With Executable Images</a> sub-section) use shell or <code>sh</code> as the default entry-point. So any valid shell command can be passed to them as arguments.</p>
<h3 id="heading-how-to-work-with-executable-images">How to Work With Executable Images</h3>
<p>In the previous section, I briefly mentioned executable images. These images are designed to behave like executable programs.</p>
<p>Take for example my <a target="_blank" href="https://github.com/fhsinchy/rmbyext">rmbyext</a> project. This is a simple Python script capable of recursively deleting files of given extensions. To learn more about the project, you can checkout the repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/fhsinchy/rmbyext">https://github.com/fhsinchy/rmbyext</a></div>
<p>If you have both Git and Python installed, you can install this script by executing the following command:</p>
<pre><code>pip install git+https:<span class="hljs-comment">//github.com/fhsinchy/rmbyext.git#egg=rmbyext</span>
</code></pre><p>Assuming Python has been set up properly on your system, the script should be available anywhere through the terminal. The generic syntax for using this script is as follows:</p>
<pre><code>rmbyext &lt;file extension&gt;
</code></pre><p>To test it out, open up your terminal inside an empty directory and create some files in it with different extensions. You can use the <code>touch</code> command to do so. Now, I have a directory on my computer with the following files:</p>
<pre><code>touch a.pdf b.pdf c.txt d.pdf e.txt

ls

# a.pdf  b.pdf  c.txt  d.pdf  e.txt
</code></pre><p>To delete all the <code>pdf</code> files from this directory, you can execute the following command:</p>
<pre><code>rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre><p>An executable image for this program should be able to take extensions of files as arguments and delete them just like the <code>rmbyext</code> program did.</p>
<p>The <a target="_blank" href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> image behaves in a similar manner. This image contains a copy of the <code>rmbyext</code> script and is configured to run the script on a directory <code>/zone</code> inside the container.</p>
<p>Now the problem is that containers are isolated from your local system, so the <code>rmbyext</code> program running inside the container doesn't have any access to your local file system. So, if somehow you can map the local directory containing the <code>pdf</code> files to the <code>/zone</code> directory inside the container, the files should be accessible to the container.</p>
<p>One way to grant a container direct access to your local file system is by using <a target="_blank" href="https://docs.docker.com/storage/bind-mounts/">bind mounts</a>. </p>
<p>A bind mount lets you form a two way data binding between the content of a local file system directory (source) and another directory inside a container (destination). This way any changes made in the destination directory will take effect on the source directory and vise versa.</p>
<p>Let's see a bind mount in action. To delete files using this image instead of the program itself, you can execute the following command:</p>
<pre><code>docker container run --rm -v $(pwd):<span class="hljs-regexp">/zone fhsinchy/</span>rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre><p>As you may have already guessed by seeing the <code>-v $(pwd):/zone</code> part in the command, the  <code>-v</code> or <code>--volume</code> option is used for creating a bind mount for a container. This option can take three fields separated by colons (<code>:</code>). The generic syntax for the option is as follows:</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre><p>The third field is optional but you must pass the absolute path of your local directory and the absolute path of the directory inside the container. </p>
<p>The source directory in my case is <code>/home/fhsinchy/the-zone</code>. Given that my terminal is opened inside the directory, <code>$(pwd)</code> will be replaced with <code>/home/fhsinchy/the-zone</code> which contains the previously mentioned <code>.pdf</code> and <code>.txt</code> files. </p>
<p>You can learn more about <a target="_blank" href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html">command substitution here</a> if you want to.</p>
<p>The <code>--volume</code> or <code>-v</code> option is valid for the <code>container run</code> as well as the <code>container create</code> commands. We'll explore volumes in greater detail in the upcoming sections so don't worry if you didn't understand them very well here.</p>
<p>The difference between a regular image and an executable one is that the entry-point for an executable image is set to a custom program instead of <code>sh</code>, in this case the <code>rmbyext</code> program. And as you've learned in the previous sub-section, anything you write after the image name in a <code>container run</code> command gets passed to the entry-point of the image.</p>
<p>So in the end the <code>docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf</code> command translates to <code>rmbyext pdf</code> inside the container. Executable images are not that common in the wild but can be very useful in certain cases.</p>
<h2 id="heading-docker-image-manipulation-basics">Docker Image Manipulation Basics</h2>
<p>Now that you have a solid understanding of how to run containers using publicly available images, it's time for you to learn about creating your very own images.</p>
<p>In this section, you'll learn the fundamentals of creating images, running containers using them, and sharing them online.</p>
<p>I would suggest you to install <a target="_blank" href="https://code.visualstudio.com/">Visual Studio Code</a> with the official <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker">Docker Extension</a> from the marketplace. This will greatly help your development experience.</p>
<h3 id="heading-how-to-create-a-docker-image">How to Create a Docker Image</h3>
<p>As I've already explained in the <a class="post-section-overview" href="#image">Hello World in Docker</a> section, images are multi-layered self-contained files that act as the template for creating Docker containers. They are like a frozen, read-only copy of a container.</p>
<p>In order to create an image using one of your programs you must have a clear vision of what you want from the image. Take the official <a target="_blank" href="https://hub.docker.com/_/nginx">nginx</a> image, for example. You can start a container using this image simply by executing the following command:</p>
<pre><code>docker container run --rm --detach --name <span class="hljs-keyword">default</span>-nginx --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> nginx

# b379ecd5b6b9ae27c144e4fa12bdc5d0635543666f75c14039eea8d5f38e3f56

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b379ecd5b6b9        nginx               <span class="hljs-string">"/docker-entrypoint.…"</span>   <span class="hljs-number">8</span> seconds ago       Up <span class="hljs-number">8</span> seconds        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   <span class="hljs-keyword">default</span>-nginx
</code></pre><p>Now, if you visit <code>http://127.0.0.1:8080</code> in the browser, you'll see a default response page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>That's all nice and good, but what if you want to make a custom NGINX image which functions exactly like the official one, but that's built by you? That's a completely valid scenario to be honest. In fact, let's do that.‌</p>
<p>In order to make a custom NGINX image, you must have a clear picture of what the final state of the image will be. In my opinion the image should be as follows:</p>
<ul>
<li>The image should have NGINX pre-installed which can be done using a package manager or can be built from source.</li>
<li>The image should start NGINX automatically upon running.</li>
</ul>
<p>That's simple. If you've cloned the project repository linked in this book, go inside the project root and look for a directory named <code>custom-nginx</code> in there. </p>
<p>Now, create a new file named <code>Dockerfile</code> inside that directory. A <code>Dockerfile</code> is a collection of instructions that, once processed by the daemon, results in an image. Content for the <code>Dockerfile</code> is as follows:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> ubuntu:latest

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">80</span>

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install nginx -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<p>Images are multi-layered files and in this file, each line (known as instructions) that you've written creates a layer for your image.</p>
<ul>
<li>Every valid <code>Dockerfile</code> starts with a <code>FROM</code> instruction. This instruction sets the base image for your resultant image. By setting <code>ubuntu:latest</code> as the base image here, you get all the goodness of Ubuntu already available in your custom image, so you can use things like the <code>apt-get</code> command for easy package installation.</li>
<li>The <code>EXPOSE</code> instruction is used to indicate the port that needs to be published. Using this instruction doesn't mean that you won't need to <code>--publish</code> the port. You'll still need to use the <code>--publish</code> option explicitly. This <code>EXPOSE</code> instruction works like a documentation for someone who's trying to run a container using your image. It also has some other uses that I won't be discussing here.</li>
<li>The <code>RUN</code> instruction in a <code>Dockerfile</code> executes a command inside the container shell. The <code>apt-get update &amp;&amp; apt-get install nginx -y</code> command checks for updated package versions and installs NGINX. The <code>apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> command is used for clearing the package cache because you don't want any unnecessary baggage in your image. These two commands are simple Ubuntu stuff, nothing fancy. The <code>RUN</code> instructions here are written in <code>shell</code> form. These can also be written in <code>exec</code> form. You can consult the <a target="_blank" href="https://docs.docker.com/engine/reference/builder/#run">official reference</a> for more information.</li>
<li>Finally the <code>CMD</code> instruction sets the default command for your image. This instruction is written in <code>exec</code> form here comprising of three separate parts. Here, <code>nginx</code> refers to the NGINX executable. The <code>-g</code> and <code>daemon off</code> are options for NGINX. Running NGINX as a single process inside containers is considered a best practice hence the usage of this option. The <code>CMD</code> instruction can also be written in <code>shell</code> form. You can consult the <a target="_blank" href="https://docs.docker.com/engine/reference/builder/#cmd">official reference</a> for more information.</li>
</ul>
<p>Now that you have a valid <code>Dockerfile</code> you can build an image out of it. Just like the container related commands, the image related commands can be issued using the following syntax:</p>
<pre><code>docker image &lt;command&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">options</span>&gt;</span></span>
</code></pre><p>To build an image using the <code>Dockerfile</code> you just wrote, open up your terminal inside the <code>custom-nginx</code> directory and execute the following command:</p>
<pre><code>docker image build .

# Sending build context to Docker daemon  <span class="hljs-number">3.584</span>kB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">4</span> : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step <span class="hljs-number">2</span>/<span class="hljs-number">4</span> : EXPOSE <span class="hljs-number">80</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">9</span>eae86582ec7
# Removing intermediate container <span class="hljs-number">9</span>eae86582ec7
#  ---&gt; <span class="hljs-number">8235</span>bd799a56
# Step <span class="hljs-number">3</span>/<span class="hljs-number">4</span> : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; Running in a44725cbb3fa
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a44725cbb3fa
#  ---&gt; 3066bd20292d
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 4792e4691660
# Removing intermediate container 4792e4691660
#  ---&gt; 3199372aa3fc
# Successfully built 3199372aa3fc</span>
</code></pre><p>To perform an image build, the daemon needs two very specific pieces of information. These are the name of the <code>Dockerfile</code> and the build context. In the command issued above:</p>
<ul>
<li><code>docker image build</code> is the command for building the image. The daemon finds any file named <code>Dockerfile</code> within the context.</li>
<li>The <code>.</code> at the end sets the context for this build. The context means the directory accessible by the daemon during the build process.</li>
</ul>
<p>Now to run a container using this image, you can use the <code>container run</code> command coupled with the image ID that you received as the result of the build process. In my case the id is <code>3199372aa3fc</code> evident by the <code>Successfully built 3199372aa3fc</code> line in the previous code block.</p>
<pre><code>docker container run --rm --detach --name custom-nginx-packaged --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> <span class="hljs-number">3199372</span>aa3fc

# ec09d4e1f70c903c3b954c8d7958421cdd1ae3d079b57f929e44131fbf8069a0

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# ec09d4e1f70c        <span class="hljs-number">3199372</span>aa3fc        <span class="hljs-string">"nginx -g 'daemon of…"</span>   <span class="hljs-number">23</span> seconds ago      Up <span class="hljs-number">22</span> seconds       <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   custom-nginx-packaged
</code></pre><p>To verify, visit <code>http://127.0.0.1:8080</code> and you should see the default response page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-tag-docker-images">How to Tag Docker Images</h3>
<p>Just like containers, you can assign custom identifiers to your images instead of relying on the randomly generated ID. In case of an image, it's called tagging instead of naming. The <code>--tag</code> or <code>-t</code> option is used in such cases. </p>
<p>Generic syntax for the option is as follows:</p>
<pre><code>--tag &lt;image repository&gt;:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">image</span> <span class="hljs-attr">tag</span>&gt;</span></span>
</code></pre><p>The repository is usually known as the image name and the tag indicates a certain build or version. </p>
<p>Take the official <a target="_blank" href="https://hub.docker.com/_/mysql">mysql</a> image, for example. If you want to run a container using a specific version of MySQL, like 5.7, you can execute <code>docker container run mysql:5.7</code> where <code>mysql</code> is the image repository and <code>5.7</code> is the tag.</p>
<p>In order to tag your custom NGINX image with <code>custom-nginx:packaged</code> you can execute the following command:</p>
<pre><code>docker image build --tag custom-nginx:packaged .

# Sending build context to Docker daemon  <span class="hljs-number">1.055</span>MB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">4</span> : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step <span class="hljs-number">2</span>/<span class="hljs-number">4</span> : EXPOSE <span class="hljs-number">80</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">53</span>ab370b9efc
# Removing intermediate container <span class="hljs-number">53</span>ab370b9efc
#  ---&gt; <span class="hljs-number">6</span>d6460a74447
# Step <span class="hljs-number">3</span>/<span class="hljs-number">4</span> : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; Running in b4951b6b48bb
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container b4951b6b48bb
#  ---&gt; fdc6cdd8925a
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3bdbd2af4f0e
# Removing intermediate container 3bdbd2af4f0e
#  ---&gt; f8837621b99d
# Successfully built f8837621b99d
# Successfully tagged custom-nginx:packaged</span>
</code></pre><p>Nothing will change except the fact that you can now refer to your image as <code>custom-nginx:packaged</code> instead of some long random string.</p>
<p>In cases where you forgot to tag an image during build time, or maybe you want to change the tag, you can use the <code>image tag</code> command to do that:</p>
<pre><code>docker image tag &lt;image id&gt; &lt;image repository&gt;:&lt;image tag&gt;

## or ##

docker image tag &lt;image repository&gt;:&lt;image tag&gt; &lt;new image repository&gt;:&lt;new image tag&gt;
</code></pre><h3 id="heading-how-to-list-and-remove-docker-images">How to List and Remove Docker Images</h3>
<p>Just like the <code>container ls</code> command, you can use the <code>image ls</code> command to list all the images in your local system:</p>
<pre><code>docker image ls

# REPOSITORY     TAG        IMAGE ID       CREATED         SIZE
# &lt;none&gt;         <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">none</span>&gt;</span>     3199372aa3fc   7 seconds ago   132MB
# custom-nginx   packaged   f8837621b99d   4 minutes ago   132MB</span>
</code></pre><p>Images listed here can be deleted using the <code>image rm</code> command. The generic syntax is as follows:</p>
<pre><code>docker image rm &lt;image identifier&gt;
</code></pre><p>The identifier can be the image ID or image repository. If you use the repository, you'll have to identify the tag as well. To delete the <code>custom-nginx:packaged</code> image, you may execute the following command:</p>
<pre><code>docker image rm custom-nginx:packaged

# Untagged: custom-nginx:packaged
# Deleted: sha256:f8837621b99d3388a9e78d9ce49fbb773017f770eea80470fb85e0052beae242
# Deleted: sha256:fdc6cdd8925ac25b9e0ed1c8539f96ad89ba1b21793d061e2349b62dd517dadf
# Deleted: sha256:c20e4aa46615fe512a4133089a5cd66f9b7da76366c96548790d5bf865bd49c4
# Deleted: sha256:<span class="hljs-number">6</span>d6460a744475a357a2b631a4098aa1862d04510f3625feb316358536fcd8641
</code></pre><p>You can also use the <code>image prune</code> command to cleanup all un-tagged dangling images as follows:</p>
<pre><code>docker image prune --force

# Deleted Images:
# deleted: sha256:ba9558bdf2beda81b9acc652ce4931a85f0fc7f69dbc91b4efc4561ef7378aff
# deleted: sha256:ad9cc3ff27f0d192f8fa5fadebf813537e02e6ad472f6536847c4de183c02c81
# deleted: sha256:f1e9b82068d43c1bb04ff3e4f0085b9f8903a12b27196df7f1145aa9296c85e7
# deleted: sha256:ec16024aa036172544908ec4e5f842627d04ef99ee9b8d9aaa26b9c2a4b52baa

# Total reclaimed space: <span class="hljs-number">59.19</span>MB
</code></pre><p>The <code>--force</code> or <code>-f</code> option skips any confirmation questions. You can also use the <code>--all</code> or <code>-a</code> option to remove all cached images in your local registry.</p>
<h3 id="heading-how-to-understand-the-many-layers-of-a-docker-image">How to Understand the Many Layers of a Docker Image</h3>
<p>From the very beginning of this book, I've been saying that images are multi-layered files. In this sub-section I'll demonstrate the various layers of an image and how they play an important role in the build process of that image. </p>
<p>For this demonstration, I'll be using the <code>custom-nginx:packaged</code> image from the previous sub-section.</p>
<p>To visualize the many layers of an image, you can use the <code>image history</code> command. The various layers of the <code>custom-nginx:packaged</code> image can be visualized as follows:</p>
<pre><code>docker image history custom-nginx:packaged

# IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
# <span class="hljs-number">7</span>f16387f7307        <span class="hljs-number">5</span> minutes ago       /bin/sh -c #(nop)  CMD [<span class="hljs-string">"nginx"</span> <span class="hljs-string">"-g"</span> <span class="hljs-string">"daemon…   0B                             
# 587c805fe8df        5 minutes ago       /bin/sh -c apt-get update &amp;&amp;     apt-get ins…   60MB                
# 6fe4e51e35c1        6 minutes ago       /bin/sh -c #(nop)  EXPOSE 80                    0B                  
# d70eaf7277ea        17 hours ago        /bin/sh -c #(nop)  CMD ["</span>/bin/bash<span class="hljs-string">"]            0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c mkdir -p /run/systemd &amp;&amp; echo 'do…   7B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c [ -z "</span>$(apt-get indextargets)<span class="hljs-string">" ]     0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c set -xe   &amp;&amp; echo '#!/bin/sh' &gt; /…   811B                
# &lt;missing&gt;           17 hours ago        /bin/sh -c #(nop) ADD file:435d9776fdd3a1834…   72.9MB</span>
</code></pre><p>There are eight layers of this image. The upper most layer is the latest one and as you go down the layers get older. The upper most layer is the one that you usually use for running containers.</p>
<p>Now, let's have a closer look at the images beginning from image <code>d70eaf7277ea</code> down to <code>7f16387f7307</code>. I'll ignore the bottom four layers where the <code>IMAGE</code> is <code>&lt;missing&gt;</code> as they are not of our concern.</p>
<ul>
<li><code>d70eaf7277ea</code> was created by <code>/bin/sh -c #(nop)  CMD ["/bin/bash"]</code> which indicates that the default shell inside Ubuntu has been loaded successfully.</li>
<li><code>6fe4e51e35c1</code> was created by <code>/bin/sh -c #(nop)  EXPOSE 80</code> which was the second instruction in your code.</li>
<li><code>587c805fe8df</code> was created by <code>/bin/sh -c apt-get update &amp;&amp; apt-get install nginx -y &amp;&amp; apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> which was the third instruction in your code. You can also see that this image has a size of <code>60MB</code> given all necessary packages were installed during the execution of this instruction.</li>
<li>Finally the upper most layer <code>7f16387f7307</code> was created by <code>/bin/sh -c #(nop)  CMD ["nginx", "-g", "daemon off;"]</code> which sets the default command for this image.</li>
</ul>
<p>As you can see, the image comprises of many read-only layers, each recording a new set of changes to the state triggered by certain instructions. When you start a container using an image, you get a new writable layer on top of the other layers.</p>
<p>This layering phenomenon that happens every time you work with Docker has been made possible by an amazing technical concept called a union file system. Here, union means union in set theory. According to <a target="_blank" href="https://en.wikipedia.org/wiki/UnionFS">Wikipedia</a> - </p>
<blockquote>
<p>It allows files and directories of separate file systems, known as branches, to be transparently overlaid, forming a single coherent file system. Contents of directories which have the same path within the merged branches will be seen together in a single merged directory, within the new, virtual filesystem.</p>
</blockquote>
<p>By utilizing this concept, Docker can avoid data duplication and can use previously created layers as a cache for later builds. This results in compact, efficient images that can be used everywhere.</p>
<h3 id="heading-how-to-build-nginx-from-source">How to Build NGINX from Source</h3>
<p>In the previous sub-section, you learned about the <code>FROM</code>, <code>EXPOSE</code>, <code>RUN</code> and <code>CMD</code> instructions. In this sub-section you'll be learning a lot more about other instructions.</p>
<p>In this sub-section you'll again create a custom NGINX image. But the twist is that you'll be building NGINX from source instead of installing it using some package manager such as <code>apt-get</code> as in the previous example.</p>
<p>In order to build NGINX from source, you first need the source of NGINX. If you've cloned my projects repository you'll see a file named <code>nginx-1.19.2.tar.gz</code> inside the <code>custom-nginx</code> directory. You'll use this archive as the source for building NGINX.</p>
<p>Before diving into writing some code, let's plan out the process first. The image creation process this time can be done in seven steps. These are as follows:</p>
<ul>
<li>Get a good base image for building the application, like <a target="_blank" href="https://hub.docker.com/_/ubuntu">ubuntu</a>.</li>
<li>Install necessary build dependencies on the base image.</li>
<li>Copy the <code>nginx-1.19.2.tar.gz</code> file inside the image.</li>
<li>Extract the contents of the archive and get rid of it.</li>
<li>Configure the build, compile and install the program using the <code>make</code> tool.</li>
<li>Get rid of the extracted source code.</li>
<li>Run <code>nginx</code> executable.</li>
</ul>
<p>Now that you have a plan, let's begin by opening up old <code>Dockerfile</code> and updating its contents as follows:</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl1<span class="hljs-number">.1</span> \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*

COPY nginx-1.19.2.tar.gz .

RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz

RUN cd nginx-1.19.2 &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /nginx-1.19.2

CMD ["nginx", "-g", "daemon off;"]</span>
</code></pre><p>As you can see, the code inside the <code>Dockerfile</code> reflects the seven steps I talked about above.</p>
<ul>
<li>The <code>FROM</code> instruction sets Ubuntu as the base image making an ideal environment for building any application.</li>
<li>The <code>RUN</code> instruction installs standard packages necessary for building NGINX from source.</li>
<li>The <code>COPY</code> instruction here is something new. This instruction is responsible for copying the the <code>nginx-1.19.2.tar.gz</code> file inside the image. The generic syntax for the <code>COPY</code> instruction is <code>COPY &lt;source&gt; &lt;destination&gt;</code> where source is in your local filesystem and the destination is inside your image. The <code>.</code> as the destination means the working directory inside the image which is by default <code>/</code> unless set otherwise.</li>
<li>The second <code>RUN</code> instruction here extracts the contents from the archive using <code>tar</code> and gets rid of it afterwards.</li>
<li>The archive file contains a directory called <code>nginx-1.19.2</code> containing the source code. So on the next step, you'll have to <code>cd</code> inside that directory and perform the build process. You can read the <a target="_blank" href="https://itsfoss.com/install-software-from-source-code/">How to Install Software from Source Code… and Remove it Afterwards</a> article to learn more on the topic.</li>
<li>Once the build and installation is complete, you remove the <code>nginx-1.19.2</code> directory using <code>rm</code> command.</li>
<li>On the final step you start NGINX in single process mode just like you did before.</li>
</ul>
<p>Now to build an image using this code, execute the following command:</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step <span class="hljs-number">1</span>/<span class="hljs-number">7</span> : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step <span class="hljs-number">2</span>/<span class="hljs-number">7</span> : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; Running in 2d0aa912ea47
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 2d0aa912ea47
#  ---&gt; cbe1ced3da11
# Step 3/7 : COPY nginx-1.19.2.tar.gz .
#  ---&gt; 7202902edf3f
# Step 4/7 : RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz
 ---&gt; Running in 4a4a95643020
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 4a4a95643020
#  ---&gt; f9dec072d6d6
# Step 5/7 : RUN cd nginx-1.19.2 &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in b07ba12f921e
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container b07ba12f921e
#  ---&gt; 5a877edafd8b
# Step 6/7 : RUN rm -rf /nginx-1.19.2
#  ---&gt; Running in 947e1d9ba828
# Removing intermediate container 947e1d9ba828
#  ---&gt; a7702dc7abb7
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3110c7fdbd57
# Removing intermediate container 3110c7fdbd57
#  ---&gt; eae55f7369d3
# Successfully built eae55f7369d3
# Successfully tagged custom-nginx:built</span>
</code></pre><p>This code is alright but there are some places where we can make improvements.</p>
<ul>
<li>Instead of hard coding the filename like <code>nginx-1.19.2.tar.gz</code>, you can create an argument using the <code>ARG</code> instruction. This way, you'll be able to change the version or filename by just changing the argument.</li>
<li>Instead of downloading the archive manually, you can let the daemon download the file during the build process. There is another instruction like <code>COPY</code> called the <code>ADD</code> instruction which is capable of adding files from the internet.</li>
</ul>
<p>Open up the <code>Dockerfile</code> file and update its content as follows:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> ubuntu:latest

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install build-essential\ </span>
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl1.<span class="hljs-number">1</span> \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

<span class="hljs-keyword">ARG</span> FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
<span class="hljs-keyword">ARG</span> EXTENSION=<span class="hljs-string">"tar.gz"</span>

<span class="hljs-keyword">ADD</span><span class="bash"> https://nginx.org/download/<span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> .</span>

<span class="hljs-keyword">RUN</span><span class="bash"> tar -xvf <span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> &amp;&amp; rm <span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span></span>

<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">cd</span> <span class="hljs-variable">${FILENAME}</span> &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/<span class="hljs-built_in">log</span>/nginx/error.log \
        --http-log-path=/var/<span class="hljs-built_in">log</span>/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install</span>

<span class="hljs-keyword">RUN</span><span class="bash"> rm -rf /<span class="hljs-variable">${FILENAME}</span>}</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<p>The code is almost identical to the previous code block except for a new instruction called <code>ARG</code> on line 13, 14 and the usage of the <code>ADD</code> instruction on line 16. Explanation for the updated code is as follows:</p>
<ul>
<li>The <code>ARG</code> instruction lets you declare variables like in other languages. These variables or arguments can later be accessed using the <code>${argument name}</code> syntax. Here, I've put the filename <code>nginx-1.19.2</code> and the file extension <code>tar.gz</code> in two separate arguments. This way I can switch between newer versions of NGINX or the archive format by making a change in just one place. In the code above, I've added default values to the variables. Variable values can be passed as options of the <code>image build</code> command as well. You can consult the <a target="_blank" href="https://docs.docker.com/engine/reference/builder/#arg">official reference</a> for more details.</li>
<li>In the <code>ADD</code> instruction, I've formed the download URL dynamically using the arguments declared above. The <code>https://nginx.org/download/${FILENAME}.${EXTENSION}</code> line will result in something like <code>https://nginx.org/download/nginx-1.19.2.tar.gz</code> during the build process. You can change the file version or the extension by changing it in just one place thanks to the <code>ARG</code> instruction.</li>
<li>The <code>ADD</code> instruction doesn't extract files obtained from the internet by default, hence the usage of <code>tar</code> on line 18.</li>
</ul>
<p>The rest of the code is almost unchanged. You should be able to understand the usage of the arguments by yourself now. Finally let's try to build an image from this updated code.</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step <span class="hljs-number">1</span>/<span class="hljs-number">9</span> : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step <span class="hljs-number">2</span>/<span class="hljs-number">9</span> : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged custom-nginx:built</span>
</code></pre><p>Now you should be able to run a container using the <code>custom-nginx:built</code> image.</p>
<pre><code>docker container run --rm --detach --name custom-nginx-built --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> custom-nginx:built

# <span class="hljs-number">90</span>ccdbc0b598dddc4199451b2f30a942249d85a8ed21da3c8d14612f17eed0aa

docker container ls

# CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                  NAMES
# <span class="hljs-number">90</span>ccdbc0b598        custom-nginx:built   <span class="hljs-string">"nginx -g 'daemon of…"</span>   <span class="hljs-number">2</span> minutes ago       Up <span class="hljs-number">2</span> minutes        <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8080</span>-&gt;<span class="hljs-number">80</span>/tcp   custom-nginx-built
</code></pre><p>A container using the <code>custom-nginx:built-v2</code> image has been successfully run. The container should be accessible at <code>http://127.0.0.1:8080</code> now.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>And here is the trusty default response page from NGINX. You can visit the <a target="_blank" href="https://docs.docker.com/engine/reference/builder/">official reference</a> site to learn more about the available instructions.</p>
<h3 id="heading-how-to-optimize-docker-images">How to Optimize Docker Images</h3>
<p>The image we built in the last sub-section is functional but very unoptimized. To prove my point let's have a look at the size of the image using the <code>image ls</code> command:</p>
<pre><code>docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     <span class="hljs-number">1</span>f3aaf40bb54   <span class="hljs-number">16</span> minutes ago   <span class="hljs-number">343</span>MB
</code></pre><p>For an image containing only NGINX, that's too much. If you pull the official image and check its size, you'll see how small it is:</p>
<pre><code>docker image pull nginx:stable

# stable: Pulling <span class="hljs-keyword">from</span> library/nginx
# a076a628af6f: Pull complete 
# <span class="hljs-number">45</span>d7b5d3927d: Pull complete 
# <span class="hljs-number">5e326</span>fece82e: Pull complete 
# <span class="hljs-number">30</span>c386181b68: Pull complete 
# b15158e9ebbe: Pull complete 
# Digest: sha256:ebd0fd56eb30543a9195280eb81af2a9a8e6143496accd6a217c14b06acd1419
# Status: Downloaded newer image <span class="hljs-keyword">for</span> nginx:stable
# docker.io/library/nginx:stable

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     <span class="hljs-number">1</span>f3aaf40bb54   <span class="hljs-number">25</span> minutes ago   <span class="hljs-number">343</span>MB
# nginx              stable    b9e1dc12387a   <span class="hljs-number">11</span> days ago      <span class="hljs-number">133</span>MB
</code></pre><p>In order to find out the root cause, let's have a look at the <code>Dockerfile</code> first:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> ubuntu:latest

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install build-essential\ </span>
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl1.<span class="hljs-number">1</span> \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

<span class="hljs-keyword">ARG</span> FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
<span class="hljs-keyword">ARG</span> EXTENSION=<span class="hljs-string">"tar.gz"</span>

<span class="hljs-keyword">ADD</span><span class="bash"> https://nginx.org/download/<span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> .</span>

<span class="hljs-keyword">RUN</span><span class="bash"> tar -xvf <span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> &amp;&amp; rm <span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span></span>

<span class="hljs-keyword">RUN</span><span class="bash"> <span class="hljs-built_in">cd</span> <span class="hljs-variable">${FILENAME}</span> &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/<span class="hljs-built_in">log</span>/nginx/error.log \
        --http-log-path=/var/<span class="hljs-built_in">log</span>/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install</span>

<span class="hljs-keyword">RUN</span><span class="bash"> rm -rf /<span class="hljs-variable">${FILENAME}</span>}</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<p>As you can see on line 3, the <code>RUN</code> instruction installs a lot of stuff. Although these packages are necessary for building NGINX from source, they are not necessary for running it. </p>
<p>Out of the 6 packages that we installed, only two are necessary for running NGINX. These are <code>libpcre3</code> and <code>zlib1g</code>. So a better idea would be to uninstall the other packages once the build process is done.</p>
<p>To do so, update your <code>Dockerfile</code> as follows:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> ubuntu:latest

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">80</span>

<span class="hljs-keyword">ARG</span> FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
<span class="hljs-keyword">ARG</span> EXTENSION=<span class="hljs-string">"tar.gz"</span>

<span class="hljs-keyword">ADD</span><span class="bash"> https://nginx.org/download/<span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> .</span>

<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install build-essential \ </span>
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl1.<span class="hljs-number">1</span> \
                    libssl-dev \
                    -y &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/<span class="hljs-keyword">run</span><span class="bash">/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    <span class="hljs-built_in">cd</span> / &amp;&amp; rm -rfv /<span class="hljs-variable">${FILENAME}</span> &amp;&amp; \
    apt-get remove build-essential \ </span>
                    libpcre3-dev \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get autoremove -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<p>As you can see, on line 10 a single <code>RUN</code> instruction is doing all the necessary heavy-lifting. The exact chain of events is as follows:</p>
<ul>
<li>From line 10 to line 17, all the necessary packages are being installed.</li>
<li>On line 18, the source code is being extracted and the downloaded archive gets removed.</li>
<li>From line 19 to line 28, NGINX is configured, built, and installed on the system.</li>
<li>On line 29, the extracted files from the downloaded archive get removed.</li>
<li>From line 30 to line 36, all the unnecessary packages are being uninstalled and cache cleared. The <code>libpcre3</code> and <code>zlib1g</code> packages are needed for running NGINX so we keep them.</li>
</ul>
<p>You may ask why am I doing so much work in a single <code>RUN</code> instruction instead of nicely splitting them into multiple instructions like we did previously. Well, splitting them up would be a mistake. </p>
<p>If you install packages and then remove them in separate <code>RUN</code> instructions, they'll live in separate layers of the image. Although the final image will not have the removed packages, their size will still be added to the final image since they exist in one of the layers consisting the image. So make sure you make these kind of changes on a single layer.</p>
<p>Let's build an image using this <code>Dockerfile</code> and see the differences.</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  <span class="hljs-number">1.057</span>MB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">7</span> : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step <span class="hljs-number">2</span>/<span class="hljs-number">7</span> : EXPOSE <span class="hljs-number">80</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">006</span>f39b75964
# Removing intermediate container <span class="hljs-number">006</span>f39b75964
#  ---&gt; <span class="hljs-number">6943</span>f7ef9376
# Step <span class="hljs-number">3</span>/<span class="hljs-number">7</span> : ARG FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> ffaf89078594
# Removing intermediate container ffaf89078594
#  ---&gt; <span class="hljs-number">91</span>b5cdb6dabe
# Step <span class="hljs-number">4</span>/<span class="hljs-number">7</span> : ARG EXTENSION=<span class="hljs-string">"tar.gz"</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> d0f5188444b6
# Removing intermediate container d0f5188444b6
#  ---&gt; <span class="hljs-number">9626</span>f941ccb2
# Step <span class="hljs-number">5</span>/<span class="hljs-number">7</span> : ADD https:<span class="hljs-comment">//nginx.org/download/${FILENAME}.${EXTENSION} .</span>
# Downloading [==================================================&gt;]  <span class="hljs-number">1.049</span>MB/<span class="hljs-number">1.049</span>MB
#  ---&gt; a8e8dcca1be8
# Step <span class="hljs-number">6</span>/<span class="hljs-number">7</span> : RUN apt-get update &amp;&amp;     apt-get install build-essential                     libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=<span class="hljs-regexp">/usr/</span>bin/nginx         --conf-path=<span class="hljs-regexp">/etc/</span>nginx/nginx.conf         --error-log-path=<span class="hljs-regexp">/var/</span>log/nginx/error.log         --http-log-path=<span class="hljs-regexp">/var/</span>log/nginx/access.log         --<span class="hljs-keyword">with</span>-pcre         --pid-path=<span class="hljs-regexp">/var/</span>run/nginx.pid         --<span class="hljs-keyword">with</span>-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apt-get remove build-essential                     libpcre3-dev                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get autoremove -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; Running in e5675cad1260
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container e5675cad1260
#  ---&gt; dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; 512aa6a95a93
# Successfully built 512aa6a95a93
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED              SIZE
# custom-nginx       built     512aa6a95a93   About a minute ago   81.6MB
# nginx              stable    b9e1dc12387a   11 days ago          133MB</span>
</code></pre><p>As you can see, the image size has gone from being 343MB to 81.6MB. The official image is 133MB. This is a pretty optimized build, but we can go a bit further in the next sub-section.</p>
<h3 id="heading-embracing-alpine-linux">Embracing Alpine Linux</h3>
<p>If you've been fiddling around with containers for some time now, you may have heard about something called <a target="_blank" href="https://alpinelinux.org/">Alpine Linux</a>. It's a full-featured <a target="_blank" href="https://en.wikipedia.org/wiki/Linux">Linux</a> distribution like <a target="_blank" href="https://ubuntu.com/">Ubuntu</a>, <a target="_blank" href="https://www.debian.org/">Debian</a> or <a target="_blank" href="https://getfedora.org/">Fedora</a>. </p>
<p>But the good thing about Alpine is that it's built around <code>musl</code> <code>libc</code> and <code>busybox</code> and is lightweight. Where the latest <a target="_blank" href="https://hub.docker.com/_/ubuntu">ubuntu</a> image weighs at around 28MB, <a target="_blank" href="https://hub.docker.com/_/alpine">alpine</a> is 2.8MB. </p>
<p>Apart from the lightweight nature, Alpine is also secure and is a much better fit for creating containers than the other distributions.</p>
<p>Although not as user friendly as the other commercial distributions, the transition to Alpine is still very simple. In this sub-section you'll learn about recreating the <code>custom-nginx</code> image using the Alpine image as its base.</p>
<p>Open up your <code>Dockerfile</code> and update its content as follows:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> alpine:latest

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">80</span>

<span class="hljs-keyword">ARG</span> FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
<span class="hljs-keyword">ARG</span> EXTENSION=<span class="hljs-string">"tar.gz"</span>

<span class="hljs-keyword">ADD</span><span class="bash"> https://nginx.org/download/<span class="hljs-variable">${FILENAME}</span>.<span class="hljs-variable">${EXTENSION}</span> .</span>

<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache pcre zlib &amp;&amp; \
    apk add --no-cache \
            --virtual .build-deps \
            build-base \ </span>
            pcre-dev \
            zlib-dev \
            openssl-dev &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/<span class="hljs-keyword">run</span><span class="bash">/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    <span class="hljs-built_in">cd</span> / &amp;&amp; rm -rfv /<span class="hljs-variable">${FILENAME}</span> &amp;&amp; \
    apk del .build-deps</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<p>The code is almost identical except for a few changes. I'll be listing the changes and explaining them as I go:</p>
<ul>
<li>Instead of using <code>apt-get install</code> for installing packages, we use <code>apk add</code>. The <code>--no-cache</code> option means that the downloaded package won't be cached. Likewise we'll use <code>apk del</code> instead of <code>apt-get remove</code> to uninstall packages.</li>
<li>The <code>--virtual</code> option for the <code>apk add</code> command is used for bundling a bunch of packages into a single virtual package for easier management. Packages that are needed only for building the program are labeled as <code>.build-deps</code> which are then removed on line 29 by executing the <code>apk del .build-deps</code> command. You can learn more about <a target="_blank" href="https://docs.alpinelinux.org/user-handbook/0.1a/Working/apk.html#_virtuals">virtuals</a> in the official docs.</li>
<li>The package names are a bit different here. Usually every Linux distribution has its package repository available to everyone where you can search for packages. If you know the packages required for a certain task, then you can just head over to the designated repository for a distribution and search for it. You can <a target="_blank" href="https://pkgs.alpinelinux.org/packages">look up Alpine Linux packages here</a>.</li>
</ul>
<p>Now build a new image using this <code>Dockerfile</code> and see the difference in file size:</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  <span class="hljs-number">1.055</span>MB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">7</span> : FROM alpine:latest
#  ---&gt; <span class="hljs-number">7731472</span>c3f2a
# Step <span class="hljs-number">2</span>/<span class="hljs-number">7</span> : EXPOSE <span class="hljs-number">80</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">8336</span>cfaaa48d
# Removing intermediate container <span class="hljs-number">8336</span>cfaaa48d
#  ---&gt; d448a9049d01
# Step <span class="hljs-number">3</span>/<span class="hljs-number">7</span> : ARG FILENAME=<span class="hljs-string">"nginx-1.19.2"</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> bb8b2eae9d74
# Removing intermediate container bb8b2eae9d74
#  ---&gt; <span class="hljs-number">87</span>ca74f32fbe
# Step <span class="hljs-number">4</span>/<span class="hljs-number">7</span> : ARG EXTENSION=<span class="hljs-string">"tar.gz"</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> aa09627fe48c
# Removing intermediate container aa09627fe48c
#  ---&gt; <span class="hljs-number">70</span>cb557adb10
# Step <span class="hljs-number">5</span>/<span class="hljs-number">7</span> : ADD https:<span class="hljs-comment">//nginx.org/download/${FILENAME}.${EXTENSION} .</span>
# Downloading [==================================================&gt;]  <span class="hljs-number">1.049</span>MB/<span class="hljs-number">1.049</span>MB
#  ---&gt; b9790ce0c4d6
# Step <span class="hljs-number">6</span>/<span class="hljs-number">7</span> : RUN apk add --no-cache pcre zlib &amp;&amp;     apk add --no-cache             --virtual .build-deps             build-base             pcre-dev             zlib-dev             openssl-dev &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=<span class="hljs-regexp">/usr/</span>bin/nginx         --conf-path=<span class="hljs-regexp">/etc/</span>nginx/nginx.conf         --error-log-path=<span class="hljs-regexp">/var/</span>log/nginx/error.log         --http-log-path=<span class="hljs-regexp">/var/</span>log/nginx/access.log         --<span class="hljs-keyword">with</span>-pcre         --pid-path=<span class="hljs-regexp">/var/</span>run/nginx.pid         --<span class="hljs-keyword">with</span>-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apk del .build-deps
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>b301f64ffc1
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container <span class="hljs-number">0</span>b301f64ffc1
#  ---&gt; dc7e4161f975
# Step <span class="hljs-number">7</span>/<span class="hljs-number">7</span> : CMD [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]
#  ---&gt; Running <span class="hljs-keyword">in</span> b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; <span class="hljs-number">3e186</span>a3c6830
# Successfully built <span class="hljs-number">3e186</span>a3c6830
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
# custom-nginx       built     <span class="hljs-number">3e186</span>a3c6830   <span class="hljs-number">8</span> seconds ago   <span class="hljs-number">12.8</span>MB
</code></pre><p>Where the ubuntu version was 81.6MB, the alpine one has come down to 12.8MB which is a massive gain. Apart from the <code>apk</code> package manager, there are some other things that differ in Alpine from Ubuntu but they're not that big a deal. You can just search the internet whenever you get stuck.</p>
<h3 id="heading-how-to-create-executable-docker-images">How to Create Executable Docker Images</h3>
<p>In the previous section you worked with the <a target="_blank" href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> image. In this section you'll learn how to make such an executable image. </p>
<p>To begin with, open up the directory where you've cloned the repository that came with this book. The code for the <code>rmbyext</code> application resides inside the sub-directory with the same name.</p>
<p>Before you start working on the <code>Dockerfile</code> take a moment to plan out what the final output should be. In my opinion it should be like something like this:</p>
<ul>
<li>The image should have Python pre-installed.</li>
<li>It should contain a copy of my <code>rmbyext</code> script.</li>
<li>A working directory should be set where the script will be executed.</li>
<li>The <code>rmbyext</code> script should be set as the entry-point so the image can take extension names as arguments.</li>
</ul>
<p>To build the above mentioned image, take the following steps:</p>
<ul>
<li>Get a good base image for running Python scripts, like <a target="_blank" href="https://hub.docker.com/_/python">python</a>.</li>
<li>Set-up the working directory to an easily accessible directory.</li>
<li>Install Git so that the script can be installed from my GitHub repository.</li>
<li>Install the script using Git and pip.</li>
<li>Get rid of the build's unnecessary packages.</li>
<li>Set <code>rmbyext</code> as the entry-point for this image.</li>
</ul>
<p>Now create a new <code>Dockerfile</code> inside the <code>rmbyext</code> directory and put the following code in it:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3</span>-alpine

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /zone</span>

<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache git &amp;&amp; \
    pip install git+https://github.com/fhsinchy/rmbyext.git<span class="hljs-comment">#egg=rmbyext &amp;&amp; \</span>
    apk del git</span>

<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [ <span class="hljs-string">"rmbyext"</span> ]</span>
</code></pre>
<p>The explanation for the instructions in this file is as follows:</p>
<ul>
<li>The <code>FROM</code> instruction sets <a target="_blank" href="https://hub.docker.com/_/python">python</a> as the base image, making an ideal environment for running Python scripts. The <code>3-alpine</code> tag indicates that you want the Alpine variant of Python 3.</li>
<li>The <code>WORKDIR</code> instruction sets the default working directory to <code>/zone</code> here. The name of the working directory is completely random here. I found zone to be a fitting name, you may use anything you want.</li>
<li>Given the <code>rmbyext</code> script is installed from GitHub, <code>git</code> is an install time dependency. The <code>RUN</code> instruction on line 5 installs <code>git</code> then installs the <code>rmbyext</code> script using Git and pip. It also gets rid of <code>git</code> afterwards.</li>
<li>Finally on line 9, the <code>ENTRYPOINT</code> instruction sets the <code>rmbyext</code> script as the entry-point for this image.</li>
</ul>
<p>In this entire file, line 9 is the magic that turns this seemingly normal image into an executable one. Now to build the image you can execute following command:</p>
<pre><code>docker image build --tag rmbyext .

# Sending build context to Docker daemon  <span class="hljs-number">2.048</span>kB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">4</span> : FROM python:<span class="hljs-number">3</span>-alpine
# <span class="hljs-number">3</span>-alpine: Pulling <span class="hljs-keyword">from</span> library/python
# <span class="hljs-number">801</span>bfaa63ef2: Already exists 
# <span class="hljs-number">8723</span>b2b92bec: Already exists 
# <span class="hljs-number">4e07029</span>ccd64: Already exists 
# <span class="hljs-number">594990504179</span>: Already exists 
# <span class="hljs-number">140</span>d7fec7322: Already exists 
# Digest: sha256:<span class="hljs-number">7492</span>c1f615e3651629bd6c61777e9660caa3819cf3561a47d1d526dfeee02cf6
# Status: Downloaded newer image <span class="hljs-keyword">for</span> python:<span class="hljs-number">3</span>-alpine
#  ---&gt; d4d4f50f871a
# Step <span class="hljs-number">2</span>/<span class="hljs-number">4</span> : WORKDIR /zone
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">454374612</span>a91
# Removing intermediate container <span class="hljs-number">454374612</span>a91
#  ---&gt; <span class="hljs-number">7</span>f7e49bc98d2
# Step <span class="hljs-number">3</span>/<span class="hljs-number">4</span> : RUN apk add --no-cache git &amp;&amp;     pip install git+https:<span class="hljs-comment">//github.com/fhsinchy/rmbyext.git#egg=rmbyext &amp;&amp;     apk del git</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">27e2</span>e96dc95a
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container <span class="hljs-number">27e2</span>e96dc95a
#  ---&gt; <span class="hljs-number">3</span>c7389432e36
# Step <span class="hljs-number">4</span>/<span class="hljs-number">4</span> : ENTRYPOINT [ <span class="hljs-string">"rmbyext"</span> ]
#  ---&gt; Running <span class="hljs-keyword">in</span> f239bbea1ca6
# Removing intermediate container f239bbea1ca6
#  ---&gt; <span class="hljs-number">1746</span>b0cedbc7
# Successfully built <span class="hljs-number">1746</span>b0cedbc7
# Successfully tagged rmbyext:latest

docker image ls

# REPOSITORY         TAG        IMAGE ID       CREATED         SIZE
# rmbyext            latest     <span class="hljs-number">1746</span>b0cedbc7   <span class="hljs-number">4</span> minutes ago   <span class="hljs-number">50.9</span>MB
</code></pre><p>Here I haven't provided any tag after the image name, so the image has been tagged as <code>latest</code> by default. You should be able to run the image as you saw in the previous section. Remember to refer to the actual image name you've set, instead of <code>fhsinchy/rmbyext</code> here.</p>
<h3 id="heading-how-to-share-your-docker-images-online">How to Share Your Docker Images Online</h3>
<p>Now that you know how to make images, it's time to share them with the world. Sharing images online is easy. All you need is an account at any of the online registries. I'll be using <a target="_blank" href="https://hub.docker.com/">Docker Hub</a> here. </p>
<p>Navigate to the <a target="_blank" href="https://hub.docker.com/signup">Sign Up</a> page and create a free account. A free account allows you to host unlimited public repositories and one private repository.</p>
<p>Once you've created the account, you'll have to sign in to it using the docker CLI. So open up your terminal and execute the following command to do so:</p>
<pre><code>docker login

# Login <span class="hljs-keyword">with</span> your Docker ID to push and pull images <span class="hljs-keyword">from</span> Docker Hub. If you don<span class="hljs-string">'t have a Docker ID, head over to https://hub.docker.com to create one.
# Username: fhsinchy
# Password: 
# WARNING! Your password will be stored unencrypted in /home/fhsinchy/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#
# Login Succeeded</span>
</code></pre><p>You'll be prompted for your username and password. If you input them properly, you should be logged in to your account successfully.</p>
<p>In order to share an image online, the image has to be tagged. You've already learned about tagging in a previous sub-section. Just to refresh your memory, the generic syntax for the <code>--tag</code> or <code>-t</code> option is as follows:</p>
<pre><code>--tag &lt;image repository&gt;:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">image</span> <span class="hljs-attr">tag</span>&gt;</span></span>
</code></pre><p>As an example, let's share the <code>custom-nginx</code> image online. To do so, open up a new terminal window inside the <code>custom-nginx</code> project directory. </p>
<p>To share an image online, you'll have to tag it following the <code>&lt;docker hub username&gt;/&lt;image name&gt;:&lt;image tag&gt;</code> syntax. My username is <code>fhsinchy</code> so the command will look like this:</p>
<pre><code>docker image build --tag fhsinchy/custom-nginx:latest --file Dockerfile.built .

# Step <span class="hljs-number">1</span>/<span class="hljs-number">9</span> : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step <span class="hljs-number">2</span>/<span class="hljs-number">9</span> : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /<span class="hljs-keyword">var</span>/lib/apt/lists<span class="hljs-comment">/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged fhsinchy/custom-nginx:latest</span>
</code></pre><p>In this command the <code>fhsinchy/custom-nginx</code> is the image repository and <code>latest</code> is the tag. The image name can be anything you want and can not be changed once you've uploaded the image. The tag can be changed whenever you want and usually reflects the version of the software or different kind of builds.</p>
<p>Take the <code>node</code> image as an example. The <code>node:lts</code> image refers to the long term support version of Node.js whereas the <code>node:lts-alpine</code> version refers to the Node.js version built for Alpine Linux, which is much smaller than the regular one.</p>
<p>If you do not give the image any tag, it'll be automatically tagged as <code>latest</code>. But that doesn't mean that the <code>latest</code> tag will always refer to the latest version. If, for some reason, you explicitly tag an older version of the image as <code>latest</code>, then Docker will not make any extra effort to cross check that.</p>
<p>Once the image has been built, you can them upload it by executing the following command:</p>
<pre><code>docker image push &lt;image repository&gt;:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">image</span> <span class="hljs-attr">tag</span>&gt;</span></span>
</code></pre><p>So in my case the command will be as follows:</p>
<pre><code>docker image push fhsinchy/custom-nginx:latest

# The push refers to repository [docker.io/fhsinchy/custom-nginx]
# <span class="hljs-number">4352</span>b1b1d9f5: Pushed 
# a4518dd720bd: Pushed 
# <span class="hljs-number">1</span>d756dc4e694: Pushed 
# d7a7e2b6321a: Pushed 
# f6253634dc78: Mounted <span class="hljs-keyword">from</span> library/ubuntu 
# <span class="hljs-number">9069</span>f84dbbe9: Mounted <span class="hljs-keyword">from</span> library/ubuntu 
# bacd3af13903: Mounted <span class="hljs-keyword">from</span> library/ubuntu 
# latest: digest: sha256:ffe93440256c9edb2ed67bf3bba3c204fec3a46a36ac53358899ce1a9eee497a size: <span class="hljs-number">1788</span>
</code></pre><p>Depending on the image size, the upload may take some time. Once it's done you should able to find the image in your hub profile page.</p>
<h2 id="heading-how-to-containerize-a-javascript-application">How to Containerize a JavaScript Application</h2>
<p>Now that you've got some idea of how to create images, it's time to work with something a bit more relevant. </p>
<p>In this sub-section, you'll be working with the source code of the <a target="_blank" href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> image that you worked with on a previous section. In the process of containerizing this very simple application, you'll be introduced to volumes and multi-staged builds, two of the most important concepts in Docker.</p>
<h3 id="heading-how-to-write-the-development-dockerfile">How to Write the Development Dockerfile</h3>
<p>To begin with, open up the directory where you've cloned the repository that came with this book. Code for the <code>hello-dock</code> application resides inside the sub-directory with the same name.</p>
<p>This is a very simple JavaScript project powered by the <a target="_blank" href="https://github.com/vitejs/vite">vitejs/vite</a> project. Don't worry though, you don't need to know JavaScript or vite in order to go through this sub-section. Having a basic understanding of <a target="_blank" href="https://nodejs.org/">Node.js</a> and <a target="_blank" href="https://www.npmjs.com/">npm</a> will suffice.</p>
<p>Just like any other project you've done in the previous sub-section, you'll begin by making a plan of how you want this application to run. In my opinion, the plan should be as follows:</p>
<ul>
<li>Get a good base image for running JavaScript applications, like <a target="_blank" href="https://hub.docker.com/_/node">node</a>.</li>
<li>Set the default working directory inside the image.</li>
<li>Copy the <code>package.json</code> file into the image.</li>
<li>Install necessary dependencies.</li>
<li>Copy the rest of the project files.</li>
<li>Start the <code>vite</code> development server by executing <code>npm run dev</code> command.</li>
</ul>
<p>This plan should always come from the developer of the application that you're containerizing. If you're the developer yourself, then you should already have a proper understanding of how this application needs to be run. </p>
<p>Now if you put the above mentioned plan inside <code>Dockerfile.dev</code>, the file should look like as follows:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-alpine

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>

<span class="hljs-keyword">USER</span> node

<span class="hljs-keyword">RUN</span><span class="bash"> mkdir -p /home/node/app</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /home/node/app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> ./package.json .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"npm"</span>, <span class="hljs-string">"run"</span>, <span class="hljs-string">"dev"</span> ]</span>
</code></pre>
<p>The explanation for this code is as follows:</p>
<ul>
<li>The <code>FROM</code> instruction here sets the official Node.js image as the base, giving you all the goodness of Node.js necessary to run any JavaScript application. The <code>lts-alpine</code> tag indicates that you want to use the Alpine variant, long term support version of the image. Available tags and necessary documentation for the image can be found on the <a target="_blank" href="https://hub.docker.com/_/node">node</a> hub page.</li>
<li>The <code>USER</code> instruction sets the default user for the image to <code>node</code>. By default Docker runs containers as the root user. But according to <a target="_blank" href="https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md">Docker and Node.js Best Practices</a> this can pose a security threat. So it's a better idea to run as a non-root user whenever possible. The node image comes with a non-root user named <code>node</code> which you can set as the default user using the <code>USER</code> instruction.</li>
<li>The <code>RUN mkdir -p /home/node/app</code> instruction creates a directory called <code>app</code> inside the home directory of the <code>node</code> user. The home directory for any non-root user in Linux is usually <code>/home/&lt;user name&gt;</code> by default.</li>
<li>Then the <code>WORKDIR</code> instruction sets the default working directory to the newly created <code>/home/node/app</code> directory. By default the working directory of any image is the root. You don't want any unnecessary files sprayed all over your root directory, do you? Hence you change the default working directory to something more sensible like <code>/home/node/app</code> or whatever you like. This working directory will be applicable to any subsequent <code>COPY</code>, <code>ADD</code>, <code>RUN</code> and <code>CMD</code> instructions.</li>
<li>The <code>COPY</code> instruction here copies the <code>package.json</code> file which contains information regarding all the necessary dependencies for this application. The <code>RUN</code> instruction executes the <code>npm install</code> command which is the default command for installing dependencies using a <code>package.json</code> file in Node.js projects. The <code>.</code> at the end represents the working directory.</li>
<li>The second <code>COPY</code> instruction copies the rest of the content from the current directory (<code>.</code>) of the host filesystem to the working directory (<code>.</code>) inside the image.</li>
<li>Finally, the <code>CMD</code> instruction here sets the default command for this image which is <code>npm run dev</code> written in <code>exec</code> form.</li>
<li>The <code>vite</code> development server by default runs on port <code>3000</code> , and adding an <code>EXPOSE</code> command seemed like a good idea, so there you go.</li>
</ul>
<p>Now, to build an image from this <code>Dockerfile.dev</code> you can execute the following command:</p>
<pre><code>docker image build --file Dockerfile.dev --tag hello-dock:dev .

# Step <span class="hljs-number">1</span>/<span class="hljs-number">7</span> : FROM node:lts
#  ---&gt; b90fa0d7cbd1
# Step <span class="hljs-number">2</span>/<span class="hljs-number">7</span> : EXPOSE <span class="hljs-number">3000</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">722</span>d639badc7
# Removing intermediate container <span class="hljs-number">722</span>d639badc7
#  ---&gt; e2a8aa88790e
# Step <span class="hljs-number">3</span>/<span class="hljs-number">7</span> : WORKDIR /app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">998e254</span>b4d22
# Removing intermediate container <span class="hljs-number">998e254</span>b4d22
#  ---&gt; <span class="hljs-number">6</span>bd4c42892a4
# Step <span class="hljs-number">4</span>/<span class="hljs-number">7</span> : COPY ./package.json .
#  ---&gt; <span class="hljs-number">24</span>fc5164a1dc
# Step <span class="hljs-number">5</span>/<span class="hljs-number">7</span> : RUN npm install
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">23</span>b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container <span class="hljs-number">23</span>b4de3f930b
#  ---&gt; c17ecb19a210
# Step <span class="hljs-number">6</span>/<span class="hljs-number">7</span> : COPY . .
#  ---&gt; afb6d9a1bc76
# Step <span class="hljs-number">7</span>/<span class="hljs-number">7</span> : CMD [ <span class="hljs-string">"npm"</span>, <span class="hljs-string">"run"</span>, <span class="hljs-string">"dev"</span> ]
#  ---&gt; Running <span class="hljs-keyword">in</span> a7ff529c28fe
# Removing intermediate container a7ff529c28fe
#  ---&gt; <span class="hljs-number">1792250</span>adb79
# Successfully built <span class="hljs-number">1792250</span>adb79
# Successfully tagged hello-dock:dev
</code></pre><p>Given the filename is not <code>Dockerfile</code> you have to explicitly pass the filename using the <code>--file</code> option. A container can be run using this image by executing the following command:</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish <span class="hljs-number">3000</span>:<span class="hljs-number">3000</span> \
    --name hello-dock-dev \
    hello-dock:dev

# <span class="hljs-number">21</span>b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268
</code></pre><p>Now visit <code>http://127.0.0.1:3000</code> to see the <code>hello-dock</code> application in action.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock-dev.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Congratulations on running your first real-world application inside a container. The code you've just written is okay but there is one big issue with it and a few places where it can be improved. Let's begin with the issue first.</p>
<h3 id="heading-how-to-work-with-bind-mounts-in-docker">How to Work With Bind Mounts in Docker</h3>
<p>If you've worked with any front-end JavaScript framework before, you should know that the development servers in these frameworks usually come with a hot reload feature. That is if you make a change in your code, the server will reload, automatically reflecting any changes you've made immediately.</p>
<p>But if you make any changes in your code right now, you'll see nothing happening to your application running in the browser. This is because you're making changes in the code that you have in your local file system but the application you're seeing in the browser resides inside the container file system.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/local-vs-container-file-system.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>To solve this issue, you can again make use of a <a target="_blank" href="https://docs.docker.com/storage/bind-mounts/">bind mount</a>. Using bind mounts, you can easily mount one of your local file system directories inside a container. Instead of making a copy of the local file system, the bind mount can reference the local file system directly from inside the container.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bind-mounts.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>This way, any changes you make to your local source code will reflect immediately inside the container,  triggering the hot reload feature of the <code>vite</code> development server. Changes made to the file system inside the container will be reflected on your local file system as well.</p>
<p>You've already learned in the <a class="post-section-overview" href="#working-with-executable-images">Working With Executable Images</a> sub-section, bind mounts can be created using the <code>--volume</code> or <code>-v</code> option for the <code>container run</code> or <code>container start</code> commands. Just to remind you, the generic syntax is as follows:</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre><p>Stop your previously started <code>hello-dock-dev</code> container, and start a new container by executing the following command:</p>
<pre><code>docker container run \
    --rm \
    --publish <span class="hljs-number">3000</span>:<span class="hljs-number">3000</span> \
    --name hello-dock-dev \
    --volume $(pwd):<span class="hljs-regexp">/home/</span>node/app \
    hello-dock:dev

# sh: <span class="hljs-number">1</span>: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> dev: <span class="hljs-string">`vite`</span>
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> dev script.
# npm ERR! This is probably not a problem <span class="hljs-keyword">with</span> npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?
</code></pre><p>Keep in mind, I've omitted the <code>--detach</code> option and that's to demonstrate a very important point. As you can see, the application is not running at all now.</p>
<p>That's because although the usage of a volume solves the issue of hot reloads, it introduces another problem. If you have any previous experience with Node.js, you may know that the dependencies of a Node.js project live inside the <code>node_modules</code> directory on the project root.</p>
<p>Now that you're mounting the project root on your local file system as a volume inside the container, the content inside the container gets replaced along with the <code>node_modules</code> directory containing all the dependencies. This means that the <code>vite</code> package has gone missing.</p>
<h3 id="heading-how-to-work-with-anonymous-volumes-in-docker">How to Work With Anonymous Volumes in Docker</h3>
<p>This problem can be solved using an anonymous volume. An anonymous volume is identical to a bind mount except that you don't need to specify the source directory here. The generic syntax for creating an anonymous volume is as follows:</p>
<pre><code>--volume &lt;container file system directory absolute path&gt;:<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">read</span> <span class="hljs-attr">write</span> <span class="hljs-attr">access</span>&gt;</span></span>
</code></pre><p>So the final command for starting the <code>hello-dock</code> container with both volumes should be as follows:</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish <span class="hljs-number">3000</span>:<span class="hljs-number">3000</span> \
    --name hello-dock-dev \
    --volume $(pwd):<span class="hljs-regexp">/home/</span>node/app \
    --volume /home/node/app/node_modules \
    hello-dock:dev

# <span class="hljs-number">53</span>d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b
</code></pre><p>Here, Docker will take the entire <code>node_modules</code> directory from inside the container and tuck it away in some other directory managed by the Docker daemon on your host file system and will mount that directory as <code>node_modules</code> inside the container.</p>
<h3 id="heading-how-to-perform-multi-staged-builds-in-docker">How to Perform Multi-Staged Builds in Docker</h3>
<p>So far in this section, you've built an image for running a JavaScript application in development mode. Now if you want to build the image in production mode, some new challenges show up. </p>
<p>In development mode the <code>npm run serve</code> command starts a development server that serves the application to the user. That server not only serves the files but also provides the hot reload feature.</p>
<p>In production mode, the <code>npm run build</code> command compiles all your JavaScript code into some static HTML, CSS, and JavaScript files. To run these files you don't need node or any other runtime dependencies. All you need is a server like <code>nginx</code> for example.</p>
<p>To create an image where the application runs in production mode, you can take the following steps:</p>
<ul>
<li>Use <code>node</code> as the base image and build the application.</li>
<li>Install <code>nginx</code> inside the node image and use that to serve the static files.</li>
</ul>
<p>This approach is completely valid. But the problem is that the <code>node</code> image is big and most of the stuff it carries is unnecessary to serve your static files. A better approach to this scenario is as follows:</p>
<ul>
<li>Use <code>node</code> image as the base and build the application.</li>
<li>Copy the files created using the <code>node</code> image to an <code>nginx</code> image.</li>
<li>Create the final image based on <code>nginx</code> and discard all <code>node</code> related stuff.</li>
</ul>
<p>This way your image only contains the files that are needed and becomes really handy. </p>
<p>This approach is a multi-staged build. To perform such a build, create a new <code>Dockerfile</code> inside your <code>hello-dock</code> project directory and put the following content in it:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:lts-alpine as builder

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> ./package.json ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm run build</span>

<span class="hljs-keyword">FROM</span> nginx:stable-alpine

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">80</span>

<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/dist /usr/share/nginx/html</span>
</code></pre>
<p>As you can see the <code>Dockerfile</code> looks a lot like your previous ones with a few oddities. The explanation for this file is as follows:</p>
<ul>
<li>Line 1 starts the first stage of the build using <code>node:lts-alpine</code> as the base image. The <code>as builder</code> syntax assigns a name to this stage so that it can be referred to later on.</li>
<li>From line 3 to line 9, it's standard stuff that you've seen many times before. The <code>RUN npm run build</code> command actually compiles the entire application and tucks it inside <code>/app/dist</code> directory where <code>/app</code> is the working directory and <code>/dist</code> is the default output directory for <code>vite</code> applications.</li>
<li>Line 11 starts the second stage of the build using <code>nginx:stable-alpine</code> as the base image.</li>
<li>The NGINX server runs on port 80 by default so the line <code>EXPOSE 80</code> is added.</li>
<li>The last line is a <code>COPY</code> instruction. The <code>--from=builder</code> part indicates that you want to copy some files from the <code>builder</code> stage. After that it's a standard copy instruction where <code>/app/dist</code> is the source and <code>/usr/share/nginx/html</code> is the destination. The destination used here is the default site path for NGINX so any static file you put inside there will be automatically served.</li>
</ul>
<p>As you can see, the resulting image is a <code>nginx</code> base image containing only the files necessary for running the application. To build this image execute the following command:</p>
<pre><code>docker image build --tag hello-dock:prod .

# Step <span class="hljs-number">1</span>/<span class="hljs-number">9</span> : FROM node:lts-alpine <span class="hljs-keyword">as</span> builder
#  ---&gt; <span class="hljs-number">72</span>aaced1868f
# Step <span class="hljs-number">2</span>/<span class="hljs-number">9</span> : WORKDIR /app
#  ---&gt; Running <span class="hljs-keyword">in</span> e361c5c866dd
# Removing intermediate container e361c5c866dd
#  ---&gt; <span class="hljs-number">241</span>b4b97b34c
# Step <span class="hljs-number">3</span>/<span class="hljs-number">9</span> : COPY ./package.json ./
#  ---&gt; <span class="hljs-number">6</span>c594c5d2300
# Step <span class="hljs-number">4</span>/<span class="hljs-number">9</span> : RUN npm install
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">6</span>dfabf0ee9f8
# npm WARN deprecated fsevents@<span class="hljs-number">2.1</span><span class="hljs-number">.3</span>: Please update to v <span class="hljs-number">2.2</span>.x
#
# &gt; esbuild@<span class="hljs-number">0.8</span><span class="hljs-number">.29</span> postinstall /app/node_modules/esbuild
# &gt; node install.js
#
# npm notice created a lockfile <span class="hljs-keyword">as</span> package-lock.json. You should commit <span class="hljs-built_in">this</span> file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~<span class="hljs-number">2.1</span><span class="hljs-number">.2</span> (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform <span class="hljs-keyword">for</span> fsevents@<span class="hljs-number">2.1</span><span class="hljs-number">.3</span>: wanted {<span class="hljs-string">"os"</span>:<span class="hljs-string">"darwin"</span>,<span class="hljs-string">"arch"</span>:<span class="hljs-string">"any"</span>} (current: {<span class="hljs-string">"os"</span>:<span class="hljs-string">"linux"</span>,<span class="hljs-string">"arch"</span>:<span class="hljs-string">"x64"</span>})
# npm WARN hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> No description
# npm WARN hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> No repository field.
# npm WARN hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> No license field.
#
# added <span class="hljs-number">327</span> packages <span class="hljs-keyword">from</span> <span class="hljs-number">301</span> contributors and audited <span class="hljs-number">329</span> packages <span class="hljs-keyword">in</span> <span class="hljs-number">35.971</span>s
#
# <span class="hljs-number">26</span> packages are looking <span class="hljs-keyword">for</span> funding
#   run <span class="hljs-string">`npm fund`</span> <span class="hljs-keyword">for</span> details
#
# found <span class="hljs-number">0</span> vulnerabilities
#
# Removing intermediate container <span class="hljs-number">6</span>dfabf0ee9f8
#  ---&gt; <span class="hljs-number">21</span>fd1b065314
# Step <span class="hljs-number">5</span>/<span class="hljs-number">9</span> : COPY . .
#  ---&gt; <span class="hljs-number">43243</span>f95bff7
# Step <span class="hljs-number">6</span>/<span class="hljs-number">9</span> : RUN npm run build
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">4</span>d918cf18584
#
# &gt; hello-dock@<span class="hljs-number">0.0</span><span class="hljs-number">.0</span> build /app
# &gt; vite build
#
# - Building production bundle...
#
# [write] dist/index.html <span class="hljs-number">0.39</span>kb, <span class="hljs-attr">brotli</span>: <span class="hljs-number">0.15</span>kb
# [write] dist/_assets/docker-handbook-github<span class="hljs-number">.3</span>adb4865.webp <span class="hljs-number">12.32</span>kb
# [write] dist/_assets/index.eabcae90.js <span class="hljs-number">42.56</span>kb, <span class="hljs-attr">brotli</span>: <span class="hljs-number">15.40</span>kb
# [write] dist/_assets/style<span class="hljs-number">.0637</span>ccc5.css <span class="hljs-number">0.16</span>kb, <span class="hljs-attr">brotli</span>: <span class="hljs-number">0.10</span>kb
# - Building production bundle...
#
# Build completed <span class="hljs-keyword">in</span> <span class="hljs-number">1.71</span>s.
#
# Removing intermediate container <span class="hljs-number">4</span>d918cf18584
#  ---&gt; <span class="hljs-number">187</span>fb3e82d0d
# Step <span class="hljs-number">7</span>/<span class="hljs-number">9</span> : EXPOSE <span class="hljs-number">80</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> b3aab5cf5975
# Removing intermediate container b3aab5cf5975
#  ---&gt; d6fcc058cfda
# Step <span class="hljs-number">8</span>/<span class="hljs-number">9</span> : FROM nginx:stable-alpine
# stable: Pulling <span class="hljs-keyword">from</span> library/nginx
# <span class="hljs-number">6</span>ec7b7d162b2: Already exists 
# <span class="hljs-number">43876</span>acb2da3: Pull complete 
# <span class="hljs-number">7</span>a79edd1e27b: Pull complete 
# eea03077c87e: Pull complete 
# eba7631b45c5: Pull complete 
# Digest: sha256:<span class="hljs-number">2</span>eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image <span class="hljs-keyword">for</span> nginx:stable
#  ---&gt; <span class="hljs-number">05</span>f64a802c26
# Step <span class="hljs-number">9</span>/<span class="hljs-number">9</span> : COPY --<span class="hljs-keyword">from</span>=builder /app/dist /usr/share/nginx/html
#  ---&gt; <span class="hljs-number">8</span>c6dfc34a10d
# Successfully built <span class="hljs-number">8</span>c6dfc34a10d
# Successfully tagged hello-dock:prod
</code></pre><p>Once the image has been built, you may run a new container by executing the following command:</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --name hello-dock-prod \
    --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> \
    hello-dock:prod

# <span class="hljs-number">224</span>aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953
</code></pre><p>The running application should be available on <code>http://127.0.0.1:8080</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Here you can see my <code>hello-dock</code> application in all its glory. Multi-staged builds can be very useful if you're building large applications with a lot of dependencies. If configured properly, images built in multiple stages can be very optimized and compact.</p>
<h3 id="heading-how-to-ignore-unnecessary-files">How to Ignore Unnecessary Files</h3>
<p>If you've been working with <code>git</code> for some time now, you may know about the <code>.gitignore</code> files in projects. These contain a list of files and directories to be excluded from the repository. </p>
<p>Well, Docker has a similar concept. The <code>.dockerignore</code> file contains a list of files and directories to be excluded from image builds. You can find a pre-created <code>.dockerignore</code> file in the <code>hello-dock</code> directory.</p>
<pre><code class="lang-dockerignore">.git
*Dockerfile*
*docker-compose*
node_modules
</code></pre>
<p>This <code>.dockerignore</code> file has to be in the build context. Files and directories mentioned here will be ignored by the <code>COPY</code> instruction. But if you do a bind mount, the <code>.dockerignore</code> file will have no effect. I've added <code>.dockerignore</code> files where necessary in the project repository.</p>
<h2 id="heading-network-manipulation-basics-in-docker">Network Manipulation Basics in Docker</h2>
<p>So far in this book, you've only worked with single container projects. But in real life, the majority of projects that you'll have to work with will have more than one container. And to be honest, working with a bunch of containers can be a little difficult if you don't understand the nuances of container isolation. </p>
<p>So in this section of the book, you'll get familiar with basic networking with Docker and you'll work hands on with a small multi-container project.</p>
<p>Well you've already learned in the previous section that containers are isolated environments. Now consider a scenario where you have a <code>notes-api</code> application powered by <a target="_blank" href="https://expressjs.com/">Express.js</a> and a <a target="_blank" href="https://www.postgresql.org/">PostgreSQL</a> database server running in two separate containers.</p>
<p>These two containers are completely isolated from each other and are oblivious to each other's existence. <strong>So how do you connect the two? Won't that be a challenge?</strong>‌</p>
<p>You may think of two possible solutions to this problem. They are as follows:</p>
<ul>
<li>Accessing the database server using an exposed port.</li>
<li>Accessing the database server using its IP address and default port.</li>
</ul>
<p>The first one involves exposing a port from the <code>postgres</code> container and the <code>notes-api</code> will connect through that. Assume that the exposed port from the <code>postgres</code> container is 5432. Now if you try to connect to <code>127.0.0.1:5432</code> from inside the <code>notes-api</code> container, you'll find that the <code>notes-api</code> can't find the database server at all.</p>
<p>The reason is that when you're saying <code>127.0.0.1</code> inside the <code>notes-api</code> container, you're simply referring to the <code>localhost</code> of that container and that container only. The <code>postgres</code> server simply doesn't exist there. As a result the <code>notes-api</code> application failed to connect.</p>
<p>The second solution you may think of is finding the exact IP address of the <code>postgres</code> container using the <code>container inspect</code> command and using that with the port. Assuming the name of the <code>postgres</code> container is <code>notes-api-db-server</code> you can easily get the IP address by executing the following command:</p>
<pre><code>docker container inspect --format=<span class="hljs-string">'{{range .NetworkSettings.Networks}} {{.IPAddress}} {{end}}'</span> notes-api-db-server

#  <span class="hljs-number">172.17</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>
</code></pre><p>Now given that the default port for <code>postgres</code> is <code>5432</code>, you can very easily access the database server by connecting to <code>172.17.0.2:5432</code> from the <code>notes-api</code> container.</p>
<p>There are problems in this approach as well. Using IP addresses to refer to a container is not recommended. Also, if the container gets destroyed and recreated, the IP address may change. Keeping track of these changing IP addresses can be pretty hectic.</p>
<p>Now that I've dismissed the possible wrong answers to the original question, the correct answer is, <strong>you connect them by putting them under a user-defined bridge network.</strong></p>
<h3 id="heading-docker-network-basics">Docker Network Basics</h3>
<p>A network in Docker is another logical object like a container and image. Just like the other two, there is a plethora of commands under the <code>docker network</code> group for manipulating networks. </p>
<p>To list out the networks in your system, execute the following command:</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# <span class="hljs-number">124</span>dccee067f   host      host      local
# <span class="hljs-number">506e3822</span>bf1f   none      <span class="hljs-literal">null</span>      local
</code></pre><p>You should see three networks in your system. Now look at the <code>DRIVER</code> column of the table here. These drivers are can be treated as the type of network. </p>
<p>By default, Docker has five networking drivers. They are as follows:</p>
<ul>
<li><code>bridge</code> - The default networking driver in Docker. This can be used when multiple containers are running in standard mode and need to communicate with each other.</li>
<li><code>host</code> - Removes the network isolation completely. Any container running under a <code>host</code> network is basically attached to the network of the host system.</li>
<li><code>none</code> - This driver disables networking for containers altogether. I haven't found any use-case for this yet.</li>
<li><code>overlay</code> - This is used for connecting multiple Docker daemons across computers and is out of the scope of this book.</li>
<li><code>macvlan</code> - Allows assignment of MAC addresses to containers, making them function like physical devices in a network.</li>
</ul>
<p>There are also third-party plugins that allow you to integrate Docker with specialized network stacks. Out of the five mentioned above, you'll only work with the <code>bridge</code> networking driver in this book.</p>
<h3 id="heading-how-to-create-a-user-defined-bridge-in-docker">How to Create a User-Defined Bridge in Docker</h3>
<p>Before you start creating your own bridge, I would like to take some time to discuss the default bridge network that comes with Docker. Let's begin by listing all the networks on your system:</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# <span class="hljs-number">124</span>dccee067f   host      host      local
# <span class="hljs-number">506e3822</span>bf1f   none      <span class="hljs-literal">null</span>      local
</code></pre><p>As you can see, Docker comes with a default bridge network named <code>bridge</code>. Any container you run will be automatically attached to this bridge network:</p>
<pre><code>docker container run --rm --detach --name hello-dock --publish <span class="hljs-number">8080</span>:<span class="hljs-number">80</span> fhsinchy/hello-dock
# a37f723dad3ae793ce40f97eb6bb236761baa92d72a2c27c24fc7fda0756657d

docker network inspect --format=<span class="hljs-string">'{{range .Containers}}{{.Name}}{{end}}'</span> bridge
# hello-dock
</code></pre><p>Containers attached to the default bridge network can communicate with each others using IP addresses which I have already discouraged in the previous sub-section.</p>
<p>A user-defined bridge, however, has some extra features over the default one. According to the official <a target="_blank" href="https://docs.docker.com/network/bridge/#differences-between-user-defined-bridges-and-the-default-bridge">docs</a> on this topic, some notable extra features are as follows:</p>
<ul>
<li><strong>User-defined bridges provide automatic DNS resolution between containers:</strong> This means containers attached to the same network can communicate with each others using the container name. So if you have two containers named <code>notes-api</code> and <code>notes-db</code> the API container will be able to connect to the database container using the <code>notes-db</code> name.</li>
<li><strong>User-defined bridges provide better isolation:</strong> All containers are attached to the default bridge network by default which can cause conflicts among them. Attaching containers to a user-defined bridge can ensure better isolation.</li>
<li><strong>Containers can be attached and detached from user-defined networks on the fly:</strong> During a container’s lifetime, you can connect or disconnect it from user-defined networks on the fly. To remove a container from the default bridge network, you need to stop the container and recreate it with different network options.</li>
</ul>
<p>Now that you've learned quite a lot about a user-defined network, it's time to create one for yourself. A network can be created using the <code>network create</code> command. The generic syntax for the command is as follows:</p>
<pre><code>docker network create &lt;network name&gt;
</code></pre><p>To create a network with the name <code>skynet</code> execute the following command:</p>
<pre><code>docker network create skynet

# <span class="hljs-number">7</span>bd5f351aa892ac6ec15fed8619fc3bbb95a7dcdd58980c28304627c8f7eb070

docker network ls

# NETWORK ID     NAME     DRIVER    SCOPE
# be0cab667c4b   bridge   bridge    local
# <span class="hljs-number">124</span>dccee067f   host     host      local
# <span class="hljs-number">506e3822</span>bf1f   none     <span class="hljs-literal">null</span>      local
# <span class="hljs-number">7</span>bd5f351aa89   skynet   bridge    local
</code></pre><p>As you can see a new network has been created with the given name. No container is currently attached to this network. In the next sub-section, you'll learn about attaching containers to a network.</p>
<h3 id="heading-how-to-attach-a-container-to-a-network-in-docker">How to Attach a Container to a Network in Docker</h3>
<p>There are mostly two ways of attaching a container to a network. First, you can use the network connect command to attach a container to a network. The generic syntax for the command is as follows:</p>
<pre><code>docker network connect &lt;network identifier&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">container</span> <span class="hljs-attr">identifier</span>&gt;</span></span>
</code></pre><p>To connect the <code>hello-dock</code> container to the <code>skynet</code> network, you can execute the following command:</p>
<pre><code>docker network connect skynet hello-dock

docker network inspect --format=<span class="hljs-string">'{{range .Containers}} {{.Name}} {{end}}'</span> skynet

#  hello-dock

docker network inspect --format=<span class="hljs-string">'{{range .Containers}} {{.Name}} {{end}}'</span> bridge

#  hello-dock
</code></pre><p>As you can see from the outputs of the two <code>network inspect</code> commands, the <code>hello-dock</code> container is now attached to both the <code>skynet</code> and the default <code>bridge</code> network.</p>
<p>The second way of attaching a container to a network is by using the <code>--network</code> option for the <code>container run</code> or <code>container create</code> commands. The generic syntax for the option is as follows:</p>
<pre><code>--network &lt;network identifier&gt;
</code></pre><p>To run another <code>hello-dock</code> container attached to the same network, you can execute the following command:</p>
<pre><code>docker container run --network skynet --rm --name alpine-box -it alpine sh

# lands you into alpine linux shell

/ # ping hello-dock

# PING hello-dock (<span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>): <span class="hljs-number">56</span> data bytes
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">0</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.191</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">1</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.103</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">2</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.139</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">3</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.142</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">4</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.146</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">5</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.095</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">6</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.181</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">7</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.138</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">8</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.158</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">9</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.137</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">10</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.145</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">11</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.138</span> ms
# <span class="hljs-number">64</span> bytes <span class="hljs-keyword">from</span> <span class="hljs-number">172.18</span><span class="hljs-number">.0</span><span class="hljs-number">.2</span>: seq=<span class="hljs-number">12</span> ttl=<span class="hljs-number">64</span> time=<span class="hljs-number">0.085</span> ms

--- hello-dock ping statistics ---
<span class="hljs-number">13</span> packets transmitted, <span class="hljs-number">13</span> packets received, <span class="hljs-number">0</span>% packet loss
round-trip min/avg/max = <span class="hljs-number">0.085</span>/<span class="hljs-number">0.138</span>/<span class="hljs-number">0.191</span> ms
</code></pre><p>As you can see, running <code>ping hello-dock</code> from inside the <code>alpine-box</code> container works because both of the containers are under the same user-defined bridge network and automatic DNS resolution is working.</p>
<p>Keep in mind, though, that in order for the automatic DNS resolution to work you must assign custom names to the containers. Using the randomly generated name will not work.</p>
<h3 id="heading-how-to-detach-containers-from-a-network-in-docker">How to Detach Containers from a Network in Docker</h3>
<p>In the previous sub-section you learned about attaching containers to a network. In this sub-section, you'll learn about how to detach them. </p>
<p>You can use the <code>network disconnect</code> command for this task. The generic syntax for the command is as follows:</p>
<pre><code>docker network disconnect &lt;network identifier&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">container</span> <span class="hljs-attr">identifier</span>&gt;</span></span>
</code></pre><p>To detach the <code>hello-dock</code> container from the <code>skynet</code> network, you can execute the following command:</p>
<pre><code>docker network disconnect skynet hello-dock
</code></pre><p>Just like the <code>network connect</code> command, the <code>network disconnect</code> command doesn't give any output.</p>
<h3 id="heading-how-to-get-rid-of-networks-in-docker">How to Get Rid of Networks in Docker</h3>
<p>Just like the other logical objects in Docker, networks can be removed using the <code>network rm</code> command. The generic syntax for the command is as follows:</p>
<pre><code>docker network rm &lt;network identifier&gt;
</code></pre><p>To remove the <code>skynet</code> network from your system, you can execute the following command:</p>
<pre><code>docker network rm skynet
</code></pre><p>You can also use the <code>network prune</code> command to remove any unused networks from your system. The command also has the <code>-f</code> or <code>--force</code> and <code>-a</code> or <code>--all</code> options.</p>
<h2 id="heading-how-to-containerize-a-multi-container-javascript-application">How to Containerize a Multi-Container JavaScript Application</h2>
<p>Now that you've learned enough about networks in Docker, in this section you'll learn to containerize a full-fledged multi-container project. The project you'll be working with is a simple <code>notes-api</code> powered by Express.js and PostgreSQL.</p>
<p>In this project there are two containers in total that you'll have to connect using a network. Apart from this, you'll also learn about concepts like environment variables and named volumes. So without further ado, let's jump right in.</p>
<h3 id="heading-how-to-run-the-database-server">How to Run the Database Server</h3>
<p>The database server in this project is a simple PostgreSQL server and uses the official <a target="_blank" href="https://hub.docker.com/_/postgres">postgres</a> image. </p>
<p>According to the official docs, in order to run a container with this image, you must provide the <code>POSTGRES_PASSWORD</code> environment variable. Apart from this one, I'll also provide a name for the default database using the <code>POSTGRES_DB</code> environment variable. PostgreSQL by default listens on port <code>5432</code>, so you need to publish that as well.</p>
<p>To run the database server you can execute the following command:</p>
<pre><code>docker container run \
    --detach \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    <span class="hljs-attr">postgres</span>:<span class="hljs-number">12</span>

# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc

docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS              PORTS      NAMES
# a7b287d34d96   postgres:<span class="hljs-number">12</span>   <span class="hljs-string">"docker-entrypoint.s…"</span>   About a minute ago   Up About a minute   <span class="hljs-number">5432</span>/tcp   notes-db
</code></pre><p>The <code>--env</code> option for the <code>container run</code> and <code>container create</code> commands can be used for providing environment variables to a container. As you can see, the database container has been created successfully and is running now.</p>
<p>Although the container is running, there is a small problem. Databases like PostgreSQL, MongoDB, and MySQL persist their data in a directory. PostgreSQL uses the <code>/var/lib/postgresql/data</code> directory inside the container to persist data. </p>
<p>Now what if the container gets destroyed for some reason? You'll lose all your data. To solve this problem, a named volume can be used.</p>
<h3 id="heading-how-to-work-with-named-volumes-in-docker">How to Work with Named Volumes in Docker</h3>
<p>Previously you've worked with bind mounts and anonymous volumes. A named volume is very similar to an anonymous volume except that you can refer to a named volume using its name. </p>
<p>Volumes are also logical objects in Docker and can be manipulated using the command-line. The <code>volume create</code> command can be used for creating a named volume.</p>
<p>The generic syntax for the command is as follows:</p>
<pre><code>docker volume create &lt;volume name&gt;
</code></pre><p>To create a volume named <code>notes-db-data</code> you can execute the following command:</p>
<pre><code>docker volume create notes-db-data

# notes-db-data

docker volume ls

# DRIVER    VOLUME NAME
# local     notes-db-data
</code></pre><p>This volume can now be mounted to <code>/var/lib/postgresql/data</code> inside the <code>notes-db</code> container. To do so, stop and remove the <code>notes-db</code> container:</p>
<pre><code>docker container stop notes-db

# notes-db

docker container rm notes-db

# notes-db
</code></pre><p>Now run a new container and assign the volume using the <code>--volume</code> or <code>-v</code> option.</p>
<pre><code>docker container run \
    --detach \
    --volume notes-db-data:<span class="hljs-regexp">/var/</span>lib/postgresql/data \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    <span class="hljs-attr">postgres</span>:<span class="hljs-number">12</span>

# <span class="hljs-number">37755e86</span>d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791
</code></pre><p>Now inspect the <code>notes-db</code> container to make sure that the mounting was successful:</p>
<pre><code>docker container inspect --format=<span class="hljs-string">'{{range .Mounts}} {{ .Name }} {{end}}'</span> notes-db

#  notes-db-data
</code></pre><p>Now the data will safely be stored inside the <code>notes-db-data</code> volume and can be reused in the future. A bind mount can also be used instead of a named volume here, but I prefer a named volume in such scenarios.</p>
<h3 id="heading-how-to-access-logs-from-a-container-in-docker">How to Access Logs from a Container in Docker</h3>
<p>In order to see the logs from a container, you can use the <code>container logs</code> command. The generic syntax for the command is as follows:</p>
<pre><code>docker container logs &lt;container identifier&gt;
</code></pre><p>To access the logs from the <code>notes-db</code> container, you can execute the following command:</p>
<pre><code>docker container logs notes-db

# The files belonging to <span class="hljs-built_in">this</span> database system will be owned by user <span class="hljs-string">"postgres"</span>.
# This user must also own the server process.

# The database cluster will be initialized <span class="hljs-keyword">with</span> locale <span class="hljs-string">"en_US.utf8"</span>.
# The <span class="hljs-keyword">default</span> database encoding has accordingly been set to <span class="hljs-string">"UTF8"</span>.
# The <span class="hljs-keyword">default</span> text search configuration will be set to <span class="hljs-string">"english"</span>.
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /<span class="hljs-keyword">var</span>/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting <span class="hljs-keyword">default</span> max_connections ... <span class="hljs-number">100</span>
# selecting <span class="hljs-keyword">default</span> shared_buffers ... <span class="hljs-number">128</span>MB
# selecting <span class="hljs-keyword">default</span> time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
#     pg_ctl -D /<span class="hljs-keyword">var</span>/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling <span class="hljs-string">"trust"</span> authentication <span class="hljs-keyword">for</span> local connections
# You can change <span class="hljs-built_in">this</span> by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting <span class="hljs-keyword">for</span> server to start...<span class="hljs-number">.2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">13</span>:<span class="hljs-number">39</span>:<span class="hljs-number">21.613</span> UTC [<span class="hljs-number">47</span>] LOG:  starting PostgreSQL <span class="hljs-number">12.5</span> (Debian <span class="hljs-number">12.5</span><span class="hljs-number">-1.</span>pgdg100+<span class="hljs-number">1</span>) on x86_64-pc-linux-gnu, compiled by gcc (Debian <span class="hljs-number">8.3</span><span class="hljs-number">.0</span><span class="hljs-number">-6</span>) <span class="hljs-number">8.3</span><span class="hljs-number">.0</span>, <span class="hljs-number">64</span>-bit
# <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">13</span>:<span class="hljs-number">39</span>:<span class="hljs-number">21.621</span> UTC [<span class="hljs-number">47</span>] LOG:  listening on Unix socket <span class="hljs-string">"/var/run/postgresql/.s.PGSQL.5432"</span>
# <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">13</span>:<span class="hljs-number">39</span>:<span class="hljs-number">21.675</span> UTC [<span class="hljs-number">48</span>] LOG:  database system was shut down at <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">13</span>:<span class="hljs-number">39</span>:<span class="hljs-number">21</span> UTC
# <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">13</span>:<span class="hljs-number">39</span>:<span class="hljs-number">21.685</span> UTC [<span class="hljs-number">47</span>] LOG:  database system is ready to accept connections
#  done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d<span class="hljs-comment">/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG:  received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG:  aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG:  shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG:  database system is shut down
#  done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG:  database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG:  database system is ready to accept connections</span>
</code></pre><p>Evident by the text in line 57, the database is up and ready to accept connections from the outside. There is also the <code>--follow</code> or <code>-f</code> option for the command which lets you attach the console to the logs output and get a continuous stream of text.</p>
<h3 id="heading-how-to-create-a-network-and-attaching-the-database-server-in-docker">How to Create a Network and Attaching the Database Server in Docker</h3>
<p>As you've learned in the previous section, the containers have to be attached to a user-defined bridge network in order to communicate with each other using container names. To do so, create a network named <code>notes-api-network</code> in your system:</p>
<pre><code>docker network create notes-api-network
</code></pre><p>Now attach the <code>notes-db</code> container to this network by executing the following command:</p>
<pre><code>docker network connect notes-api-network notes-db
</code></pre><h3 id="heading-how-to-write-the-dockerfile">How to Write the Dockerfile</h3>
<p>Go to the directory where you've cloned the project code. Inside there, go inside the <code>notes-api/api</code> directory, and create a new <code>Dockerfile</code>. Put the following code in the file:</p>
<pre><code># stage one
FROM node:lts-alpine <span class="hljs-keyword">as</span> builder

# install dependencies <span class="hljs-keyword">for</span> node-gyp
RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install --only=prod

# stage two
FROM node:lts-alpine

EXPOSE <span class="hljs-number">3000</span>
ENV NODE_ENV=production

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --<span class="hljs-keyword">from</span>=builder /app/node_modules  /home/node/app/node_modules

CMD [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"bin/www"</span> ]
</code></pre><p>This is a multi-staged build. The first stage is used for building and installing the dependencies using <code>node-gyp</code> and the second stage is for running the application. I'll go through the steps briefly:</p>
<ul>
<li>Stage 1 uses <code>node:lts-alpine</code> as its base and uses <code>builder</code> as the stage name.</li>
<li>On line 5, we install <code>python</code>, <code>make</code>, and <code>g++</code>. The <code>node-gyp</code> tool requires these three packages to run.</li>
<li>On line 7, we set <code>/app</code> directory as the <code>WORKDIR</code> .</li>
<li>On line 9 and 10, we copy the <code>package.json</code> file to the <code>WORKDIR</code> and install all the dependencies.</li>
<li>Stage 2 also uses <code>node-lts:alpine</code> as the base.</li>
<li>On line 16, we set the <code>NODE_ENV</code> environment variable to <code>production</code>. This is important for the API to run properly.</li>
<li>From line 18 to line 20, we set the default user to <code>node</code>, create the <code>/home/node/app</code> directory, and set that as the <code>WORKDIR</code>.</li>
<li>On line 22, we copy all the project files and on line 23 we copy the <code>node_modules</code> directory from the <code>builder</code> stage. This directory contains all the built dependencies necessary for running the application.</li>
<li>On line 25, we set the default command.</li>
</ul>
<p>To build an image from this <code>Dockerfile</code>, you can execute the following command:</p>
<pre><code>docker image build --tag notes-api .

# Sending build context to Docker daemon  <span class="hljs-number">37.38</span>kB
# Step <span class="hljs-number">1</span>/<span class="hljs-number">14</span> : FROM node:lts-alpine <span class="hljs-keyword">as</span> builder
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">2</span>/<span class="hljs-number">14</span> : RUN apk add --no-cache python make g++
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">5</span>f20a0ecc04b
# fetch http:<span class="hljs-comment">//dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz</span>
# fetch http:<span class="hljs-comment">//dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz</span>
# (<span class="hljs-number">1</span>/<span class="hljs-number">21</span>) Installing binutils (<span class="hljs-number">2.33</span><span class="hljs-number">.1</span>-r0)
# (<span class="hljs-number">2</span>/<span class="hljs-number">21</span>) Installing gmp (<span class="hljs-number">6.1</span><span class="hljs-number">.2</span>-r1)
# (<span class="hljs-number">3</span>/<span class="hljs-number">21</span>) Installing isl (<span class="hljs-number">0.18</span>-r0)
# (<span class="hljs-number">4</span>/<span class="hljs-number">21</span>) Installing libgomp (<span class="hljs-number">9.3</span><span class="hljs-number">.0</span>-r0)
# (<span class="hljs-number">5</span>/<span class="hljs-number">21</span>) Installing libatomic (<span class="hljs-number">9.3</span><span class="hljs-number">.0</span>-r0)
# (<span class="hljs-number">6</span>/<span class="hljs-number">21</span>) Installing mpfr4 (<span class="hljs-number">4.0</span><span class="hljs-number">.2</span>-r1)
# (<span class="hljs-number">7</span>/<span class="hljs-number">21</span>) Installing mpc1 (<span class="hljs-number">1.1</span><span class="hljs-number">.0</span>-r1)
# (<span class="hljs-number">8</span>/<span class="hljs-number">21</span>) Installing gcc (<span class="hljs-number">9.3</span><span class="hljs-number">.0</span>-r0)
# (<span class="hljs-number">9</span>/<span class="hljs-number">21</span>) Installing musl-dev (<span class="hljs-number">1.1</span><span class="hljs-number">.24</span>-r3)
# (<span class="hljs-number">10</span>/<span class="hljs-number">21</span>) Installing libc-dev (<span class="hljs-number">0.7</span><span class="hljs-number">.2</span>-r0)
# (<span class="hljs-number">11</span>/<span class="hljs-number">21</span>) Installing g++ (<span class="hljs-number">9.3</span><span class="hljs-number">.0</span>-r0)
# (<span class="hljs-number">12</span>/<span class="hljs-number">21</span>) Installing make (<span class="hljs-number">4.2</span><span class="hljs-number">.1</span>-r2)
# (<span class="hljs-number">13</span>/<span class="hljs-number">21</span>) Installing libbz2 (<span class="hljs-number">1.0</span><span class="hljs-number">.8</span>-r1)
# (<span class="hljs-number">14</span>/<span class="hljs-number">21</span>) Installing expat (<span class="hljs-number">2.2</span><span class="hljs-number">.9</span>-r1)
# (<span class="hljs-number">15</span>/<span class="hljs-number">21</span>) Installing libffi (<span class="hljs-number">3.2</span><span class="hljs-number">.1</span>-r6)
# (<span class="hljs-number">16</span>/<span class="hljs-number">21</span>) Installing gdbm (<span class="hljs-number">1.13</span>-r1)
# (<span class="hljs-number">17</span>/<span class="hljs-number">21</span>) Installing ncurses-terminfo-base (<span class="hljs-number">6.1</span>_p20200118-r4)
# (<span class="hljs-number">18</span>/<span class="hljs-number">21</span>) Installing ncurses-libs (<span class="hljs-number">6.1</span>_p20200118-r4)
# (<span class="hljs-number">19</span>/<span class="hljs-number">21</span>) Installing readline (<span class="hljs-number">8.0</span><span class="hljs-number">.1</span>-r0)
# (<span class="hljs-number">20</span>/<span class="hljs-number">21</span>) Installing sqlite-libs (<span class="hljs-number">3.30</span><span class="hljs-number">.1</span>-r2)
# (<span class="hljs-number">21</span>/<span class="hljs-number">21</span>) Installing python2 (<span class="hljs-number">2.7</span><span class="hljs-number">.18</span>-r0)
# Executing busybox<span class="hljs-number">-1.31</span><span class="hljs-number">.1</span>-r9.trigger
# OK: <span class="hljs-number">212</span> MiB <span class="hljs-keyword">in</span> <span class="hljs-number">37</span> packages
# Removing intermediate container <span class="hljs-number">5</span>f20a0ecc04b
#  ---&gt; <span class="hljs-number">637</span>ca797d709
# Step <span class="hljs-number">3</span>/<span class="hljs-number">14</span> : WORKDIR /app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">846361</span>b57599
# Removing intermediate container <span class="hljs-number">846361</span>b57599
#  ---&gt; <span class="hljs-number">3</span>d58a482896e
# Step <span class="hljs-number">4</span>/<span class="hljs-number">14</span> : COPY ./package.json .
#  ---&gt; <span class="hljs-number">11</span>b387794039
# Step <span class="hljs-number">5</span>/<span class="hljs-number">14</span> : RUN npm install --only=prod
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">2e27</span>e33f935d
#  added <span class="hljs-number">269</span> packages <span class="hljs-keyword">from</span> <span class="hljs-number">220</span> contributors and audited <span class="hljs-number">1137</span> packages <span class="hljs-keyword">in</span> <span class="hljs-number">140.322</span>s
#
# <span class="hljs-number">4</span> packages are looking <span class="hljs-keyword">for</span> funding
#   run <span class="hljs-string">`npm fund`</span> <span class="hljs-keyword">for</span> details
#
# found <span class="hljs-number">0</span> vulnerabilities
#
# Removing intermediate container <span class="hljs-number">2e27</span>e33f935d
#  ---&gt; eb7cb2cb0b20
# Step <span class="hljs-number">6</span>/<span class="hljs-number">14</span> : FROM node:lts-alpine
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">7</span>/<span class="hljs-number">14</span> : EXPOSE <span class="hljs-number">3000</span>
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">4</span>ea24f871747
# Removing intermediate container <span class="hljs-number">4</span>ea24f871747
#  ---&gt; <span class="hljs-number">1</span>f0206f2f050
# Step <span class="hljs-number">8</span>/<span class="hljs-number">14</span> : ENV NODE_ENV=production
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">5</span>d40d6ac3b7e
# Removing intermediate container <span class="hljs-number">5</span>d40d6ac3b7e
#  ---&gt; <span class="hljs-number">31</span>f62da17929
# Step <span class="hljs-number">9</span>/<span class="hljs-number">14</span> : USER node
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">0963e1</span>fb19a0
# Removing intermediate container <span class="hljs-number">0963e1</span>fb19a0
#  ---&gt; <span class="hljs-number">0</span>f4045152b1c
# Step <span class="hljs-number">10</span>/<span class="hljs-number">14</span> : RUN mkdir -p /home/node/app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>ac591b3adbd
# Removing intermediate container <span class="hljs-number">0</span>ac591b3adbd
#  ---&gt; <span class="hljs-number">5908373</span>dfc75
# Step <span class="hljs-number">11</span>/<span class="hljs-number">14</span> : WORKDIR /home/node/app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">55253</span>b62ff57
# Removing intermediate container <span class="hljs-number">55253</span>b62ff57
#  ---&gt; <span class="hljs-number">2883</span>cdb7c77a
# Step <span class="hljs-number">12</span>/<span class="hljs-number">14</span> : COPY . .
#  ---&gt; <span class="hljs-number">8e60893</span>a7142
# Step <span class="hljs-number">13</span>/<span class="hljs-number">14</span> : COPY --<span class="hljs-keyword">from</span>=builder /app/node_modules  /home/node/app/node_modules
#  ---&gt; <span class="hljs-number">27</span>a85faa4342
# Step <span class="hljs-number">14</span>/<span class="hljs-number">14</span> : CMD [ <span class="hljs-string">"node"</span>, <span class="hljs-string">"bin/www"</span> ]
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">349</span>c8ca6dd3e
# Removing intermediate container <span class="hljs-number">349</span>c8ca6dd3e
#  ---&gt; <span class="hljs-number">9</span>ea100571585
# Successfully built <span class="hljs-number">9</span>ea100571585
# Successfully tagged notes-api:latest
</code></pre><p>Before you run a container using this image, make sure the database container is running, and is attached to the <code>notes-api-network</code>.</p>
<pre><code>docker container inspect notes-db

# [
#     {
#         ...
#         <span class="hljs-string">"State"</span>: {
#             <span class="hljs-string">"Status"</span>: <span class="hljs-string">"running"</span>,
#             <span class="hljs-string">"Running"</span>: <span class="hljs-literal">true</span>,
#             <span class="hljs-string">"Paused"</span>: <span class="hljs-literal">false</span>,
#             <span class="hljs-string">"Restarting"</span>: <span class="hljs-literal">false</span>,
#             <span class="hljs-string">"OOMKilled"</span>: <span class="hljs-literal">false</span>,
#             <span class="hljs-string">"Dead"</span>: <span class="hljs-literal">false</span>,
#             <span class="hljs-string">"Pid"</span>: <span class="hljs-number">11521</span>,
#             <span class="hljs-string">"ExitCode"</span>: <span class="hljs-number">0</span>,
#             <span class="hljs-string">"Error"</span>: <span class="hljs-string">""</span>,
#             <span class="hljs-string">"StartedAt"</span>: <span class="hljs-string">"2021-01-26T06:55:44.928510218Z"</span>,
#             <span class="hljs-string">"FinishedAt"</span>: <span class="hljs-string">"2021-01-25T14:19:31.316854657Z"</span>
#         },
#         ...
#         <span class="hljs-string">"Mounts"</span>: [
#             {
#                 <span class="hljs-string">"Type"</span>: <span class="hljs-string">"volume"</span>,
#                 <span class="hljs-string">"Name"</span>: <span class="hljs-string">"notes-db-data"</span>,
#                 <span class="hljs-string">"Source"</span>: <span class="hljs-string">"/var/lib/docker/volumes/notes-db-data/_data"</span>,
#                 <span class="hljs-string">"Destination"</span>: <span class="hljs-string">"/var/lib/postgresql/data"</span>,
#                 <span class="hljs-string">"Driver"</span>: <span class="hljs-string">"local"</span>,
#                 <span class="hljs-string">"Mode"</span>: <span class="hljs-string">"z"</span>,
#                 <span class="hljs-string">"RW"</span>: <span class="hljs-literal">true</span>,
#                 <span class="hljs-string">"Propagation"</span>: <span class="hljs-string">""</span>
#             }
#         ],
#         ...
#         <span class="hljs-string">"NetworkSettings"</span>: {
#             ...
#             <span class="hljs-string">"Networks"</span>: {
#                 <span class="hljs-string">"bridge"</span>: {
#                     <span class="hljs-string">"IPAMConfig"</span>: <span class="hljs-literal">null</span>,
#                     <span class="hljs-string">"Links"</span>: <span class="hljs-literal">null</span>,
#                     <span class="hljs-string">"Aliases"</span>: <span class="hljs-literal">null</span>,
#                     <span class="hljs-string">"NetworkID"</span>: <span class="hljs-string">"e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab"</span>,
#                     <span class="hljs-string">"EndpointID"</span>: <span class="hljs-string">"2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371"</span>,
#                     <span class="hljs-string">"Gateway"</span>: <span class="hljs-string">"172.17.0.1"</span>,
#                     <span class="hljs-string">"IPAddress"</span>: <span class="hljs-string">"172.17.0.2"</span>,
#                     <span class="hljs-string">"IPPrefixLen"</span>: <span class="hljs-number">16</span>,
#                     <span class="hljs-string">"IPv6Gateway"</span>: <span class="hljs-string">""</span>,
#                     <span class="hljs-string">"GlobalIPv6Address"</span>: <span class="hljs-string">""</span>,
#                     <span class="hljs-string">"GlobalIPv6PrefixLen"</span>: <span class="hljs-number">0</span>,
#                     <span class="hljs-string">"MacAddress"</span>: <span class="hljs-string">"02:42:ac:11:00:02"</span>,
#                     <span class="hljs-string">"DriverOpts"</span>: <span class="hljs-literal">null</span>
#                 },
#                 <span class="hljs-string">"notes-api-network"</span>: {
#                     <span class="hljs-string">"IPAMConfig"</span>: {},
#                     <span class="hljs-string">"Links"</span>: <span class="hljs-literal">null</span>,
#                     <span class="hljs-string">"Aliases"</span>: [
#                         <span class="hljs-string">"37755e86d627"</span>
#                     ],
#                     <span class="hljs-string">"NetworkID"</span>: <span class="hljs-string">"06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea"</span>,
#                     <span class="hljs-string">"EndpointID"</span>: <span class="hljs-string">"5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c"</span>,
#                     <span class="hljs-string">"Gateway"</span>: <span class="hljs-string">"172.18.0.1"</span>,
#                     <span class="hljs-string">"IPAddress"</span>: <span class="hljs-string">"172.18.0.2"</span>,
#                     <span class="hljs-string">"IPPrefixLen"</span>: <span class="hljs-number">16</span>,
#                     <span class="hljs-string">"IPv6Gateway"</span>: <span class="hljs-string">""</span>,
#                     <span class="hljs-string">"GlobalIPv6Address"</span>: <span class="hljs-string">""</span>,
#                     <span class="hljs-string">"GlobalIPv6PrefixLen"</span>: <span class="hljs-number">0</span>,
#                     <span class="hljs-string">"MacAddress"</span>: <span class="hljs-string">"02:42:ac:12:00:02"</span>,
#                     <span class="hljs-string">"DriverOpts"</span>: {}
#                 }
#             }
#         }
#     }
# ]
</code></pre><p>I've shortened the output for easy viewing here. On my system, the <code>notes-db</code> container is running, uses the <code>notes-db-data</code> volume, and is attached to the <code>notes-api-network</code> bridge.</p>
<p>Once you're assured that everything is in place, you can run a new container by executing the following command:</p>
<pre><code>docker container run \
    --detach \
    --name=notes-api \
    --env DB_HOST=notes-db \
    --env DB_DATABASE=notesdb \
    --env DB_PASSWORD=secret \
    --publish=<span class="hljs-number">3000</span>:<span class="hljs-number">3000</span> \
    --network=notes-api-network \
    notes-api

# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919
</code></pre><p>You should be able to understand this long command by yourself, so I'll go through the environment variables briefly. </p>
<p>The <code>notes-api</code> application requires three environment variables to be set. They are as follows:</p>
<ul>
<li><code>DB_HOST</code> - This is the host of the database server. Given that both the database server and the API are attached to the same user-defined bridge network, the database server can be refereed to using its container name which is <code>notes-db</code> in this case.</li>
<li><code>DB_DATABASE</code> - The database that this API will use. On <a target="_blank" href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS2MtB5zjVVjK3Ujaz4/containerizing-a-multi-container-javascript-application#running-the-database-server">Running the Database Server</a> we set the default database name to <code>notesdb</code> using the <code>POSTGRES_DB</code> environment variable. We'll use that here.</li>
<li><code>DB_PASSWORD</code> - Password for connecting to the database. This was also set on <a target="_blank" href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS2MtB5zjVVjK3Ujaz4/containerizing-a-multi-container-javascript-application#running-the-database-server">Running the Database Server</a> sub-section using the <code>POSTGRES_PASSWORD</code> environment variable.</li>
</ul>
<p>To check if the container is running properly or not, you can use the <code>container ls</code> command:</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
# f9ece420872d   notes-api     <span class="hljs-string">"docker-entrypoint.s…"</span>   <span class="hljs-number">12</span> minutes ago   Up <span class="hljs-number">12</span> minutes   <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">3000</span>-&gt;<span class="hljs-number">3000</span>/tcp   notes-api
# <span class="hljs-number">37755e86</span>d627   postgres:<span class="hljs-number">12</span>   <span class="hljs-string">"docker-entrypoint.s…"</span>   <span class="hljs-number">17</span> hours ago     Up <span class="hljs-number">14</span> minutes   <span class="hljs-number">5432</span>/tcp                 notes-db
</code></pre><p>The container is running now. You can visit <code>http://127.0.0.1:3000/</code> to see the API in action.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bonjour-mon-ami.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The API has five routes in total that you can see inside the <code>/notes-api/api/api/routes/notes.js</code> file.</p>
<p>Although the container is running, there is one last thing that you'll have to do before you can start using it. You'll have to run the database migration necessary for setting up the database tables, and you can do that by executing <code>npm run db:migrate</code> command inside the container.</p>
<h3 id="heading-how-to-execute-commands-in-a-running-container">How to Execute Commands in a Running Container</h3>
<p>You've already learned about executing commands in a stopped container. Another scenario is executing a command inside a running container.</p>
<p>For this, you'll have to use the <code>exec</code> command to execute a custom command inside a running container.</p>
<p>The generic syntax for the <code>exec</code> command is as follows:</p>
<pre><code>docker container exec &lt;container identifier&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">command</span>&gt;</span></span>
</code></pre><p>To execute <code>npm run db:migrate</code> inside the <code>notes-api</code> container, you can execute the following command:</p>
<pre><code>docker container exec notes-api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
#
# Using environment: production
# Batch <span class="hljs-number">1</span> run: <span class="hljs-number">1</span> migrations
</code></pre><p>In cases where you want to run an interactive command inside a running container, you'll have to use the <code>-it</code> flag. As an example, if you want to access the shell running inside the <code>notes-api</code> container, you can execute following the command:</p>
<pre><code>docker container exec -it notes-api sh

# / # uname -a
# Linux b5b1367d6b31 <span class="hljs-number">5.10</span><span class="hljs-number">.9</span><span class="hljs-number">-201.</span>fc33.x86_64 #<span class="hljs-number">1</span> SMP Wed Jan <span class="hljs-number">20</span> <span class="hljs-number">16</span>:<span class="hljs-number">56</span>:<span class="hljs-number">23</span> UTC <span class="hljs-number">2021</span> x86_64 Linux
</code></pre><h3 id="heading-how-to-write-management-scripts-in-docker">How to Write Management Scripts in Docker</h3>
<p>Managing a multi-container project along with the network and volumes and stuff means writing a lot of commands. To simplify the process, I usually have help from simple <a target="_blank" href="https://opensource.com/article/17/1/getting-started-shell-scripting">shell scripts</a> and a <a target="_blank" href="https://opensource.com/article/18/8/what-how-makefile">Makefile</a>. </p>
<p>You'll find four shell scripts in the <code>notes-api</code> directory. They are as follows:</p>
<ul>
<li><code>boot.sh</code> - Used for starting the containers if they already exist.</li>
<li><code>build.sh</code> - Creates and runs the containers. It also creates the images, volumes, and networks if necessary.</li>
<li><code>destroy.sh</code> - Removes all containers, volumes and networks associated with this project.</li>
<li><code>stop.sh</code> - Stops all running containers.</li>
</ul>
<p>There is also a <code>Makefile</code> that contains four targets named <code>start</code>, <code>stop</code>, <code>build</code> and <code>destroy</code>, each invoking the previously mentioned shell scripts.</p>
<p>If the container is in a running state in your system, executing <code>make stop</code> should stop all the containers. Executing <code>make destroy</code> should stop the containers and remove everything. Make sure you're running the scripts inside the <code>notes-api</code> directory:</p>
<pre><code>make destroy

# ./shutdown.sh
# stopping api container ---&gt;
# notes-api
# api container stopped ---&gt;

# stopping db container ---&gt;
# notes-db
# db container stopped ---&gt;

# shutdown script finished

# ./destroy.sh
# removing api container ---&gt;
# notes-api
# api container removed ---&gt;

# removing db container ---&gt;
# notes-db
# db container removed ---&gt;

# removing db data volume ---&gt;
# notes-db-data
# db data volume removed ---&gt;

# removing network ---&gt;
# notes-api-network
# network removed ---&gt;

# destroy script finished
</code></pre><p>If you're getting a permission denied error, than execute <code>chmod +x</code> on the scripts:</p>
<pre><code>chmod +x boot.sh build.sh destroy.sh shutdown.sh
</code></pre><p>I'm not going to explain these scripts because they're simple <code>if-else</code> statements along with some Docker commands that you've already seen many times. If you have some understanding of the Linux shell, you should be able to understand the scripts as well.</p>
<h2 id="heading-how-to-compose-projects-using-docker-compose">How to Compose Projects Using Docker-Compose</h2>
<p>In the previous section, you've learned about managing a multi-container project and the difficulties of it. Instead of writing so many commands, there is an easier way to manage multi-container projects, a tool called <a target="_blank" href="https://docs.docker.com/compose/">Docker Compose</a>.</p>
<p>According to the Docker <a target="_blank" href="https://docs.docker.com/compose/">documentation</a> -</p>
<blockquote>
<p>Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.</p>
</blockquote>
<p>Although Compose works in all environments, it's more focused on development and testing. Using Compose on a production environment is not recommended at all.</p>
<h3 id="heading-docker-compose-basics">Docker Compose Basics</h3>
<p>Go the directory where you've cloned the repository that came with this book. Go inside the <code>notes-api/api</code> directory and create a <code>Dockerfile.dev</code> file. Put the following code in it:</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># stage one</span>
<span class="hljs-keyword">FROM</span> node:lts-alpine as builder

<span class="hljs-comment"># install dependencies for node-gyp</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache python make g++</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> ./package.json .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>

<span class="hljs-comment"># stage two</span>
<span class="hljs-keyword">FROM</span> node:lts-alpine

<span class="hljs-keyword">ENV</span> NODE_ENV=development

<span class="hljs-keyword">USER</span> node
<span class="hljs-keyword">RUN</span><span class="bash"> mkdir -p /home/node/app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /home/node/app</span>

<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/node_modules /home/node/app/node_modules</span>

<span class="hljs-keyword">CMD</span><span class="bash"> [ <span class="hljs-string">"./node_modules/.bin/nodemon"</span>, <span class="hljs-string">"--config"</span>, <span class="hljs-string">"nodemon.json"</span>, <span class="hljs-string">"bin/www"</span> ]</span>
</code></pre>
<p>The code is almost identical to the <code>Dockerfile</code> that you worked with in the previous section. The three differences in this file are as follows:</p>
<ul>
<li>On line 10, we run <code>npm install</code> instead of <code>npm run install --only=prod</code> because we want the development dependencies also.</li>
<li>On line 15, we set the <code>NODE_ENV</code> environment variable to <code>development</code> instead of <code>production</code>.</li>
<li>On line 24, we use a tool called <a target="_blank" href="https://nodemon.io/">nodemon</a> to get the hot-reload feature for the API.</li>
</ul>
<p>You already know that this project has two containers:</p>
<ul>
<li><code>notes-db</code> - A database server powered by PostgreSQL.</li>
<li><code>notes-api</code> - A REST API powered by Express.js</li>
</ul>
<p>In the world of Compose, each container that makes up the application is known as a service. The first step in composing a multi-container project is to define these services.</p>
<p>Just like the Docker daemon uses a <code>Dockerfile</code> for building images, Docker Compose uses a <code>docker-compose.yaml</code> file to read service definitions from.</p>
<p>Head to the <code>notes-api</code> directory and create a new <code>docker-compose.yaml</code> file. Put the following code into the newly created file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.8"</span>

<span class="hljs-attr">services:</span> 
    <span class="hljs-attr">db:</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:12</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-db-dev</span>
        <span class="hljs-attr">volumes:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">notes-db-dev-data:/var/lib/postgresql/data</span>
        <span class="hljs-attr">environment:</span>
            <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">notesdb</span>
            <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">secret</span>
    <span class="hljs-attr">api:</span>
        <span class="hljs-attr">build:</span>
            <span class="hljs-attr">context:</span> <span class="hljs-string">./api</span>
            <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile.dev</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">notes-api:dev</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-api-dev</span>
        <span class="hljs-attr">environment:</span> 
            <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">db</span> <span class="hljs-comment">## same as the database service name</span>
            <span class="hljs-attr">DB_DATABASE:</span> <span class="hljs-string">notesdb</span>
            <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">secret</span>
        <span class="hljs-attr">volumes:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">/home/node/app/node_modules</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./api:/home/node/app</span>
        <span class="hljs-attr">ports:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-number">3000</span><span class="hljs-string">:3000</span>

<span class="hljs-attr">volumes:</span>
    <span class="hljs-attr">notes-db-dev-data:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">notes-db-dev-data</span>
</code></pre>
<p>Every valid <code>docker-compose.yaml</code> file starts by defining the file version. At the time of writing, <code>3.8</code> is the latest version. You can look up the latest version <a target="_blank" href="https://docs.docker.com/compose/compose-file/">here</a>.</p>
<p>Blocks in an YAML file are defined by indentation. I will go through each of the blocks and will explain what they do.</p>
<ul>
<li>The <code>services</code> block holds the definitions for each of the services or containers in the application. <code>db</code> and <code>api</code> are the two services that comprise this project.</li>
<li>The <code>db</code> block defines a new service in the application and holds necessary information to start the container. Every service requires either a pre-built image or a <code>Dockerfile</code> to run a container. For the <code>db</code> service we're using the official PostgreSQL image.</li>
<li>Unlike the <code>db</code> service, a pre-built image for the <code>api</code> service doesn't exist. So we'll use the <code>Dockerfile.dev</code> file.</li>
<li>The <code>volumes</code> block defines any name volume needed by any of the services. At the time it only enlists <code>notes-db-dev-data</code> volume used by the <code>db</code> service.</li>
</ul>
<p>Now that have a high level overview of the <code>docker-compose.yaml</code> file, let's have a closer look at the individual services.</p>
<p>The definition code for the <code>db</code> service is as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:12</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-db-dev</span>
    <span class="hljs-attr">volumes:</span> 
        <span class="hljs-bullet">-</span> <span class="hljs-string">db-data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">environment:</span>
        <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">notesdb</span>
        <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">secret</span>
</code></pre>
<ul>
<li>The <code>image</code> key holds the image repository and tag used for this container. We're using the <code>postgres:12</code> image for running the database container.</li>
<li>The <code>container_name</code> indicates the name of the container. By default containers are named following <code>&lt;project directory name&gt;_&lt;service name&gt;</code> syntax. You can override that using <code>container_name</code>.</li>
<li>The <code>volumes</code> array holds the volume mappings for the service and supports named volumes, anonymous volumes, and bind mounts. The syntax <code>&lt;source&gt;:&lt;destination&gt;</code> is identical to what you've seen before.</li>
<li>The <code>environment</code> map holds the values of the various environment variables needed for the service.</li>
</ul>
<p>Definition code for the <code>api</code> service is as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">api:</span>
    <span class="hljs-attr">build:</span>
        <span class="hljs-attr">context:</span> <span class="hljs-string">./api</span>
        <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile.dev</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">notes-api:dev</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-api-dev</span>
    <span class="hljs-attr">environment:</span> 
        <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">db</span> <span class="hljs-comment">## same as the database service name</span>
        <span class="hljs-attr">DB_DATABASE:</span> <span class="hljs-string">notesdb</span>
        <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">secret</span>
    <span class="hljs-attr">volumes:</span> 
        <span class="hljs-bullet">-</span> <span class="hljs-string">/home/node/app/node_modules</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">./api:/home/node/app</span>
    <span class="hljs-attr">ports:</span> 
        <span class="hljs-bullet">-</span> <span class="hljs-number">3000</span><span class="hljs-string">:3000</span>
</code></pre>
<ul>
<li>The <code>api</code> service doesn't come with a pre-built image. Instead it has a build configuration. Under the <code>build</code> block we define the context and the name of the Dockerfile for building an image. You should have an understanding of context and Dockerfile by now so I won't spend time explaining those.</li>
<li>The <code>image</code> key holds the name of the image to be built. If not assigned, the image will be named following the <code>&lt;project directory name&gt;_&lt;service name&gt;</code> syntax.</li>
<li>Inside the <code>environment</code> map, the <code>DB_HOST</code> variable demonstrates a feature of Compose. That is, you can refer to another service in the same application by using its name. So the <code>db</code> here, will be replaced by the IP address of the <code>api</code> service container. The <code>DB_DATABASE</code> and <code>DB_PASSWORD</code> variables have to match up with <code>POSTGRES_DB</code> and <code>POSTGRES_PASSWORD</code> respectively from the <code>db</code> service definition.</li>
<li>In the <code>volumes</code> map, you can see an anonymous volume and a bind mount described. The syntax is identical to what you've seen in previous sections.</li>
<li>The <code>ports</code> map defines any port mapping. The syntax, <code>&lt;host port&gt;:&lt;container port&gt;</code> is identical to the <code>--publish</code> option you used before.</li>
</ul>
<p>Finally, the code for the <code>volumes</code> is as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">volumes:</span>
    <span class="hljs-attr">db-data:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">notes-db-dev-data</span>
</code></pre>
<p>Any named volume used in any of the services has to be defined here. If you don't define a name, the volume will be named following the <code>&lt;project directory name&gt;_&lt;volume key&gt;</code> and the key here is <code>db-data</code>. </p>
<p>You can learn about the different options for volume configuration in the official <a target="_blank" href="https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes">docs</a>.</p>
<h3 id="heading-how-to-start-services-in-docker-compose">How to Start Services in Docker Compose</h3>
<p>There are a few ways of starting services defined in a YAML file. The first command that you'll learn about is the <code>up</code> command. The <code>up</code> command builds any missing images, creates containers, and starts them in one go.</p>
<p>Before you execute the command, though, make sure you've opened your terminal in the same directory where the <code>docker-compose.yaml</code> file is. This is very important for every <code>docker-compose</code> command you execute.</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network <span class="hljs-string">"notes-api_default"</span> <span class="hljs-keyword">with</span> the <span class="hljs-keyword">default</span> driver
# Creating volume <span class="hljs-string">"notes-db-dev-data"</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">default</span> driver
# Building api
# Sending build context to Docker daemon  <span class="hljs-number">37.38</span>kB
#
# Step <span class="hljs-number">1</span>/<span class="hljs-number">13</span> : FROM node:lts-alpine <span class="hljs-keyword">as</span> builder
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">2</span>/<span class="hljs-number">13</span> : RUN apk add --no-cache python make g++
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">197056</span>ec1964
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container <span class="hljs-number">197056</span>ec1964
#  ---&gt; <span class="hljs-number">6609935</span>fe50b
# Step <span class="hljs-number">3</span>/<span class="hljs-number">13</span> : WORKDIR /app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">17010</span>f65c5e7
# Removing intermediate container <span class="hljs-number">17010</span>f65c5e7
#  ---&gt; b10d12e676ad
# Step <span class="hljs-number">4</span>/<span class="hljs-number">13</span> : COPY ./package.json .
#  ---&gt; <span class="hljs-number">600</span>d31d9362e
# Step <span class="hljs-number">5</span>/<span class="hljs-number">13</span> : RUN npm install
#  ---&gt; Running <span class="hljs-keyword">in</span> a14afc8c0743
### LONG INSTALLATION STUFF GOES HERE ###
#  Removing intermediate container a14afc8c0743
#  ---&gt; <span class="hljs-number">952</span>d5d86e361
# Step <span class="hljs-number">6</span>/<span class="hljs-number">13</span> : FROM node:lts-alpine
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">7</span>/<span class="hljs-number">13</span> : ENV NODE_ENV=development
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>d5376a9e78a
# Removing intermediate container <span class="hljs-number">0</span>d5376a9e78a
#  ---&gt; <span class="hljs-number">910</span>c081ce5f5
# Step <span class="hljs-number">8</span>/<span class="hljs-number">13</span> : USER node
#  ---&gt; Running <span class="hljs-keyword">in</span> cfaefceb1eff
# Removing intermediate container cfaefceb1eff
#  ---&gt; <span class="hljs-number">1480176</span>a1058
# Step <span class="hljs-number">9</span>/<span class="hljs-number">13</span> : RUN mkdir -p /home/node/app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">3</span>ae30e6fb8b8
# Removing intermediate container <span class="hljs-number">3</span>ae30e6fb8b8
#  ---&gt; c391cee4b92c
# Step <span class="hljs-number">10</span>/<span class="hljs-number">13</span> : WORKDIR /home/node/app
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">6</span>aa27f6b50c1
# Removing intermediate container <span class="hljs-number">6</span>aa27f6b50c1
#  ---&gt; <span class="hljs-number">761</span>a7435dbca
# Step <span class="hljs-number">11</span>/<span class="hljs-number">13</span> : COPY . .
#  ---&gt; b5d5c5bdf3a6
# Step <span class="hljs-number">12</span>/<span class="hljs-number">13</span> : COPY --<span class="hljs-keyword">from</span>=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; <span class="hljs-number">9e1</span>a19960420
# Step <span class="hljs-number">13</span>/<span class="hljs-number">13</span> : CMD [ <span class="hljs-string">"./node_modules/.bin/nodemon"</span>, <span class="hljs-string">"--config"</span>, <span class="hljs-string">"nodemon.json"</span>, <span class="hljs-string">"bin/www"</span> ]
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">5</span>bdd62236994
# Removing intermediate container <span class="hljs-number">5</span>bdd62236994
#  ---&gt; <span class="hljs-number">548e178</span>f1386
# Successfully built <span class="hljs-number">548e178</span>f1386
# Successfully tagged notes-api:dev
# Creating notes-api-dev ... done
# Creating notes-db-dev  ... done
</code></pre><p>The <code>--detach</code> or <code>-d</code> option here functions the same as the one you've seen before. The <code>--file</code> or <code>-f</code> option is only needed if the YAML file is not named <code>docker-compose.yaml</code> (but I've used here for demonstration purposes).</p>
<p>Apart from the the <code>up</code> command there is the <code>start</code> command. The main difference between these two is that the <code>start</code> command doesn't create missing containers, only starts existing containers. It's basically the same as the <code>container start</code> command.</p>
<p>The <code>--build</code> option for the <code>up</code> command forces a rebuild of the images. There are some other options for the <code>up</code> command that you can see in the official <a target="_blank" href="https://docs.docker.com/compose/reference/up/">docs</a>.</p>
<h3 id="heading-how-to-list-services-in-docker-compose">How to List Services in Docker Compose</h3>
<p>Although service containers started by Compose can be listed using the <code>container ls</code> command, there is the <code>ps</code> command for listing containers defined in the YAML only.</p>
<pre><code>docker-compose ps

#     Name                   Command               State           Ports         
# -------------------------------------------------------------------------------
# notes-api-dev   docker-entrypoint.sh ./nod ...   Up      <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">3000</span>-&gt;<span class="hljs-number">3000</span>/tcp
# notes-db-dev    docker-entrypoint.sh postgres    Up      <span class="hljs-number">5432</span>/tcp
</code></pre><p>It's not as informative as the <code>container ls</code> output, but it's useful when you have tons of containers running simultaneously.</p>
<h3 id="heading-how-to-execute-commands-inside-a-running-service-in-docker-compose">How to Execute Commands Inside a Running Service in Docker Compose</h3>
<p>I hope you remember from the previous section that you have to run some migration scripts to create the database tables for this API. </p>
<p>Just like the <code>container exec</code> command, there is an <code>exec</code> command for <code>docker-compose</code>. Generic syntax for the command is as follows:</p>
<pre><code>docker-compose exec &lt;service name&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">command</span>&gt;</span></span>
</code></pre><p>To execute the <code>npm run db:migrate</code> command inside the <code>api</code> service, you can execute the following command:</p>
<pre><code>docker-compose exec api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
# 
# Using environment: development
# Batch <span class="hljs-number">1</span> run: <span class="hljs-number">1</span> migrations
</code></pre><p>Unlike the <code>container exec</code> command, you don't need to pass the <code>-it</code> flag for interactive sessions. <code>docker-compose</code> does that automatically.</p>
<h3 id="heading-how-to-access-logs-from-a-running-service-in-docker-compose">How to Access Logs from a Running Service in Docker Compose</h3>
<p>You can also use the <code>logs</code> command to retrieve logs from a running service. The generic syntax for the command is as follows:</p>
<pre><code>docker-compose logs &lt;service name&gt;
</code></pre><p>To access the logs from the <code>api</code> service, execute the following command:</p>
<pre><code>docker-compose logs api

# Attaching to notes-api-dev
# notes-api-dev | [nodemon] <span class="hljs-number">2.0</span><span class="hljs-number">.7</span>
# notes-api-dev | [nodemon] reading config ./nodemon.json
# notes-api-dev | [nodemon] to restart at any time, enter <span class="hljs-string">`rs`</span>
# notes-api-dev | [nodemon] or send SIGHUP to <span class="hljs-number">1</span> to restart
# notes-api-dev | [nodemon] ignoring: *.test.js
# notes-api-dev | [nodemon] watching path(s): *.*
# notes-api-dev | [nodemon] watching extensions: js,mjs,json
# notes-api-dev | [nodemon] starting <span class="hljs-string">`node bin/www`</span>
# notes-api-dev | [nodemon] forking
# notes-api-dev | [nodemon] child pid: <span class="hljs-number">19</span>
# notes-api-dev | [nodemon] watching <span class="hljs-number">18</span> files
# notes-api-dev | app running -&gt; http:<span class="hljs-comment">//127.0.0.1:3000</span>
</code></pre><p>This is just a portion from the log output. You can kind of hook into the output stream of the service and get the logs in real-time by using the <code>-f</code> or <code>--follow</code> option. Any later log will show up instantly in the terminal as long as you don't exit by pressing <code>ctrl + c</code> or closing the window. The container will keep running even if you exit out of the log window.</p>
<h3 id="heading-how-to-stop-services-in-docker-compose">How to Stop Services in Docker Compose</h3>
<p>To stop services, there are two approaches that you can take. The first one is the <code>down</code> command. The <code>down</code> command stops all running containers and removes them from the system. It also removes any networks:</p>
<pre><code>docker-compose down --volumes

# Stopping notes-api-dev ... done
# Stopping notes-db-dev  ... done
# Removing notes-api-dev ... done
# Removing notes-db-dev  ... done
# Removing network notes-api_default
# Removing volume notes-db-dev-data
</code></pre><p>The <code>--volumes</code> option indicates that you want to remove any named volume(s) defined in the <code>volumes</code> block. You can learn about the additional options for the <code>down</code> command in the official <a target="_blank" href="https://docs.docker.com/compose/reference/down/">docs</a>.</p>
<p>Another command for stopping services is the <code>stop</code> command which functions identically to the <code>container stop</code> command. It stops all the containers for the application and keeps them. These containers can later be started with the <code>start</code> or <code>up</code> command.</p>
<h3 id="heading-how-to-compose-a-full-stack-application-in-docker-compose">How to Compose a Full-stack Application in Docker Compose</h3>
<p>In this sub-section, we'll be adding a front-end to our notes API and turning it into a complete full-stack application. I won't be explaining any of the <code>Dockerfile.dev</code> files in this sub-section (except the one for the <code>nginx</code> service) as they are identical to some of the others you've already seen in previous sub-sections.‌</p>
<p>If you've cloned the project code repository, then go inside the <code>fullstack-notes-application</code> directory. Each directory inside the project root contains the code for each service and the corresponding <code>Dockerfile</code>.‌</p>
<p>Before we start with the <code>docker-compose.yaml</code> file let's look at a diagram of how the application is going to work:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/fullstack-application-design.svg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Instead of accepting requests directly like we previously did, in this application all the requests will be first received by an NGINX (lets call it router) service. </p>
<p>The router will then see if the requested end-point has <code>/api</code> in it. If yes, the router will route the request to the back-end or if not, the router will route the request to the front-end.</p>
<p>You do this because when you run a front-end application it doesn't run inside a container. It runs on the browser, served from a container. As a result, Compose networking doesn't work as expected and the front-end application fails to find the <code>api</code> service.</p>
<p>NGINX, on the other hand, runs inside a container and can communicate with the different services across the entire application.</p>
<p>I will not get into the configuration of NGINX here. That topic is kinda out of the scope of this book. But if you want to have a look at it, go ahead and check out the <code>/notes-api/nginx/development.conf</code> and <code>/notes-api/nginx/production.conf</code> files. Code for the <code>/notes-api/nginx/Dockerfile.dev</code> is as follows:</p>
<pre><code>FROM nginx:stable-alpine

COPY ./development.conf /etc/nginx/conf.d/<span class="hljs-keyword">default</span>.conf
</code></pre><p>All it does is copy the configuration file to <code>/etc/nginx/conf.d/default.conf</code> inside the container.</p>
<p>Let's start writing the <code>docker-compose.yaml</code> file. Apart from the <code>api</code> and <code>db</code> services there will be the <code>client</code> and <code>nginx</code> services. There will also be some network definitions that I'll get into shortly.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.8"</span>

<span class="hljs-attr">services:</span> 
    <span class="hljs-attr">db:</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:12</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-db-dev</span>
        <span class="hljs-attr">volumes:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">db-data:/var/lib/postgresql/data</span>
        <span class="hljs-attr">environment:</span>
            <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">notesdb</span>
            <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">secret</span>
        <span class="hljs-attr">networks:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">backend</span>
    <span class="hljs-attr">api:</span>
        <span class="hljs-attr">build:</span> 
            <span class="hljs-attr">context:</span> <span class="hljs-string">./api</span>
            <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile.dev</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">notes-api:dev</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-api-dev</span>
        <span class="hljs-attr">volumes:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">/home/node/app/node_modules</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./api:/home/node/app</span>
        <span class="hljs-attr">environment:</span> 
            <span class="hljs-attr">DB_HOST:</span> <span class="hljs-string">db</span> <span class="hljs-comment">## same as the database service name</span>
            <span class="hljs-attr">DB_PORT:</span> <span class="hljs-number">5432</span>
            <span class="hljs-attr">DB_USER:</span> <span class="hljs-string">postgres</span>
            <span class="hljs-attr">DB_DATABASE:</span> <span class="hljs-string">notesdb</span>
            <span class="hljs-attr">DB_PASSWORD:</span> <span class="hljs-string">secret</span>
        <span class="hljs-attr">networks:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">backend</span>
    <span class="hljs-attr">client:</span>
        <span class="hljs-attr">build:</span>
            <span class="hljs-attr">context:</span> <span class="hljs-string">./client</span>
            <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile.dev</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">notes-client:dev</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-client-dev</span>
        <span class="hljs-attr">volumes:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">/home/node/app/node_modules</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./client:/home/node/app</span>
        <span class="hljs-attr">networks:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">nginx:</span>
        <span class="hljs-attr">build:</span>
            <span class="hljs-attr">context:</span> <span class="hljs-string">./nginx</span>
            <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile.dev</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">notes-router:dev</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">notes-router-dev</span>
        <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
        <span class="hljs-attr">ports:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-number">8080</span><span class="hljs-string">:80</span>
        <span class="hljs-attr">networks:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">backend</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">frontend</span>

<span class="hljs-attr">volumes:</span>
    <span class="hljs-attr">db-data:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">notes-db-dev-data</span>

<span class="hljs-attr">networks:</span> 
    <span class="hljs-attr">frontend:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">fullstack-notes-application-network-frontend</span>
        <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
    <span class="hljs-attr">backend:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">fullstack-notes-application-network-backend</span>
        <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<p>The file is almost identical to the previous one you worked with. The only thing that needs some explanation is the network configuration. The code for the <code>networks</code> block is as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">networks:</span> 
    <span class="hljs-attr">frontend:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">fullstack-notes-application-network-frontend</span>
        <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
    <span class="hljs-attr">backend:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">fullstack-notes-application-network-backend</span>
        <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<p>I've defined two bridge networks. By default, Compose creates a bridge network and attaches all containers to that. In this project, however, I wanted proper network isolation. So I defined two networks, one for the front-end services and one for the back-end  services.</p>
<p>I've also added <code>networks</code> block in each of the service definitions. This way the the <code>api</code> and <code>db</code> service will be attached to one network and the <code>client</code> service will be attached to a separate network. But the <code>nginx</code> service will be attached to both the networks so that it can perform as router between the front-end and back-end services.</p>
<p>Start all the services by executing the following command:</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network <span class="hljs-string">"fullstack-notes-application-network-backend"</span> <span class="hljs-keyword">with</span> driver <span class="hljs-string">"bridge"</span>
# Creating network <span class="hljs-string">"fullstack-notes-application-network-frontend"</span> <span class="hljs-keyword">with</span> driver <span class="hljs-string">"bridge"</span>
# Creating volume <span class="hljs-string">"notes-db-dev-data"</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">default</span> driver
# Building api
# Sending build context to Docker daemon  <span class="hljs-number">37.38</span>kB
# 
# Step <span class="hljs-number">1</span>/<span class="hljs-number">13</span> : FROM node:lts-alpine <span class="hljs-keyword">as</span> builder
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">2</span>/<span class="hljs-number">13</span> : RUN apk add --no-cache python make g++
#  ---&gt; Running <span class="hljs-keyword">in</span> <span class="hljs-number">8</span>a4485388fd3
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container <span class="hljs-number">8</span>a4485388fd3
#  ---&gt; <span class="hljs-number">47</span>fb1ab07cc0
# Step <span class="hljs-number">3</span>/<span class="hljs-number">13</span> : WORKDIR /app
#  ---&gt; Running <span class="hljs-keyword">in</span> bc76cc41f1da
# Removing intermediate container bc76cc41f1da
#  ---&gt; <span class="hljs-number">8</span>c03fdb920f9
# Step <span class="hljs-number">4</span>/<span class="hljs-number">13</span> : COPY ./package.json .
#  ---&gt; a1d5715db999
# Step <span class="hljs-number">5</span>/<span class="hljs-number">13</span> : RUN npm install
#  ---&gt; Running <span class="hljs-keyword">in</span> fabd33cc0986
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container fabd33cc0986
#  ---&gt; e09913debbd1
# Step <span class="hljs-number">6</span>/<span class="hljs-number">13</span> : FROM node:lts-alpine
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">7</span>/<span class="hljs-number">13</span> : ENV NODE_ENV=development
#  ---&gt; Using cache
#  ---&gt; b7c12361b3e5
# Step <span class="hljs-number">8</span>/<span class="hljs-number">13</span> : USER node
#  ---&gt; Using cache
#  ---&gt; f5ac66ca07a4
# Step <span class="hljs-number">9</span>/<span class="hljs-number">13</span> : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">60094</span>b9a6183
# Step <span class="hljs-number">10</span>/<span class="hljs-number">13</span> : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">316</span>a252e6e3e
# Step <span class="hljs-number">11</span>/<span class="hljs-number">13</span> : COPY . .
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">3</span>a083622b753
# Step <span class="hljs-number">12</span>/<span class="hljs-number">13</span> : COPY --<span class="hljs-keyword">from</span>=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">707979</span>b3371c
# Step <span class="hljs-number">13</span>/<span class="hljs-number">13</span> : CMD [ <span class="hljs-string">"./node_modules/.bin/nodemon"</span>, <span class="hljs-string">"--config"</span>, <span class="hljs-string">"nodemon.json"</span>, <span class="hljs-string">"bin/www"</span> ]
#  ---&gt; Using cache
#  ---&gt; f2da08a5f59b
# Successfully built f2da08a5f59b
# Successfully tagged notes-api:dev
# Building client
# Sending build context to Docker daemon  <span class="hljs-number">43.01</span>kB
# 
# Step <span class="hljs-number">1</span>/<span class="hljs-number">7</span> : FROM node:lts-alpine
#  ---&gt; <span class="hljs-number">471e8</span>b4eb0b2
# Step <span class="hljs-number">2</span>/<span class="hljs-number">7</span> : USER node
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">4</span>be5fb31f862
# Step <span class="hljs-number">3</span>/<span class="hljs-number">7</span> : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">1</span>fefc7412723
# Step <span class="hljs-number">4</span>/<span class="hljs-number">7</span> : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; d1470d878aa7
# Step <span class="hljs-number">5</span>/<span class="hljs-number">7</span> : COPY ./package.json .
#  ---&gt; Using cache
#  ---&gt; bbcc49475077
# Step <span class="hljs-number">6</span>/<span class="hljs-number">7</span> : RUN npm install
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">860</span>a4a2af447
# Step <span class="hljs-number">7</span>/<span class="hljs-number">7</span> : CMD [ <span class="hljs-string">"npm"</span>, <span class="hljs-string">"run"</span>, <span class="hljs-string">"serve"</span> ]
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">11</span>db51d5bee7
# Successfully built <span class="hljs-number">11</span>db51d5bee7
# Successfully tagged notes-client:dev
# Building nginx
# Sending build context to Docker daemon   <span class="hljs-number">5.12</span>kB
# 
# Step <span class="hljs-number">1</span>/<span class="hljs-number">2</span> : FROM nginx:stable-alpine
#  ---&gt; f2343e2e2507
# Step <span class="hljs-number">2</span>/<span class="hljs-number">2</span> : COPY ./development.conf /etc/nginx/conf.d/<span class="hljs-keyword">default</span>.conf
#  ---&gt; Using cache
#  ---&gt; <span class="hljs-number">02</span>a55d005a98
# Successfully built <span class="hljs-number">02</span>a55d005a98
# Successfully tagged notes-router:dev
# Creating notes-client-dev ... done
# Creating notes-api-dev    ... done
# Creating notes-router-dev ... done
# Creating notes-db-dev     ... done
</code></pre><p>Now visit <code>http://localhost:8080</code> and voilà!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/notes-application.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Try adding and deleting notes to see if the application works properly. The project also comes with shell scripts and a <code>Makefile</code>. Explore them to see how you can run this project without the help of <code>docker-compose</code> like you did in the previous section.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I would like to thank you from the bottom of my heart for the time you've spent reading this book. I hope you've enjoyed it and have learned all the essentials of Docker.</p>
<p>Apart from this one, I've written full-length handbooks on other complicated topics available for free on <a target="_blank" href="https://www.freecodecamp.org/news/author/farhanhasin/">freeCodeCamp</a>.</p>
<p>These handbooks are part of my mission to simplify hard to understand technologies for everyone. Each of these handbooks takes a lot of time and effort to write.</p>
<p>If you've enjoyed my writing and want to keep me motivated, consider leaving starts on <a target="_blank" href="https://github.com/fhsinchy/">GitHub</a> and endorse me for relevant skills on <a target="_blank" href="https://www.linkedin.com/in/farhanhasin/">LinkedIn</a>. I also accept sponsorship so you may consider <a target="_blank" href="https://www.buymeacoffee.com/farhanhasin">buying me a coffee</a> if you want to.</p>
<p>I'm always open to suggestions and discussions on <a target="_blank" href="https://twitter.com/frhnhsin">Twitter</a> or <a target="_blank" href="https://www.linkedin.com/in/farhanhasin/">LinkedIn</a>. Hit me with direct messages.</p>
<p>In the end, consider sharing the resources with others, because </p>
<blockquote>
<p>Sharing knowledge is the most fundamental act of friendship. Because it is a way you can give something without loosing something. — Richard Stallman</p>
</blockquote>
<p>Till the next one, stay safe and keep learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Enable Live-reload on Docker-based Applications with Docker Volumes ]]>
                </title>
                <description>
                    <![CDATA[ By Erick Wendel In this post you'll learn how to configure a development environment with live-reload enabled. This will allow you to convert a legacy application so it uses Docker, Docker volumes, and docker-compose. Some developers turn up their no... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-enable-live-reload-on-docker-based-applications/</link>
                <guid isPermaLink="false">66d45e3ca3a4f04fb2dd2e3f</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker Containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ node ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 25 Jun 2020 17:16:31 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/5f9c9a0f740569d1a4ca233c.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Erick Wendel</p>
<p>In this post you'll learn how to configure a development environment with live-reload enabled. This will allow you to convert a legacy application so it uses Docker, Docker volumes, and docker-compose.</p>
<p>Some developers turn up their noses when talking about using Docker for their development environment. They say that Docker isn't good for development because it always needs to rebuild the entire image to reflect all new modifications. This makes it unproductive and slow. </p>
<p>In this article, our goal is to tackle this mindset by demonstrating how simple configurations can result in many benefits such as a reliable environment over production and development environments.</p>
<p>By the end of this post you will have learned how to:</p>
<ul>
<li>Convert a legacy application to run within a Docker container;</li>
<li>Enable dependency caching on Node.js modules;</li>
<li>Enable live-reload by using docker volumes;</li>
<li>Aggregate all services within docker-compose.</li>
</ul>
<h2 id="heading-requirements"><strong>Requirements</strong></h2>
<p>In the next steps, you'll clone an existing project to execute all examples in this article. Before starting to code make sure you have the following tools installed on your machine:</p>
<ul>
<li><a target="_blank" href="https://docs.docker.com/desktop/">Docker</a> and <a target="_blank" href="https://docs.docker.com/compose/">Docker compose</a></li>
<li><a target="_blank" href="https://nodejs.org/en/download/current/">Node.js 1</a>0+</li>
<li><a target="_blank" href="https://code.visualstudio.com/download">Git</a></li>
</ul>
<h2 id="heading-why-use-docker"><strong>Why use docker?</strong></h2>
<p>More and more cutting-edge technologies are being released for the internet all the time. They're stable, and they're fun to develop and release, but they're not predictable when working over different environments. So developers created Docker to help reduce the chances of possible errors.</p>
<p>Docker is one of my favorite tools that I work with every day on desktop, web, and IoT apps. It has given me the power to not only move applications through different environments, but also to keep my local environment as clean as possible.  </p>
<p>Developers working with cutting-edge technologies are always working with something new. But what about legacy applications? Should we just rewrite everything with new tech? I know this is not as simple as it seems. We should work on new stuff, but also make improvements to existing applications. </p>
<p>Let's say you have decided to move from Windows Servers to Unix servers. How would you do it? Do you know every dependency your app requires to work?</p>
<h2 id="heading-what-should-a-development-environment-look-like">What should a development environment look like?</h2>
<p>Developers have always tried to be more productive by adding plugins, boilerplates, and codebases on their editors/IDEs/terminals. The best environment in my opinion should be:</p>
<ol>
<li>Easy to run and test;</li>
<li>Environment agnostic;</li>
<li>Fast to evaluate modifications;</li>
<li>Easy to replicate on any machine.</li>
</ol>
<p>Following these principles, we'll configure an application over the next sections of this article. Also, if you've never heard about live-reload (or hot reload), it is a feature that watches for changes in your code and restarts the server if needed. So you don't need to go back and forth, restarting your app or even rebuilding the system.</p>
<h2 id="heading-getting-started">Getting started</h2>
<p>First, you'll need to have an empty folder called <code>post-docker-livereload</code> which you'll use as a workspace. Go to the <a target="_blank" href="https://github.com/ErickWendel/nodejs-with-mongodb-api-example">Github repository</a> and clone it on your post-docker-live-reload folder.</p>
<p>Secondly, let's analyse what the application requires. If you take a look at the README.md file, there are a few instructions demonstrating how to run this app as shown in the image below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screen-Shot-2020-06-24-at-18.10.43-1.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>It requires Node.js version 10 or higher and MongoDB. Instead of installing MongoDB on your local environment machine, you'll install it using Docker. You'll also expose it on localhost:27017 so applications that are not running through Docker can access it without knowing the internal Docker IP address. </p>
<p>Copy the command below and paste it in your terminal:</p>
<pre><code class="lang-bash">docker run --name mongodb -p 27017:27017 -d mongo:4
</code></pre>
<p>Using the command above, it'll download and run the MongoDB instance. Notice that if you already have an instance with this name it'll throw an error about invalid name. </p>
<p>If you see the error, run <code>docker rm mongodb</code> and it will remove any previous instance so you can run the docker run command again.</p>
<h2 id="heading-digging-into-the-application">Digging into the application</h2>
<p>The README.md file says that you need a MongoDB instance running before starting your app, along with Node.js. </p>
<p>If you have Node.js installed, go to the <code>nodejs-with-mongodb-api-example</code> folder and run the following commands:</p>
<pre><code class="lang-bash">npm i 
npm run build 
npm start
</code></pre>
<p>After running these commands, you can go to a browser on <a target="_blank" href="http://localhost:3000">http://localhost:3000</a> and see the application running as shown in the image below:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/01-start.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Keep in mind that the app already has a command to enable live reload which is <code>npm run dev:watch</code>. The pipeline should reflect the following steps:</p>
<ol>
<li>Developer changes Typescript files;</li>
<li>Typescript transpiles files to Javascript;</li>
<li>Server notices changes to Javascript and restarts the Node.js server.</li>
</ol>
<p>So mirroring files to Docker containers will reflect all changes in the container. The <code>npm run build:watch</code> from the application will catch the changes and generate output files in the lib folder so the <code>npm run dev:run</code> will restart the server every time it has been triggered.</p>
<h2 id="heading-dockerizing-applications">Dockerizing applications</h2>
<p>If Docker is a completely new world to you, don't be afraid! You will configure it from scratch. You'll need to create a few files to start:</p>
<ol>
<li><code>Dockerfile</code> - a receipt file that lists how to install and run your app;</li>
<li><code>.dockerignore</code> - a file that lists what file(s) won't go within the Docker container instance.</li>
</ol>
<h3 id="heading-creating-the-dockerfile">Creating the Dockerfile</h3>
<p>The Dockerfile is the key concept here. There you specify the steps and dependencies to prepare and run the application. As long as you have read the README.md file, it will be easy to implement the receipt file. </p>
<p>I'm going to put the whole file below and dig into it later. In your <code>nodejs-with-mongodb-api-example</code> folder create a <code>Dockerfile</code> file and paste the code below:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">14</span>-alpine

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /src</span>

<span class="hljs-keyword">ADD</span><span class="bash"> package.json /src </span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm i --silent</span>

<span class="hljs-keyword">ADD</span><span class="bash"> . /src </span>

<span class="hljs-keyword">RUN</span><span class="bash"> npm run build </span>

<span class="hljs-keyword">CMD</span><span class="bash"> npm start</span>
</code></pre>
<p>What's happening there?</p>
<ul>
<li>On line 1 - It uses as its image base Node.js 14 - alpine version;</li>
<li>From lines 2 to 4 - It copies and installs Node.js dependencies from host to container. Note that the order there is important. Adding package.json to the src folder before restoring dependencies will cache it and prevent it from installing packages every time you need to build your image;</li>
<li>From lines 6 to 7 - It runs commands for the compilation process and then for starting the program as mentioned in the README.md file.</li>
</ul>
<h3 id="heading-ignoring-unnecessary-files-with-dockerignore">Ignoring unnecessary files with .dockerignore</h3>
<p>Also, I'm working on an OSX-based system and the Docker container will run on a Linux Alpine-based system. When you run <code>npm install</code> it will restore dependencies for specific environments. </p>
<p>You will now create a file to ignore the generated code from your local machine such as node<em>modules and lib</em>._ So when you copy all files from the current directory to the container it won't get invalid package versions. </p>
<p>In the <code>nodejs-with-mongodb-api-example</code> folder create a <code>.dockerignore</code> file and paste the code below:</p>
<pre><code class="lang-txt">node_modules/
lib/
</code></pre>
<h3 id="heading-building-the-docker-image">Building the docker image</h3>
<p>I prefer running this app from the rootFolder. Go back to the <code>post-docker-live-reload</code> folder and run the following commands to prepare an image for further use:</p>
<pre><code class="lang-shell">docker build -t app nodejs-with-mongodb-api-example
</code></pre>
<p>Notice that the command above uses the <code>-t</code> flag to tell you the image name and, right after that, the folder which contains the <code>Dockerfile</code> file.</p>
<h3 id="heading-working-with-volumes">Working with volumes</h3>
<p>Before running the app, let's do a few hacks to improve our experience in the Docker containers.</p>
<p>Docker volumes is a feature to mirror files through your local machine and Docker environment. You can also share volumes over containers and reuse them to cache dependencies.</p>
<p>Your goal is to watch any changes on local <code>.ts</code> files and mirror those changes in the container. Even though the files and the <code>node_modules</code> folder are in the same path. </p>
<p>Do you remember when I said that the dependencies in each operating system would be different? To make sure our local environment won't affect the Docker environment when mirroring files, we'll isolate the container's <code>node_modules</code> folder on a distinct volume. </p>
<p>Consequently, when creating the <code>node_modules</code> folder on the container, it won't create the folder on local machine environment. Run the command below in your terminal to create it:</p>
<pre><code>docker volume create --name nodemodules
</code></pre><h3 id="heading-running-and-enabling-live-reload">Running and enabling live-reload</h3>
<p>As you know, the <code>npm run dev:watch</code> specified in the README.md shows how to enable live-reload. The problem is that you're coding on a local machine and it must reflect directly your container. </p>
<p>By running the following commands you will link your local environment with the Docker container so any change in <code>nodejs-with-mongodb-api-example</code> will affect the container's <code>src</code> folder.</p>
<pre><code class="lang-shell">docker run \
    --name app \
    --link mongodb \
    -e MONGO_URL=mongodb \
    -e PORT=4000 \
    -p 4000:4000 \
    -v `pwd`/nodejs-with-mongodb-api-example:/src \
    -v nodemodules:/src/node_modules \
    app npm run dev:watch
</code></pre>
<p>Let's dig into what's happening there:</p>
<ul>
<li><code>--link</code> is giving permission to the app to access the MongoDB instance;</li>
<li><code>-e</code> - is the environment variables. As mentioned in the README.md file you can specify the MongoDB connection string you want to connect to by overriding the <code>MONGO_URL</code> variable. Override the <code>PORT</code> variable if you want to run it on a different port. Note that the value <code>mongodb</code> is the same name we used to create our MongoDB instance in the previous sections. This value is also an alias for the internal MongoDB instance's IP;</li>
<li><code>-v</code> - maps the current directory to the Docker container by using a virtual volume. Using the <code>pwd</code> command you can get the absolute path for your current working directory and then the folder you want to mirror on the Docker container. There's the <code>:/src</code>. The <code>src</code> path is the <code>WORKDIR</code> instruction defined on <code>Dockerfile</code> so we mirror the local folder to the Docker container's src;</li>
<li><code>-v</code> - the second volume there will mirror the individual volume we created on the container's <code>node_modules</code> folder;</li>
<li><code>app</code> - the image name;</li>
<li><code>npm run dev:watch</code> - this last command will override the <code>CMD</code> instruction from the <code>Dockerfile</code>.</li>
</ul>
<p>After running the command below, you can trigger the browser after changing the local <code>index.ts</code> file to see the results. The video below demonstrates these steps in practice:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/O9vEQhU_JEM" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>You know that working with shell commands works. But is not that common to use them here, and it's not productive for running all those commands, building images, and managing instances by hand. So compose it!</p>
<p>Docker compose is a way to simplify the aggregation and linking of services. You can specify the databases, logs, application, volumes, networks, and so on.</p>
<p>First, you need to remove all active instances to avoid conflict on exposed ports. Run the following commands on your terminal to remove volumes, services and containers:</p>
<pre><code class="lang-bash">docker rm app 
docker volume rm nodemodules
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
</code></pre>
<h3 id="heading-the-docker-compose-file">The docker-compose file</h3>
<p>Create a <code>docker-compose.yml</code> file on your <code>post-docker-livereload</code> folder using the data below:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">services:</span>
    <span class="hljs-attr">mongodb:</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">mongo:4</span>
        <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-number">27017</span><span class="hljs-string">:27017</span>
    <span class="hljs-attr">app:</span>
        <span class="hljs-attr">build:</span> <span class="hljs-string">nodejs-with-mongodb-api-example</span>
        <span class="hljs-attr">command:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">dev:watch</span>
        <span class="hljs-attr">ports:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-number">4000</span><span class="hljs-string">:4000</span>
        <span class="hljs-attr">environment:</span> 
            <span class="hljs-attr">MONGO_URL:</span> <span class="hljs-string">mongodb</span>
            <span class="hljs-attr">PORT:</span> <span class="hljs-number">4000</span>
        <span class="hljs-attr">volumes:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./nodejs-with-mongodb-api-example:/src/</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">nodemodules:/src/node_modules</span>
        <span class="hljs-attr">links:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">mongodb</span>
        <span class="hljs-attr">depends_on:</span> 
            <span class="hljs-bullet">-</span> <span class="hljs-string">mongodb</span>

<span class="hljs-attr">volumes:</span>
    <span class="hljs-attr">nodemodules:</span> {}
</code></pre>
<p>The file above specifies resources by sections. Notice that you have <code>links</code> and <code>depends_on</code> sections there. The links field is the same flag you've used in your shell command. The <code>depends_on</code> will make sure that the MongoDB is a dependency for running your app. It'll run the MongoDB before your app like magic! </p>
<p>Going back to your terminal, to start all services and build the Docker image and create volumes and link services run the following command:</p>
<pre><code class="lang-shell">docker-compose up --build
</code></pre>
<p>If you need to remove all services created before by that <code>Dockerfile</code> you can also run <code>docker-compose down</code>.</p>
<h2 id="heading-docker-is-your-friend">Docker is your friend!</h2>
<p>That's right, my friend. Docker can help you prevent many possible errors before they happen. You can use it for front and back end applications. Even for IoT when you need to control hardware, you can specify policies for using it.</p>
<p>As your next steps, I highly recommend that you take a look at container orchestrators such as Kubernetes and Docker swarm. They could improve even more your existing applications and help you go to the next level.</p>
<h2 id="heading-thank-you-for-reading"><strong>Thank you for reading</strong></h2>
<p>I really appreciate the time we spent together. I hope this content will be more than just text. I hope it will help make you a better thinker and also a better programmer. Follow me on <a target="_blank" href="https://twitter.com/erickwendel_">Twitter</a> and check out my <a target="_blank" href="https://erickwendel.com/">personal blog</a> where I share all my valuable content.</p>
<p>See ya! ?</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Ultimate Guide to End to End Tests with Selenium and Docker ]]>
                </title>
                <description>
                    <![CDATA[ By Jean-Paul Delimat Automated end to end testing is the most effective way to test your app. It also requires the least effort to get the benefit of tests if you currently have no tests at all. And you don’t need a ton of infrastructure or cloud ser... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/end-to-end-tests-with-selenium-and-docker-the-ultimate-guide/</link>
                <guid isPermaLink="false">66d45f6b8812486a37369ceb</guid>
                
                    <category>
                        <![CDATA[ Continuous Integration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ selenium ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Testing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 16 Dec 2019 22:46:40 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/12/end-to-end-testing-selenium-docker.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Jean-Paul Delimat</p>
<p>Automated end to end testing is the most effective way to test your app. It also requires the least effort to get the benefit of tests if you currently have no tests at all. And you don’t need a ton of infrastructure or cloud servers to get there. In this guide we’ll see how we can easily overcome the two main hurdles of end to end testing.</p>
<p>The first hurdle is Selenium. The API you use to write tests is ugly and non-intuitive. But it is not that difficult to use, and with a few convenient functions it can become a breeze to write Selenium tests. The effort is well-rewarded because you can automatically test your end users' flows end to end.</p>
<p>The second hurdle is putting components together in an isolated environment. We want the frontend, the backend, the database and everything else your app uses. We will use Docker compose to put things together and automate the tests. It is easy even if you have your components in different Git repositories.</p>
<h2 id="heading-writing-end-to-end-tests-in-selenium">Writing end to end tests in Selenium</h2>
<p>Even if you are an API only business, you have a frontend, and an admin or back office frontend. So end to end tests ultimately talk to a frontend application. </p>
<p>The industry standard tool is Selenium. Selenium provides an API to talk to the web browser and interact with the DOM. You can check what elements are displayed, fill inputs and click around. Anything a real user would do with your application, you can automate.</p>
<p>Selenium uses something called the WebDriver API. It is not very handy to use at first glance. But the learning curve is not steep. Creating a few convenience functions will get you productive in no time. I won’t go into the details of the WebDriver API here. You can have a look at <a target="_blank" href="https://marmelab.com/blog/2016/04/19/e2e-testing-with-node-and-es6.html">this excellent article</a> to dig deeper.</p>
<p>There are also libraries to make your life easier. <a target="_blank" href="https://nightwatchjs.org/">Nightwatch</a> is the most popular.</p>
<p>If you have an Angular application, <a target="_blank" href="https://www.protractortest.org/">Protractor</a> is your best friend. It integrates with the Angular event loop and allows you to use selectors based on your model. That is gold.</p>
<p>Writing a test for your most critical user feature or your app should take only a few hours, so go ahead. It will run automatically ever after. Let's see how.</p>
<h2 id="heading-running-your-tests-in-docker">Running your tests in Docker</h2>
<p>We need to run our tests in an isolated environment so the outcome is predictable. And so we can enable <a target="_blank" href="https://fire.ci/blog/how-to-get-started-with-continuous-integration/">Continuous Integration</a> easily. We'll use Docker compose.</p>
<p>Selenium provides Docker images out of the box to test with one or several browsers. The images spawn a Selenium server and a browser underneath. It can work with different browsers.</p>
<p>Let’s start with one browser for now: headless-chrome. You can see the <em>docker-compose.yml</em> file below (the commands are from an Angular example).</p>
<p>Note: If you've never used Docker you can easily install it on your computer. Docker has the troublesome tendency of forcing you to sign up for an account just to download the thing. But you actually don't have to. Go to the release notes (<a target="_blank" href="https://docs.docker.com/docker-for-windows/release-notes/">link for Windows</a> and <a target="_blank" href="https://docs.docker.com/docker-for-mac/release-notes/">link for Mac</a>) and download not the latest version but the one right before. This is a direct download link.</p>
<pre><code>version: <span class="hljs-string">'3.1'</span>

<span class="hljs-attr">services</span>:
 app-serve:
   build: .
   image: myapp
   <span class="hljs-attr">command</span>: npm run serve:production
   <span class="hljs-attr">expose</span>:
    - <span class="hljs-number">4200</span>

 app-e2e-tests:
   image: myapp
   <span class="hljs-attr">command</span>: dockerize -wait tcp:<span class="hljs-comment">//app-serve:4200 </span>
             -wait tcp:<span class="hljs-comment">//selenium-chrome-standalone:4444 </span>
             -timeout <span class="hljs-number">10</span>s -wait-retry-interval <span class="hljs-number">1</span>s bash -c <span class="hljs-string">"npm run e2e"</span>
   <span class="hljs-attr">depends_on</span>:
     - app-serve
     - selenium-chrome-standalone

 selenium-chrome-standalone:
   image: selenium/standalone-chrome
   <span class="hljs-attr">expose</span>:
    - <span class="hljs-number">44444</span>
</code></pre><p>The file above tells Docker to spin up an environment with 3 containers:</p>
<ul>
<li>Our app to test: the container uses the myapp image which we’ll build right below</li>
<li>A container running the tests: the container uses the same myapp image. It uses <a target="_blank" href="https://github.com/jwilder/dockerize">dockerize</a> to wait for the servers to be up before running the tests</li>
<li>The Selenium server: the container uses the official Selenium image. Nothing to do here. We could run the tests from the same container as the app. Splitting it makes things clearer. It also allows you to separate outputs from the 2 containers in the result logs.</li>
</ul>
<p>The containers will live inside a private virtual network and see each other as http://the-container-name (more <a target="_blank" href="https://docs.docker.com/compose/networking/">here</a> on networking in Docker).</p>
<p>We need a <em>Dockerfile</em> to build the <em>myapp</em> image used for the containers. It should turn your frontend code into a bundle as close to production as possible. Running unit tests and linting is a good idea at that stage. After all, there's no need to run end to end tests if the basics do not work.</p>
<p>In the <em>Dockerfile</em> below we use a node image as base, install dockerize, and bundle the application. It is important to build for production. You don’t want to test a development build that is pre-optimizations. Many things can go wrong there.</p>
<pre><code>FROM node:<span class="hljs-number">12.13</span><span class="hljs-number">.0</span> AS base

ENV DOCKERIZE_VERSION v0<span class="hljs-number">.6</span><span class="hljs-number">.0</span>

RUN wget https:<span class="hljs-comment">//github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \</span>
   &amp;&amp; tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
   &amp;&amp; rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

RUN mkdir -p ~/app
WORKDIR ~/app

COPY package.json .
COPY package-lock.json .

FROM base AS dependencies

RUN npm install

FROM dependencies AS runtime

COPY . .
RUN npm run lint
RUN npm run test:ci
RUN npm run build --production
</code></pre><p>Now that we have all the pieces together let's run the tests using this command:</p>
<pre><code>docker-compose up --build --abort-on-container-exit
</code></pre><p>It is long-ish, so script it in your project somehow. It will build the myapp image based on the provided <em>Dockerfile</em> and then start all the containers. Dockerize makes sure your app and Selenium are up before executing the tests.</p>
<p>The <em>--abort-on-container-exit</em> option will kill the environment when one container exists. Since only our testing container is meant to exit (the others are servers), that is what we want. </p>
<p>The docker-compose command will have the same exit code as the exiting container. It means you can easily detect from the command line if the tests succeeded or not.</p>
<p>You are now ready to run end to end tests locally and on any server supporting Docker. That's pretty good!</p>
<p>The tests run with only one browser for now, though. Let’s add more.</p>
<h2 id="heading-testing-on-different-browsers">Testing on different browsers</h2>
<p>The standalone Selenium image spins up a Selenium server with the browser you want. To run the tests on different browsers you need to update your tests' configuration and use the selenium/hub Docker image.</p>
<p>The image creates a hub between your application and the standalone Selenium images. Replace the selenium container section in your docker-compose.yml as follows:</p>
<pre><code>  selenium-hub:
    image: selenium/hub
    <span class="hljs-attr">container_name</span>: selenium-hub
    <span class="hljs-attr">expose</span>:
      - <span class="hljs-number">4444</span>
  <span class="hljs-attr">chrome</span>:
    image: selenium/node-chrome
    <span class="hljs-attr">volumes</span>:
      - <span class="hljs-regexp">/dev/</span>shm:<span class="hljs-regexp">/dev/</span>shm
    <span class="hljs-attr">depends_on</span>:
      - selenium-hub
    <span class="hljs-attr">environment</span>:
      - HUB_HOST=selenium-hub
      - HUB_PORT=<span class="hljs-number">4444</span>
  <span class="hljs-attr">firefox</span>:
    image: selenium/node-firefox
    <span class="hljs-attr">volumes</span>:
      - <span class="hljs-regexp">/dev/</span>shm:<span class="hljs-regexp">/dev/</span>shm
    <span class="hljs-attr">depends_on</span>:
      - selenium-hub
    <span class="hljs-attr">environment</span>:
      - HUB_HOST=selenium-hub
      - HUB_PORT=<span class="hljs-number">4444</span>
</code></pre><p>We now have 3 containers: Chrome, Firefox and the Selenium hub.</p>
<p>All the Docker images provided by Selenium are <a target="_blank" href="https://github.com/SeleniumHQ/docker-selenium">in this repository</a>.</p>
<p>Careful! There is a tricky timing effect to consider. We use dockerize to have our test container wait for the Selenium hub to be up. That is not enough because we need to wait for the standalone images to be ready – in fact to register themselves to the hub.</p>
<p>We can do this by waiting for the standalone images to expose a port, but that is not a guarantee. It is easier to wait a few seconds before starting the tests. Update your test script to wait 5 seconds before the tests start (you can add a sleep command after the dockerize call).</p>
<p>Now you can be sure that your tests won’t start until all the browsers are ready. Waiting is not ideal but a few seconds are worth it. There is nothing more annoying than failing tests because of unstable automations.</p>
<p>Good. We have now covered the front end part. Let’s add the back end.</p>
<h2 id="heading-add-the-back-end-as-containers-or-git-modules">Add the back end as containers or git modules</h2>
<p>The above might seem overkill to test only the front end part of your app. But we're aiming for much more. We want to test the whole system.</p>
<p>Let’s add a database and a back end to our Docker compose environment.</p>
<p>If you are a front end developer you might think "we are the front end team – we don’t care about testing the back end." Are you sure?</p>
<blockquote>
<p>The front end is always the last part to integrate all the other pieces. That means crunch time. Crunch time that would no longer exist if you were able to test the front end with the back end continuously and catch errors sooner.</p>
</blockquote>
<p>The technique I describe here is very easy to apply even if your back end is in a different repository.</p>
<p>This is what the <em>docker-compose.yml</em> looks like:</p>
<pre><code>version: <span class="hljs-string">'3.1'</span>

<span class="hljs-attr">services</span>:
 db:
   image: postgres
   <span class="hljs-attr">environment</span>:
     POSTGRES_USER: john
     <span class="hljs-attr">POSTGRES_PASSWORD</span>: mysecretpassword
   <span class="hljs-attr">expose</span>:
     - <span class="hljs-number">5432</span>
 <span class="hljs-attr">backend</span>:
   context: ./backend
   <span class="hljs-attr">dockerfile</span>: ./backend/Dockerfile
   <span class="hljs-attr">image</span>:mybackend
   <span class="hljs-attr">command</span>: dockerize
       -wait tcp:<span class="hljs-comment">//db:5432 -timeout 10s</span>
       bash -c <span class="hljs-string">"./seed_db.sh &amp;&amp; ./start_server.sh"</span>
   <span class="hljs-attr">environment</span>:
     APP_DB_HOST: db
     <span class="hljs-attr">APP_DB_USER</span>: john
     <span class="hljs-attr">APP_DB_PASSWORD</span>: mysecretpassword
   <span class="hljs-attr">expose</span>:
     - <span class="hljs-number">8000</span>
   <span class="hljs-attr">depends_on</span>:
     - db
 app-serve:
   build: .
   image: myapp
   <span class="hljs-attr">command</span>: npm run serve:sw
   <span class="hljs-attr">expose</span>:
    - <span class="hljs-number">4200</span>
 app-e2e-tests:
   image: myapp
   <span class="hljs-attr">command</span>: dockerize -wait tcp:<span class="hljs-comment">//app-serve:4200 </span>
        -wait tcp:<span class="hljs-comment">//backend:8000 </span>
        -wait tcp:<span class="hljs-comment">//selenium-chrome-standalone:4444 -timeout 10s </span>
        -wait-retry-interval <span class="hljs-number">1</span>s bash -c <span class="hljs-string">"npm run e2e:docker"</span>
   <span class="hljs-attr">depends_on</span>:
     - app-serve
     - selenium-chrome-standalone
 selenium-chrome-standalone:
   image: selenium/standalone-chrome
   <span class="hljs-attr">expose</span>:
    - <span class="hljs-number">44444</span>
</code></pre><p>In this example we added a postgres database and a container for the back end to run. Dockerize synchronizes the containers' commands.</p>
<p>If your system has more than one back end component, add as many containers as you need. You need to wire the container dependencies properly. This means proper hostnames as environment variables on your components. And order of startup if some components depend on others.</p>
<p>The Selenium tests you have written should not need any modifications. You might need to put test data in the database. To keep this step in the testing area we added the seeding script before the backend startup script. This way we are sure that things happen in the proper order:</p>
<ul>
<li>The DB starts and is ready to accept connections</li>
<li>A script seeds the DB data</li>
<li>The back end and the front end start – so the tests can start</li>
</ul>
<h3 id="heading-monorepository">Monorepository</h3>
<p>If you look at the back end container you can see there is a catch. It uses an image called <em>mybackend</em> built from a file located at <em>backend/Dockerfile</em>. This implies that your back end is in the same git repository in a folder called <em>backend</em>. The name is just an example of course.</p>
<p>If your back end and front end are in the same repository you are good. Define the Dockerfile to build your back end and adjust the startup command to what you need.</p>
<p>That’s all good but usually the back end is not in the same repository. Or you can have many back end components in different repositories. What do you do then?</p>
<h3 id="heading-multiple-repositories">Multiple repositories</h3>
<p>The super clean solution is to have a CI process on each back end component repository. </p>
<p>If you don’t have any you can check out the <a target="_blank" href="https://fire.ci/blog/api-end-to-end-testing-with-docker/">API end to end testing with Docker</a> guide to get started. It uses the same techniques as this article so you have a consistent setup across your whole project.</p>
<p>The CI process for each component runs automated tests. Upon success it pushes a docker image with the component to a private Docker registry. The back end container in our <em>docker-compose.yml</em> file above would use this image.</p>
<p>This solution requires a private Docker registry to store your images. You can use <a target="_blank" href="https://hub.docker.com/">Docker Hub</a> but then it becomes public. If you don't have one already and don’t plan to do so, it is not worth the effort.</p>
<p>The other solution is to use the submodules feature in git. Your back end repository becomes a virtual child of your front end repository. You just need to add the file <em>.gitmodules</em> like this to your front end repository:</p>
<pre><code>[submodule <span class="hljs-string">"backend"</span>]
  path = backend
  url = git@your:backend/repository.git
  branch = develop
</code></pre><p>Run the command <em>git submodule update --remote</em> which will pull the specified branch of the back end repository into a folder called "backend". Add as many submodules as you need if you have more than one back end component.</p>
<p>That’s it. Have your CI run the submodule command and from a file system perspective it's as if you're in a monorepository. </p>
<p>If you don’t want the back end code locally while developing the front end just don’t run the command. You’ll have an empty back end folder.</p>
<h3 id="heading-versioning-and-back-endfront-end-incompatibilities">Versioning and back end/front end incompatibilities</h3>
<p>The 2 techniques above test the front end with the latest “CI tests passed” version of your back end. That may lead to broken builds if your components are not compatible at times.</p>
<p>If they are compatible more often than not, stick to the “always test with the latest versions” approach. You’ll fix the occasional incompatibilities on the fly.</p>
<p>That won’t work, though, if incompatibilities are business as usual. In this case you need to manually control version updates. That is very easy to do.</p>
<p>You can lock the version of a component in the <em>docker-compose.yml</em> file or in the <em>.gitmodules</em> file. When pushing to the Docker registry you would tag the component image with the commit number of the corresponding code. The relevant docker-compose.yml file section becomes:</p>
<pre><code>backend:
  image: backendapp:<span class="hljs-number">34028</span>fc
</code></pre><p>Similarly the <em>.gitmodules</em> file would not target a branch head but a given commit:</p>
<pre><code>[submodule <span class="hljs-string">"backend"</span>]
  path = backend
  url = git@your:backend/repository.git
  branch = <span class="hljs-number">34028</span>fc
</code></pre><p>Bonus: version updates are versioned with your code. You can track which version was used for each build. This is useful when fixing failed builds or trying to reproduce old bugs.</p>
<p>We could push the approach to the next level. You could have a dedicated repository that would wire all your components as git modules. Bumping the versions could be a form of delivery and handover to the test/QA team.  </p>
<p>In theory it is best to keep the latest versions of components working together more often than not. And drop the need for manual versioning. If that is not the case that is OK. Ignore the purists who will tell you that you are not following best practices and so on.</p>
<blockquote>
<p>If you are just starting don't aim for the stars at first. Pick what works best for you to enjoy the benefits of automated testing right now. Then keep improving your process along the way.</p>
</blockquote>
<h2 id="heading-bonus-on-writing-maintainable-selenium-tests">Bonus on writing maintainable Selenium tests</h2>
<p>Back to Selenium and three important bits of advice to help you write good UI tests.</p>
<p>First, avoid CSS selectors if you can. Selenium works on the DOM and can identify elements by IDs or CSS or XPath. Use IDs as much as possible even if you have to add them to your app code for only this purpose. CSS and XPath selectors are shaky. As soon as your application structure changes, they will be broken.</p>
<p>Second, use the Page Objects approach. It is about encapsulating your application so selectors are not directly used in tests. If your page HTML/CSS changes, your tests will have to be rewritten to use new selectors. Page Objects abstract selectors and turn them into user actions. Here is a great article on <a target="_blank" href="https://johnfergusonsmart.com/page-objects-that-suck-less-tips-for-writing-more-maintainable-page-objects/">how to use Page Objects properly</a>.</p>
<p>Third, don’t build long user journeys in your tests. If your tests fail at the 50th action it’s going to be difficult to reproduce and fix. Create test suites that play part of the scenarios starting from the login page. This way you are always a few clicks away from the bug your tests will catch.</p>
<p>Also don’t risk writing tests that rely on state from previous actions. Test suite coupling is something you want to avoid. </p>
<p>Let's take a practical example. Say you are testing a SaaS application for schools. The use cases could be:</p>
<ul>
<li>Create a class</li>
<li>Register kids' and parents' data</li>
<li>Setup the weekly plan for the class</li>
<li>Check absences</li>
<li>Input grades</li>
</ul>
<p>Along the way you will have the login process and some navigation checks.</p>
<p>You could write a test that goes through the whole chain as described above. And this would be convenient because to declare kids you need a class to exist. To check absences you need a weekly plan in place. And you need kids to input grades. It’s a quick win to build one test suite that does all these things at first.</p>
<p>If you have nothing at the moment and want to achieve good test coverage quickly: go for it! Done is better than perfect if it allows you to catch errors in your application now.</p>
<p>The cleaner solution would be to use a baseline scenario to start smaller test suites. In the example above the baseline scenario should be to create a class and register kids' data.</p>
<p>Create a test suite that does exactly that: create a class and registered kids' and parents' data. Always run it first. If this stops working then you don’t need to move further on. This version of the code will never reach end users anyway.</p>
<p>Then create a function that encapsulates the baseline scenario. It will be duplicate code to some extent with the previous test suite. But it will allow you to have a one line function to use as a setup hook for all the other test suites. This is the best of both worlds: test scenarios starting from a fresh state in the application with minimal effort.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this article gave you some good insight into how you can quickly set up end to end tests for a complex system. Multiple components in multiple repositories should not be a barrier. Docker compose makes it easy to put things together.</p>
<p>End to end tests are the best way to avoid crunch time. In complex systems, late delivery of some components puts a burden on other teams. Integrations are done in a rush. Code quality drops. That's a vicious circle. Testing often and catching cross component errors early is the solution.</p>
<p>Selenium tests can be done quick and dirty to get going fast. That is perfectly OK. Automate things. Then improve. Remember:</p>
<blockquote>
<p>Done is better than perfect any day of the year.</p>
</blockquote>
<p>Thanks for reading! </p>
<p><em>If you want more of my articles like this, you can find them on <a target="_blank" href="https://fire.ci/blog">The Fire CI Blog</a>.</em>  </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Brilliant Add-on For Static Sites That Will Make You Dance ]]>
                </title>
                <description>
                    <![CDATA[ By Jared Wolff This post is originally from www.jaredwolff.com Privacy. Performance. Brilliant looks. Can you have all three? (Of course!) Having a statically generated blog is great. Many folks use services like Disqus and Google Analytics to make t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-setup-worry-free-blog-comments-in-20-simple-steps/</link>
                <guid isPermaLink="false">66d8505639c4dccc43d4d4a7</guid>
                
                    <category>
                        <![CDATA[ blog ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nginx ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Static Site Generators ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 08 Jul 2019 12:30:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/07/Copy-of-Static-Site-Docker-Recipes-2.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Jared Wolff</p>
<p><strong>This post is originally from <a target="_blank" href="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/">www.jaredwolff.com</a></strong></p>
<p>Privacy.</p>
<p>Performance.</p>
<p>Brilliant looks.</p>
<p>Can you have all three?</p>
<p>(Of course!)</p>
<p>Having a statically generated blog is great. Many folks use services like Disqus and Google Analytics to make them even better. Not surprising if you were one of them!  Privacy concerns are are the forefront of everyone’s attention. So, rather than keeping the status quo, it’s time to do something about it!</p>
<p><strong>If you've been looking to protect your site visitor's privacy and improve performance this blog post is for you.</strong></p>
<p>In this article we'll be using DigitalOcean’s Docker droplet. It allows you to host several different applications/services on one (virtual) machine. By the end of it you'll know how to run your own comments server using Commento. Plus i’ll share a few tricks i’ve learned along the way to make it much easier for you.</p>
<p>Leeeets go!</p>
<h2 id="heading-reverse-proxy">Reverse Proxy</h2>
<p>One of the most important aspects of this setup is the reverse proxy. A reverse proxy acts like a router. Requests come in for a certain domain.  That request is then routed to the service associated with that domain.</p>
<p>Here’s a diagram from the Nginx Reverse Proxy + Let’s Encrypt Helper documentation. It'll help illustrate the idea.</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/webproxy-1f1c7540-4b86-4478-bb3e-f05043d671a5.jpg" alt="Nginx Reverse Proxy with Let's Encrypt" width="730" height="334" loading="lazy"></p>
<p>Another benefit is that there’s an extra layer of protection to the outside world. Your websites run in a private network and the only access is through the Nginx reverse proxy. Point your DNS to the server and Nginx handles all the magic.</p>
<p>Here's how to get it setup:</p>
<ol>
<li>Go ahead and set up your Digital Ocean Droplet. <a target="_blank" href="https://marketplace.digitalocean.com/apps/docker">All the info you need is right here</a>. The $5 version is more than sufficient.</li>
<li><p><a target="_blank" href="https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion">Go here to clone the repository.</a> You can also run this in your terminal. Make sure you SSH into your Digital Ocean droplet first!</p>
<p>     git clone git@github.com:evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git</p>
</li>
<li><p>Change directories to the cloned repository.</p>
</li>
<li>Copy <code>.env.sample</code> to <code>.env</code> and update the values inside. I had to change the <code>IP</code> value to the IP of my Digital Ocean Droplet. I left all the other ones alone.</li>
<li>Run <code>docker-compose up -d</code> to start everything. (you can run without the <code>-d</code> option to make sure everything starts ok. Or you can attach the log output using <code>docker container logs -f &lt;container name</code></li>
</ol>
<p>When pointing your sub-domains to this server, make sure you use an A record. Here's an example of mine:</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-9c0432cd-4d40-4c89-88f3-24037d915eaf.52.32_PM.png" alt="NS1 A Record Configuration" width="730" height="581" loading="lazy"></p>
<p>Depending on your DNS provider, you'll have to figure out how to set an A record. That is beyond the purpose of this article though!</p>
<h2 id="heading-setting-up-commento-with-docker-compose">Setting Up Commento with Docker Compose</h2>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Compose-1c868832-6819-43e2-8696-ab698a10dbee.jpg" alt="Commento Logo with Docker Logo" width="730" height="486" loading="lazy"></p>
<p>Here is the current docker compose file i'm using for Commento. It includes a few more environment variables for configuring Github, Gitlab and Google. It also includes the environment variables for setting the SMTP settings. These parameters are important. Otherwise you can't receive password reset or moderation emails!</p>
<p>    version: '3'</p>
<p>    services:
      commento:
        image: registry.gitlab.com/commento/commento
        container_name: commento
        restart: always
        environment:
          COMMENTO_ORIGIN: https://${COMMENTS_URL}
          COMMENTO_PORT: 8080
          COMMENTO_POSTGRES: postgres://postgres:postgres@postgres:5432/commento?sslmode=disable
          COMMENTO_SMTP_HOST: ${SMTP_HOST}
          COMMENTO_SMTP_PORT: ${SMTP_PORT}
          COMMENTO_SMTP_USERNAME: ${SMTP_USERNAME}
          COMMENTO_SMTP_PASSWORD: ${SMTP_PASSWORD}
          COMMENTO_SMTP_FROM_ADDRESS: ${SMTP_FROM_ADDRESS}
          COMMENTO_GITHUB_KEY: ${COMMENTO_GITHUB_KEY}
          COMMENTO_GITHUB_SECRET: ${COMMENTO_GITHUB_SECRET}
          COMMENTO_GITLAB_KEY: ${COMMENTO_GITLAB_KEY}
          COMMENTO_GITLAB_SECRET: ${COMMENTO_GITLAB_SECRET}
          COMMENTO_GOOGLE_KEY: ${COMMENTO_GOOGLE_KEY}
          COMMENTO_GOOGLE_SECRET: ${COMMENTO_GOOGLE_SECRET}
          COMMENTO_TWITTER_KEY: ${COMMENTO_TWITTER_KEY}
          COMMENTO_TWITTER_SECRET: ${COMMENTO_TWITTER_SECRET}
          VIRTUAL_HOST: ${COMMENTS_URL}
          VIRTUAL_PORT: 8080
          LETSENCRYPT_HOST: ${COMMENTS_URL}
          LETSENCRYPT_EMAIL: ${EMAIL}
        depends_on:</p>
<ul>
<li>postgres
networks:</li>
<li>db_network</li>
<li><p>webproxy</p>
<p>postgres:
image: postgres
container_name: postgres
environment:
POSTGRES_DB: commento
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
networks:</p>
</li>
<li>db_network
volumes:</li>
<li><p>postgres_data_volume:/var/lib/postgresql/data</p>
<p>networks:
db_network:
webproxy:
external: true</p>
<p>volumes:
postgres_data_volume:</p>
</li>
</ul>
<p>To set the environment variables, put them inside an <code>.env</code> file. Make sure the <code>.env</code> file is in the same directory as <code>docker-compose.yml</code>. When you run <code>docker-compose up</code> it will apply the variables set in the <code>.env</code> file. Nothing gets set if they're left blank.</p>
<p>Set the required <code>COMMENTS_URL</code> and <code>EMAIL</code> or you may run into problems. The best way to set these is by pacing them in the <code>.env</code> file. Here is an example:</p>
<p>    COMMENTS_URL=comments.your.url
    EMAIL=you@your.url</p>
<h2 id="heading-getting-oauth-key-amp-secret">Getting OAuth Key &amp; Secret</h2>
<p>Commento works with most popular OAuth providers. Thus visitors can leave comments without making an account.</p>
<p>The instructions are similar for each. I've outlined the steps for all of them below.</p>
<h3 id="heading-twitter">Twitter</h3>
<ol>
<li><p>Login to <a target="_blank" href="http://twitter.com">Twitter.com</a> and apply for a developer account: <a target="_blank" href="https://developer.twitter.com/en/application/use-case">https://developer.twitter.com/en/application/use-case</a></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-4171cdf7-6c2b-408b-bb64-57822ede91cb.26.08_PM.png" alt="Twitter API Access" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Describe how you'll use the API. You can use what I wrote.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-4c0aecf2-c020-4005-bd5f-81e3b4ac6b8f.28.43_PM.png" alt="How will you use the API?" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Double check your entry and click <strong>Looks Good!</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-ade63510-86d3-48a4-a121-221f6e14cd96.28.50_PM.png" alt="Is everything correct?" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Agree to the terms of service.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-2e8e3089-bd51-4d27-8573-6987aafc663e.28.59_PM.png" alt="Agree to Developer Agreement" width="730" height="581" loading="lazy"></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-145b1bfd-9fc7-4ea6-ba5f-032e59d7fe8d.41.47_PM.png" alt="You did it!" width="730" height="581" loading="lazy"></p>
</li>
<li><p>They'll tell you to check your email for a confirmation. Confirm your email and you should be able to create your first app!</p>
</li>
<li><p>Once approved to to <strong>Get started</strong> click <strong>Create an app</strong>.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-640686b8-15c6-4af0-b9df-65ce15ae0fe7.29.22_PM.png" alt="Create an app" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Next screen, again click <strong>Create an app</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-de2b85d5-8bb7-428f-bfd1-2a23d0b7d4e0.29.26_PM.png" alt="Create an app" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Enter all the appropriate details. For the callback URL, use <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/github/callback</code></a> where <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>&lt;your URL&gt;</code></a> is your Commento subdomain.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-91acb343-9dee-4917-be77-9704fe439722.32.44_PM.png" alt="App details" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Finally, once you're done filling out the information to go the <strong>Keys and Token</strong>s area. Save both the key and token. Enter them into the <code>.env</code> file. You can use <code>COMMENTO_TWITTER_KEY</code> and <code>COMMENTO_TWITTER_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-b910e9ff-dc34-45e8-94df-affb06702617.33.07_PM.png" alt="Get oauth key and secret" width="730" height="581" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-gitlab">Gitlab</h3>
<ol>
<li>Login to <a target="_blank" href="http://gitlab.com">Gitlab.com</a> and go to to top right and click <strong>Settings</strong></li>
<li><p>Then click on <strong>Applications</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-c6da9d02-2052-4fa4-89de-d5212b8f49ca.56.47_PM.png" alt="Gitlab profile" width="730" height="445" loading="lazy"></p>
</li>
<li><p>Enter a name for your app. I put <strong>Commento</strong>.</p>
</li>
<li>Set the Redirect URI to <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/gitlab/callback</code></a></li>
<li><p>Select the <strong>read_user</strong> scope.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-e616c338-6144-4704-93c6-914db6fad5f6.59.15_PM.png" alt="Gitlab add application" width="730" height="500" loading="lazy"></p>
</li>
<li><p>Click the green <strong>Save Application</strong> button</p>
</li>
<li><p>Copy the <strong>Application ID</strong> and <strong>Secret</strong> and place them in your <code>.env</code> file using <code>COMMENTO_GITLAB_KEY</code> and <code>COMMENTO_GITLAB_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_1-a4f4ab4a-9fd6-423f-821c-6ff2f174e589.04.10_PM.png" alt="Application key and secret" width="730" height="689" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-github">Github</h3>
<ol>
<li>To get your OAuth key and secret, you'll need to go to this URL: <a target="_blank" href="https://github.com/settings/developers">https://github.com/settings/developers</a></li>
<li><p>Once there, click on <strong>New OAuth App</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-18bf8f23-916f-476b-8c25-3377de931fe3.15.33_AM.png" alt="Add OAuth application" width="730" height="562" loading="lazy"></p>
</li>
<li><p>Enter your details. For the callback URL, use <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/github/callback</code></a> where <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>&lt;your URL&gt;</code></a> is your Commento subdomain.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-6e616334-7123-4de4-a4fd-f2fe319b1971.28.24_AM.png" alt="Register new OAuth application" width="730" height="585" loading="lazy"></p>
<p> <em>Note: Make sure you include <code>https</code> in your URLs.</em></p>
</li>
<li><p>Grab the <strong>Client ID</strong> and <strong>Client secret</strong> and put that into your <code>.env</code> file using <code>COMMENTO_GITHUB_KEY</code> and <code>COMMENTO_GITHUB_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-7505a3ef-386a-4b75-a7dc-1dd3e22d0baf.29.28_AM.png" alt="Application created successfully" width="730" height="585" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-google">Google</h3>
<p>Setting up Google is just about as tedious to set up as Twitter. Despite how scary I just made it out to be, it's completely doable. Here are the steps.</p>
<ol>
<li>Go to this URL: <a target="_blank" href="https://console.developers.google.com/cloud-resource-manager?previousPage=%2Fapi">Google Developer Console</a></li>
<li><p>Create a new project</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-f3793926-cc54-4345-b81c-5ec0f4631a35.42.48_AM.png" alt="Create a new project" width="730" height="588" loading="lazy"></p>
</li>
<li><p>Click the <strong>GoogleAPIs logo</strong> in the top left corner to go back once you have a project. (Make sure the dropdown next to the <strong>GoogleAPIs logo</strong> is the same as your new project!)</p>
</li>
<li>Then, click <strong>Credentials</strong> on the left side.</li>
<li><p>Update the <strong>Application Name</strong> and <strong>Authorized Domains</strong> in the <strong>OAuth consent screen</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-d839a5c9-3368-4f18-b674-73b6e4e7c17c.47.15_AM.png" alt="Setup application" width="730" height="499" loading="lazy"></p>
</li>
<li><p>Click <strong>Create credentials</strong> then <strong>OAuth client ID</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-201545f9-4d47-4e0c-ae9a-b40efdc35a4b.44.36_AM.png" alt="Setup credentials" width="730" height="545" loading="lazy"></p>
</li>
<li><p>On the <strong>Create OAuth client ID</strong> enter the subdomain associated with Commento to <strong>Authorized Javascript origins.</strong> Then, enter the full callback URL. For example <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://comments.jaredwolff.com/api/oauth/google/callback</code></a>. Make it yours by replacing <code>comments.jaredwolff.com</code> with your URL.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-fdba3491-d562-41f3-acff-2857ea816cec.52.15_AM.png" alt="Create OAuth Client ID" width="730" height="706" loading="lazy"></p>
<p> Once entered, click the <strong>create</strong> button.</p>
</li>
<li><p>Grab the <strong>client ID</strong> and <strong>client secret</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-0c3f2895-0cb9-4b3a-a154-a3d80fd9716a.57.40_AM.png" alt="OAuth Credentials" width="730" height="706" loading="lazy"></p>
</li>
<li><p>Update your <code>.env</code> file using <code>COMMENTO_GOOGLE_KEY</code> and <code>COMMENTO_GOOGLE_SECRET</code></p>
</li>
</ol>
<h2 id="heading-install-your-application">Install your application</h2>
<p>You've entered your OAuth Credentials email, domain and SMTP credentials. It's time to wrap this show up!</p>
<ol>
<li>Once you're done editing your <code>.env</code> file. Run <code>docker-compose up</code> (For files not named <code>docker-compose.yml</code>, use the <code>-f</code> flag. Example: <code>docker-compose -f commento.yml up</code></li>
<li>Watch the output for errors. If it looks good you may want to kill it (<strong>CTRL+C</strong>) and run with the <code>-d</code> flag</li>
<li><p>On first start, Commento will prompt you with a login screen.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-d5a1ca53-93b3-49c5-a3a7-e8b728259e2d.11.29_PM.png" alt="Commento Login" width="730" height="545" loading="lazy"></p>
</li>
<li><p>Create a new account by clicking <strong>Don't have an account yet? Sign up.</strong></p>
</li>
<li>Enter your information and click <strong>Sign Up</strong></li>
<li><p>Check your email and click the included link:</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-e263aa4f-201b-42ac-986c-b28c5f003f38.12.48_PM.png" alt="Validation email with link" width="730" height="733" loading="lazy"></p>
</li>
<li><p>Log in with your freshly made account.</p>
</li>
<li><p>Then, click <strong>Add a New Domain.</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-46acfe9c-f3f4-4d3e-b8fb-97fbff643a86.10.47_PM.png" alt="Add new domain" width="730" height="598" loading="lazy"></p>
</li>
<li><p>Once created go to <strong>Installation Guide.</strong>  Copy the snippet and place it where ever you want your comments to live. In my case, I put the snippet in an area just after my <code>&lt;article&gt;</code> tag.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-f78f36c5-f3f7-45ec-971d-9bf0bf7b7d1f.36.35_PM.png" alt="Code snippet" width="730" height="589" loading="lazy"></p>
</li>
<li><p>Re-compile your site and check for success!</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-8f7ffbdc-c49f-49bc-95bb-1f53a926f361.30.27_PM.png" alt="Blog comment section with checkmarks" width="730" height="589" loading="lazy"></p>
<p>Checkmark! Finally, I recommend you try logging in with each individual OAuth configuration. That way you know it working for your website visitors. ?</p>
</li>
</ol>
<h2 id="heading-alternatives">Alternatives</h2>
<p>I spent a good chunk playing around with some of the alternatives. This is by no means a definitive guide on what will work best for your site. Here are some of the top ones as of this writing:</p>
<p><a target="_blank" href="https://utteranc.es/#configuration">https://utteranc.es/#configuration</a></p>
<p><a target="_blank" href="https://github.com/netlify/gotell">https://github.com/netlify/gotell</a></p>
<p><a target="_blank" href="https://github.com/eduardoboucas/staticman">https://github.com/eduardoboucas/staticman</a></p>
<p><a target="_blank" href="https://posativ.org/isso/">https://posativ.org/isso/</a></p>
<p><a target="_blank" href="https://www.remarkbox.com/">https://www.remarkbox.com</a></p>
<p><a target="_blank" href="https://www.vis4.net/blog/2017/10/hello-schnack/">https://www.vis4.net/blog/2017/10/hello-schnack/</a></p>
<p><a target="_blank" href="https://github.com/gka/schnack">https://github.com/gka/schnack</a></p>
<p>There's also a huge thread over at the Hugo blog which has a ton more links and resources as well:</p>
<p><a target="_blank" href="https://discourse.gohugo.io/t/alternative-to-disqus-needed-more-than-ever/5516">https://discourse.gohugo.io/t/alternative-to-disqus-needed-more-than-ever/5516</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congrats! You are now hosting your own comments server! ?</p>
<p>In this article you've learned how to harness the power of Docker and a Nginx Reverse Proxy. As an added bonus, you know how to  set up OAuth credentials! That way future setup will be easy peasy.</p>
<p>By the way, this is only the tip of the iceberg. You can set up the same server for analytics, data collection and more. <a target="_blank" href="https://www.jaredwolff.com/files/host-your-comments/">All the example code including code for other applications can be found here.</a></p>
<p>Finally, if you're looking pay for Commento head to <a target="_blank" href="http://www.commento.io">www.commento.io</a> and sign up for the service. You'll be supporting awesome open source software!</p>
<p>If you have comments and questions let's hear em'. Start the conversation down below. ???</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Practical Introduction to Docker Compose ]]>
                </title>
                <description>
                    <![CDATA[ By Faizan Bashir Docker containers opened a world of possibilities for the tech community, hassles in setting up new software were decreased unlike old times when a mess was to be sorted by a grievous format, it reduced the time to set up and use new... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-practical-introduction-to-docker-compose/</link>
                <guid isPermaLink="false">66d45edd182810487e0ce184</guid>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker Containers ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 05 Jul 2019 18:15:08 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/07/1_JK4VDnsrF6YnAb2nyhMsdQ-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Faizan Bashir</p>
<p>Docker containers opened a world of possibilities for the tech community, hassles in setting up new software were decreased unlike old times when a mess was to be sorted by a grievous format, it reduced the time to set up and use new software which eventually played a big part for techies to learn new things, roll it out in a container and scrap it when done. Things became easy, and the best thing its open source anyone and everyone can use it, comes with a little learning curve though.</p>
<p>Out of the myriad possibilities was the possibility of implementing complex technology stacks for our applications, which previously would have been the domain of experts. Today with the help of containers software engineers with sound understanding of the underlying systems can implement a complex stack and why not it’s the need of the hour, the figure of speech “Jack of all trades” got a fancy upgrade; “Master of some” based on the needs of the age. Simply put “T” shaped skills.</p>
<p>The possibility of defining a complex stack in a file and running it with a single command, pretty tempting huh. The guys at Docker Inc. choose to call it Docker compose.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*1g8v7eeFV2OWt1Tkmoc-4A.jpeg" alt="Image" width="750" height="422" loading="lazy"></p>
<p>In this article, we will use Docker’s example Voting App and deploy it using Docker compose.</p>
<hr>
<h3 id="heading-docker-compose">Docker Compose</h3>
<p>In the words of Docker Inc.</p>
<blockquote>
<p><em>Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.</em></p>
</blockquote>
<hr>
<h3 id="heading-the-voting-app">The Voting App</h3>
<p>Introducing the most favourite demonstration app for the Docker community “The Voting App”, as if it needs an introduction at all. This is a simple application based on micro-services architecture, consisting of 5 simple services.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*DIZdPFJO4EQbPNq0pR_b8g.png" alt="Image" width="1024" height="768" loading="lazy"></p>
<p><em>Voting app architecture:</em> <a target="_blank" href="https://github.com/docker/example-voting-app"><em>https://github.com/docker/example-voting-app</em></a></p>
<ol>
<li><p><strong>Voting-App</strong>: Frontend of the application written in Python, used by users to cast their votes.</p>
</li>
<li><p><strong>Redis</strong>: In-memory database, used as intermediate storage.</p>
</li>
<li><p><strong>Worker</strong>: .Net service, used to fetch votes from Redis and store in Postres database.</p>
</li>
<li><p><strong>DB</strong>: PostgreSQL database, used as database.</p>
</li>
<li><p><strong>Result-App</strong>: Frontend of the application written in Node.js, displays the voting results.</p>
</li>
</ol>
<p>The Voting repo has a file called <code>docker-compose.yml</code> this file contains the configuration for creating the containers, exposing ports, binding volumes and connecting containers through networks required for the voting app to work. Sounds like a lot of pretty long <code>docker run</code> and <code>docker network create</code> commands otherwise, docker compose allows us to put all of that stuff in a single docker-compose file in <a target="_blank" href="http://yaml.org/start.html">yaml</a> format.</p>
<pre><code class="lang-bash">version: <span class="hljs-string">"3"</span>

services:
  vote:
    build: ./vote
    <span class="hljs-built_in">command</span>: python app.py
    volumes:
     - ./vote:/app
    ports:
      - <span class="hljs-string">"5000:80"</span>
    networks:
      - front-tier
      - back-tier

  result:
    build: ./result
    <span class="hljs-built_in">command</span>: nodemon server.js
    volumes:
      - ./result:/app
    ports:
      - <span class="hljs-string">"5001:80"</span>
      - <span class="hljs-string">"5858:5858"</span>
    networks:
      - front-tier
      - back-tier

  worker:
    build:
      context: ./worker
    depends_on:
      - <span class="hljs-string">"redis"</span>
    networks:
      - back-tier

  redis:
    image: redis:alpine
    container_name: redis
    ports: [<span class="hljs-string">"6379"</span>]
    networks:
      - back-tier

  db:
    image: postgres:9.4
    container_name: db
    volumes:
      - <span class="hljs-string">"db-data:/var/lib/postgresql/data"</span>
    networks:
      - back-tier

volumes:
  db-data:

networks:
  front-tier:
  back-tier:
</code></pre>
<p>Git <code>clone</code> and <code>cd</code> into the voting app repo.</p>
<p><a target="_blank" href="https://github.com/dockersamples/example-voting-app"><strong>dockersamples/example-voting-app</strong></a><br><a target="_blank" href="https://github.com/dockersamples/example-voting-app">_example-voting-app - Example Docker Compose app_github.com</a></p>
<hr>
<h3 id="heading-compose-time">Compose Time</h3>
<p>With all of our application defined in a single compose file we can take a sigh of relief, chill and simply run the application. The beauty of compose lies in the fact that a single command creates all the services, wires up the networks(literally), mounts all volumes and exposes the ports. Its time to welcome the <code>up</code> command, its performs all of the aforementioned tasks.$ docker-compose up</p>
<p>After lots of “Pull complete”, hundreds of megabytes and few minutes (maybe more). . .</p>
<p>Voila, we have the voting app up and running.</p>
<p>Command <code>docker ps</code> lists all the running containers</p>
<pre><code class="lang-bash">$ docker ps -a --format=<span class="hljs-string">"table {{.Names}}\t{{.Image}}\t{{.Ports}}"</span> 
NAMES               IMAGE               PORTS
voting_worker_1     voting_worker      
db                  postgres:9.4        5432/tcp
voting_vote_1       voting_vote         0.0.0.0:5000-&gt;80/tcp
voting_result_1     voting_result       0.0.0.0:5858-&gt;5858/tcp, 0.0.0.0:5001-&gt;80/tcp
redis               redis:alpine        0.0.0.0:32768-&gt;6379/tcp
</code></pre>
<p>The above command displays all the running containers, respective images and the exposed port numbers.</p>
<p>The Voting app can be accessed on <a target="_blank" href="http://localhost:5000/">http://localhost:5000</a></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*2OBAYVFG35tX6dHI08TWPg.png" alt="Image" width="1365" height="648" loading="lazy"></p>
<p>Likewise the Voting results app can be accessed on <a target="_blank" href="http://localhost:5001/">http://localhost:5001</a></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*E-WleHhSji49ZLIafS8xgQ.png" alt="Image" width="1365" height="648" loading="lazy"></p>
<p>Each vote cast on the Voting app is first stored in the Redis in-memory database, the .Net worker service fetches the vote and stores it in the Postgres DB which is accessed by the Node.js frontend.</p>
<hr>
<h3 id="heading-compose-features">Compose Features</h3>
<p>Compose provide the flexibility to use a project name to isolate the environments from each other, the project name is the base name of the directory that contains the project. In our voting app this is signified by the name of the containers <code>voting_worker_1</code> where <code>voting</code> is the base name of the directory. We can set a custom project name using the <code>-p</code> flag followed by the custom name.</p>
<p>Compose preserves all volumes used by the services defined in the compose file, thus no data is lost when the containers are recreated using <code>docker-compose up</code>. Another cool feature is that only the containers which have changed are recreated, the containers whose state did not change remain untouched.</p>
<p>Another cool feature is the support for variables in the compose file, we can define variables in a <code>.env</code> file and use them in the docker-compose file. Here the <code>POSTGRES_VERSION=9.4</code> can defined in the environment file or can be defined in the shell. It is used in the compose file in the following manner:</p>
<pre><code class="lang-bash">db:  
  image: <span class="hljs-string">"postgres:<span class="hljs-variable">${POSTGRES_VERSION}</span>"</span>
</code></pre>
<hr>
<h3 id="heading-command-cheat-sheet">Command Cheat Sheet</h3>
<p>It's easy as breeze to start, stop and play around with compose.</p>
<pre><code class="lang-bash">$ docker-compose up -d
$ docker-compose down
$ docker-compose start
$ docker-compose stop
$ docker-compose build
$ docker-compose logs -f db
$ docker-compose scale db=4
$ docker-compose events
$ docker-compose <span class="hljs-built_in">exec</span> db bash
</code></pre>
<hr>
<h3 id="heading-summary">Summary</h3>
<p>Docker Compose is a great tool to quickly deploy and scrap containers, the compose file can run seamlessly on any machine installed with docker-compose. Experimentation and learning technologies is just a Compose file away ;).</p>
<p>I hope this article helped in the understanding of Docker Compose. I’d love to hear about how you use Docker Compose in your projects. Share the knowledge, help it reach more people.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
