<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>docker &#8211; Other Things</title>
	<atom:link href="https://blog.adamzolo.com/category/docker/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.adamzolo.com</link>
	<description>Blog about Things by Adam Zolotarev</description>
	<lastBuildDate>Mon, 26 Jan 2026 16:10:56 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.1</generator>
	<item>
		<title>How We Fixed the &#8220;First Web Container is Unhealthy&#8221; Error: A DNS Deep Dive</title>
		<link>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/</link>
					<comments>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 16:10:56 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Rails]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1082</guid>

					<description><![CDATA[The Error That Nearly Broke Our Deployment Three hours into our Kamal deployment, we were stuck in a loop: ERROR Failed to boot web on {ip_address} INFO First web container is unhealthy on {ip_address}, not booting any other roles The container would start, but Kamal&#8217;s health check kept failing. After 30 seconds, Kamal would kill&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/" title="Continue reading &#8216;How We Fixed the &#8220;First Web Container is Unhealthy&#8221; Error: A DNS Deep Dive&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[<br />
<h2>The Error That Nearly Broke Our Deployment</h2>
<p>Three hours into our Kamal deployment, we were stuck in a loop:</p>
<pre><code>ERROR Failed to boot web on {ip_address}
  INFO First web container is unhealthy on {ip_address}, not booting any other roles</code></pre>
<p>The container would start, but Kamal&#8217;s health check kept failing. After 30 seconds, Kamal would kill the container<br />
   and retry, creating an endless loop.</p>
<p>We spent hours debugging deployment scripts, PostgreSQL configurations, and Rails settings. The fix turned out to<br />
  be much simpler: DNS configuration.</p>
<h2>The Root Cause: Broken DNS Resolution</h2>
<h3>What Was Happening</h3>
<p>When Kamal tried to verify container health, it performed this sequence:</p>
<ol>
<li>Container starts → my_app-web-abc123 boots</li>
<li>Traefik (Kamal proxy) tries to check /up endpoint</li>
<li>DNS lookup → Resolve my_app-web-abc123 to an IP address</li>
<li>Health check fails → DNS resolution times out or fails</li>
<li>Container killed → Kamal marks it as unhealthy</li>
</ol>
<h3>The DNS Failure</h3>
<p>The Traefik container&#8217;s /etc/resolv.conf showed:</p>
<pre><code>nameserver 127.0.0.53
  search members.linode.com
  options edns0 trust-ad ndots:0</code></pre>
<p><strong>Problem:</strong> 127.0.0.53 is the host&#8217;s systemd-resolved DNS server. It&#8217;s not accessible from inside<br />
  Docker containers!</p>
<p>When Traefik tried to resolve my_app-web-abc123:</p>
<ul>
<li>It queried 127.0.0.53 (systemd-resolved)</li>
<li>The query failed with &#8220;connection refused&#8221;</li>
<li>Health check failed</li>
<li>Container was killed</li>
</ul>
<h2>The Solution: Proper Docker DNS Configuration</h2>
<h3>What We Fixed</h3>
<p>We configured Docker&#8217;s DNS settings in <code>/etc/docker/daemon.json</code>:</p>
<pre><code>{
    "dns": ["127.0.0.11", "8.8.8.8", "1.1.1.1"]
  }</code></pre>
<h3>Why This Works</h3>
<p><strong>1. 127.0.0.11 (Docker&#8217;s Internal DNS) &#8211; First Priority</strong></p>
<ul>
<li>Resolves container hostnames automatically</li>
<li>Handles inter-container communication</li>
<li>Always available inside Docker networks</li>
</ul>
<p><strong>2. 8.8.8.8 (Google DNS) &#8211; Second Priority</strong></p>
<ul>
<li>Resolves external domains (APIs, gems, etc.)</li>
<li>Fast and reliable</li>
<li>Global infrastructure</li>
</ul>
<p><strong>3. 1.1.1.1 (Cloudflare DNS) &#8211; Third Priority</strong></p>
<ul>
<li>Privacy-focused external DNS</li>
<li>Backup if 8.8.8.8 fails</li>
<li>No query logging</li>
</ul>
<h3>How Docker Uses This</h3>
<p>Docker&#8217;s DNS resolution order:</p>
<ol>
<li>Try 127.0.0.11 (internal) → container names</li>
<li>If that fails → 8.8.8.8 (external) → domains</li>
<li>If that fails → 1.1.1.1 (external) → domains</li>
</ol>
<h2>The IPv4/IPv6 Issue</h2>
<p>While debugging, we discovered another subtle problem:</p>
<h3>The IPv6 Trap</h3>
<p>The server setup script used:</p>
<pre><code>SERVER_IP=$(curl -s ifconfig.me || echo "ip_address_goes_here")</code></pre>
<p><strong>Problem:</strong> ifconfig.me returned an IPv6 address:</p>
<pre><code>2600:3c03::...</code></pre>
<p>This IPv6 address was used in PostgreSQL&#8217;s pg_hba.conf:</p>
<pre><code>host my_app_production my_app_user 2600:3c03.../32 md5</code></pre>
<p>PostgreSQL had issues with this IPv6 address, causing authentication failures.</p>
<h3>The Fix</h3>
<p>Force IPv4 detection:</p>
<pre><code>SERVER_IP=$(curl -s -4 ifconfig.me || echo "ip_address_goes_here")</code></pre>
<p>The <code>-4</code> flag ensures we always get an IPv4 address, which PostgreSQL handles reliably.</p>
<h2>The PostgreSQL Network Isolation Issue</h2>
<h3>The Problem</h3>
<p>Kamal uses a separate Docker network (172.18.0.0/16) for containers, while PostgreSQL is on the host&#8217;s Docker<br />
  bridge network (172.17.0.0/16).</p>
<p>The firewall only allowed 172.17.0.0/16:</p>
<pre><code>5432/tcp  ALLOW  172.17.0.0/16</code></pre>
<h3>The Fix</h3>
<p>Add the Kamal network to both firewall and PostgreSQL config:</p>
<p><strong>Firewall (ufw):</strong></p>
<pre><code>sudo ufw allow from 172.18.0.0/16 to any port 5432</code></pre>
<p><strong>PostgreSQL (pg_hba.conf):</strong></p>
<pre><code>host my_app_production my_app_user 172.18.0.0/16 md5</code></pre>
<h2>Complete Fix in our setup script</h2>
<h3>IPv4 Fix </h3>
<pre><code>SERVER_IP=$(curl -s -4 ifconfig.me || echo "ip_address_goes_here")</code></pre>
<h3>Kamal Network Firewall Rule</h3>
<pre><code>sudo ufw allow from 172.18.0.0/16 to any port 5432</code></pre>
<h3>PostgreSQL Kamal Network Rule </h3>
<pre><code>host $DB_NAME $DB_USER 172.18.0.0/16 md5</code></pre>
<h3>Docker DNS Configuration </h3>
<pre><code>{
    "dns": ["127.0.0.11", "8.8.8.8", "1.1.1.1"]
  }</code></pre>
<h2>Key Takeaways</h2>
<ol>
<li><strong>DNS is Critical for Container Orchestration</strong>
<ul>
<li>Always configure Docker&#8217;s DNS properly</li>
<li>Include both internal and external DNS servers</li>
<li>Test DNS resolution from containers</li>
</ul>
</li>
<li><strong>Network Isolation Matters</strong>
<ul>
<li>Docker networks are isolated by default</li>
<li>PostgreSQL must allow connections from all Docker networks</li>
<li>Firewall rules must match</li>
</ul>
</li>
<li><strong>IPv4 vs IPv6 Can Break Things</strong>
<ul>
<li>PostgreSQL works better with IPv4</li>
<li>Force IPv4 when detecting server IPs</li>
<li>Test both IPv4 and IPv6 connectivity</li>
</ul>
</li>
<li><strong>Health Checks are Essential</strong>
<ul>
<li>The /up endpoint is critical for Kamal</li>
<li>DNS must work for health checks to succeed</li>
<li>Timeout settings matter (30s default)</li>
</ul>
</li>
</ol>
<h2>Troubleshooting DNS Issues</h2>
<p>If you encounter &#8220;First web container is unhealthy&#8221;:</p>
<ol>
<li><strong>Check Container Logs</strong><br />
  <code>docker logs my_app-web-abc123</code></li>
<li><strong>Check Traefik/Kamal Proxy Logs</strong><br />
  <code>docker logs kamal-proxy | grep -i healthcheck</code></li>
<li><strong>Test DNS Resolution</strong>
<pre><code># From inside Traefik container
  docker exec kamal-proxy getent hosts my_app-web-abc123
  docker exec kamal-proxy getent hosts google.com</code></pre>
</li>
<li><strong>Verify DNS Configuration</strong>
<pre><code># Check daemon.json
  cat /etc/docker/daemon.json

  # Check container's resolv.conf
  docker exec kamal-proxy cat /etc/resolv.conf</code></pre>
</li>
<li><strong>Check PostgreSQL Connectivity</strong>
<pre><code># From kamal network
  docker run --rm --network kamal postgres:16 psql \
    -h 172.17.0.1 -U my_app_user -d my_app_production -c "SELECT 1"</code></pre>
</li>
</ol>
<h2>Results</h2>
<p>After implementing all fixes:</p>
<ul>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DNS resolution works (internal and external)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Health checks pass (Traefik can reach containers)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PostgreSQL connections work (from both Docker networks)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Deployments succeed (consistent, reliable)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IPv4 detection works (no IPv6 issues)</li>
</ul>
<h2>Final Thoughts</h2>
<p>The &#8220;First web container is unhealthy&#8221; error can be a DNS configuration issue, not a deployment or application<br />
  problem.</p>
<p>By understanding how Docker networks work, how DNS resolution functions, and how PostgreSQL authentication works, we can prevent this issue from ever occurring again.</p>
<p><strong>Key files to review:</strong></p>
<ul>
<li><code>/etc/docker/daemon.json</code> &#8211; Docker DNS configuration</li>
<li><code>/etc/postgresql/16/main/pg_hba.conf</code> &#8211; PostgreSQL authentication</li>
<li><code>/etc/ufw/rules.conf</code> &#8211; Firewall rules</li>
</ul>
<p>The fix is now automated in our setup script, ensuring new servers have proper DNS and network configuration from<br />
  day one.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Docker MySQL with a Custom SQL Script for Development</title>
		<link>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/</link>
					<comments>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Tue, 04 Jan 2022 15:07:10 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=988</guid>

					<description><![CDATA[The setup is similar to setting up MariaDB. Start with standard docker-compose file. If using custom SQL mode, specify the necessary options in the command options: Add dev.dockerfile: Finally, add your init.sql file. Let&#8217;s give all privileges to our dev_user and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/" title="Continue reading &#8216;Docker MySQL with a Custom SQL Script for Development&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>The setup is similar to <a rel="noreferrer noopener" href="https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/" target="_blank">setting up MariaDB</a>.</p>



<p>Start with standard docker-compose file. If using custom SQL mode, specify the necessary options in the command options:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
version: &quot;3.7&quot;
services:
    mysql:
        build:
            context: .
            dockerfile: dev.dockerfile
        restart: always
        command: --sql_mode=&quot;STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE&quot;
        environment:
            MYSQL_ROOT_PASSWORD: root_password
            MYSQL_DATABASE: dev
            MYSQL_USER: dev_user
            MYSQL_PASSWORD: dev_password
        ports:
            - 3306:3306

</pre></div>


<p>Add <code>dev.dockerfile</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
FROM mysql:8.0.17

ADD init.sql /docker-entrypoint-initdb.d/ddl.sql


</pre></div>


<p><meta charset="utf-8">Finally, add your <code>init.sql</code> file. Let&#8217;s give all privileges to our <code>dev_user</code> and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older packages that require the less secure  au</p>



<p>Finally, add your <code>init.sql</code> file. Let&#8217;s give all privileges to our <code>dev_user</code> and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older packages that require the less secure <meta charset="utf-8">mysql_native_password authentication method):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
GRANT ALL PRIVILEGES ON *.* TO &#039;dev_user&#039;@&#039;%&#039;;
ALTER USER &#039;dev_user&#039;@&#039;%&#039; IDENTIFIED WITH mysql_native_password BY &#039;dev_password&#039;;
</pre></div>


<p><meta charset="utf-8">If you want to access the database container from other containers, while running them separately, you can specify <code>host.docker.internal</code> as the host address of your database.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Dockerizing MariaDB with a Custom SQL Script in Development</title>
		<link>https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/</link>
					<comments>https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Mon, 12 Apr 2021 20:05:13 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=974</guid>

					<description><![CDATA[Start with standard docker-compose file. Add dev.dockerfile: Finally, add your init.sql file. Let&#8217;s give all privileges to our sql_user: Now, run docker-compose build, then docker-compose up. Access from another container If you want to access the database container from other containers, while running them separately, you can specify host.docker.internal as the address of your database.&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/" title="Continue reading &#8216;Dockerizing MariaDB with a Custom SQL Script in Development&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>Start with standard docker-compose file.</p>



<pre class="wp-block-code"><code>version: "3.7"
services:
    mariadb:
        build:
            context: .
            dockerfile: dev.dockerfile
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: password
            MYSQL_DATABASE: db_name
            MYSQL_USER: sql_user
            MYSQL_PASSWORD: password
        ports:
            - 3306:3306</code></pre>



<p>Add <code>dev.dockerfile</code>:</p>



<pre class="wp-block-code"><code>FROM mariadb:latest

ADD init.sql /docker-entrypoint-initdb.d/ddl.sql</code></pre>



<p>Finally, add your <code>init.sql</code> file. Let&#8217;s give all privileges to our <code>sql_user</code>:</p>



<pre class="wp-block-code"><code>GRANT ALL PRIVILEGES ON *.* TO 'sql_user'@'%';</code></pre>



<p>Now, run <code>docker-compose build</code>, then <code>docker-compose up</code>.</p>



<p></p>



<h2 class="wp-block-heading">Access from another container</h2>



<p>If you want to access the database container from other containers, while running them separately, you can specify <code>host.docker.internal</code> as the address of your database.</p>



<p>If you&#8217;re on linux, then you need docker engine &gt;= 20.03, and you need to add to your docker-compose file:</p>



<pre class="wp-block-code"><code>  my_app:
    extra_hosts:
      - "host.docker.internal:host-gateway"</code></pre>



<p>If you&#8217;re are on Mac ^^ will break your setup unless you are at least on Docker Desktop for Mac 3.3.0. See <a href="https://github.com/docker/for-linux/issues/264">Support host.docker.internal DNS name to host · Issue #264 · docker/for-linux (github.com)</a> for details.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>docker-compose build and deployment for Angular</title>
		<link>https://blog.adamzolo.com/docker-compose-build-and-deployment-for-angular/</link>
					<comments>https://blog.adamzolo.com/docker-compose-build-and-deployment-for-angular/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Fri, 02 Apr 2021 19:24:50 +0000</pubDate>
				<category><![CDATA[angular]]></category>
		<category><![CDATA[docker]]></category>
		<guid isPermaLink="false">http://blog.adamzolo.com/?p=959</guid>

					<description><![CDATA[In this tutorial we&#8217;ll make docker-compose files for angular and write a simple deploy script to build and deploy the images from your local machine. Development Let&#8217;s start with the dev environment. First, add .dockerignore file in the root of your project: Create .docker directory in the root of your project. Add dev.dockerfile: We are&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/docker-compose-build-and-deployment-for-angular/" title="Continue reading &#8216;docker-compose build and deployment for Angular&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>In this tutorial we&#8217;ll make docker-compose files for angular and write a simple deploy script to build and deploy the images from your local machine.</p>



<h2 class="wp-block-heading">Development</h2>



<p>Let&#8217;s start with the dev environment. First, add <code>.dockerignore</code> file in the root of your project:</p>



<pre class="wp-block-code"><code>.git
.gitignore
.vscode
docker-compose*.yml
Dockerfile
node_modules
</code></pre>



<p>Create <code>.docker</code> directory in the root of your project. Add <code>dev.dockerfile</code>:</p>



<pre class="wp-block-code"><code>FROM node:10

RUN mkdir /home/node/app &amp;&amp; chown node:node /home/node/app
RUN mkdir /home/node/app/node_modules &amp;&amp; chown node:node /home/node/app/node_modules
WORKDIR  /home/node/app
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --quiet
COPY --chown=node:node . .</code></pre>



<p>We are using node 10 image and using a less privileged <code>node</code> user. <code>npm ci</code> &#8220;is similar to&nbsp;<a href="https://docs.npmjs.com/cli-commands/install"><code>npm install</code></a>, except it&#8217;s meant to be used in automated environments such as test platforms, continuous integration, and deployment &#8212; or any situation where you want to make sure you&#8217;re doing a clean install of your dependencies.&#8221; &#8211; <a href="https://docs.npmjs.com/cli/v7/commands/npm-ci#description" target="_blank" rel="noreferrer noopener">npm-ci | npm Docs (npmjs.com)</a></p>



<p>Create <code>docker-compose.yml</code> file in the root of your project:</p>



<pre class="wp-block-code"><code># docker-compose
version: '3.7'
services:
services:
  app:
    container_name: 'your-container-name'
    build:
      context: .
      dockerfile: .docker/dev.dockerfile
    command: sh -c "npm start"
    ports:
      - 4200:4200
    working_dir: /home/node/app
    volumes:
      - ./:/home/node/app
      - node_modules:/home/node/app/node_modules
volumes:
  node_modules:


</code></pre>



<p>With this setup, the node_modules will be overridden when we build a new container. Basically, this means you may have to run <code>docker-compose run app npm install</code> when you need to update your packages. Rebuilding the image is not going to do it for you.</p>



<p>For alternative setups, check out this <a href="https://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds" data-type="URL" data-id="https://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds" target="_blank" rel="noreferrer noopener">stackoverflow answer</a>.</p>



<p>In you <code>package.json</code> you should have the definition of the npm start command:</p>



<pre class="wp-block-code"><code>"scripts": {
    "ng": "ng",
    "start": "ng serve --host 0.0.0.0",
    "build": "ng build"
  },</code></pre>



<p>Run <code>docker-compose build</code> and <code>docker-compose up</code>.</p>



<h2 class="wp-block-heading">Deployment</h2>



<h3 class="wp-block-heading">Docker Setup</h3>



<p>Let&#8217;s add <code>production.dockerfile</code> to <code>.docker</code> directory:</p>



<pre class="wp-block-code"><code># Stage 1
FROM node:10 as node

RUN mkdir /home/node/app &amp;&amp; chown node:node /home/node/app
RUN mkdir /home/node/app/node_modules &amp;&amp; chown node:node /home/node/app/node_modules
WORKDIR  /home/node/app
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --quiet
COPY --chown=node:node . .

# max_old_space_size is optional but can help when you have a lot of modules
RUN node --max_old_space_size=4096 node_modules/.bin/ng build --prod

# Stage 2
# Using a light-weight nginx image
FROM nginx:alpine

COPY --from=node /home/node/app/dist /usr/share/nginx/html
COPY --from=node /home/node/app/.docker/nginx.conf /etc/nginx/conf.d/default.conf
</code></pre>



<p>Add <code>docker-compose.production.yml</code> file:</p>



<pre class="wp-block-code"><code>version: '3.7'
services:
services:
  app:
    build:
      context: .
      dockerfile: .docker/production.dockerfile
    image: production-image
    container_name: production-container
    ports:
      - 80:80</code></pre>



<h3 class="wp-block-heading">Deploy script</h3>



<p>We are going to ssh into our destination server and copy the updated image directly. Using a repository has a lot of advantages over this approach, but if you need something simple this will work:</p>



<pre class="wp-block-code"><code>#!/bin/sh

# Build the image locally, upload to your production box and start the new container based on the latest image

{
    echo "Create an image"
    docker-compose -f docker-compose.yml -f docker-compose.production.yml build

    echo "Upload the latest image"
    echo $(date +"%T")
    docker save production-image:latest | ssh -C user@your_server_ip docker load

    echo "Stop and restart containers"
    ssh -C user@your_server_ip "echo Stopping container at $(date +'%T'); \
        docker stop production-container || true; \
        docker rm production-container || true; \
        docker container run -d --restart unless-stopped -p 80:80 --name production-container production-image:latest; \
        echo Restarted container at $(date +'%T'); \
        docker image prune -f || true"

    echo "Finished"
    echo $(date +"%T")
} || {
    # catch
    echo "Something went wrong"
}

</code></pre>



<p>We are starting a new container based on the latest uploaded image on our destination host and mapping the host port 80 to the container port 80.</p>



<p></p>



<p><strong>Helpful resources:</strong></p>



<ul class="wp-block-list"><li><a href="https://codinglatte.com/posts/angular/docker-compose-angular-multi-environment-deployments/">Docker Compose &#8211; Angular Multi Environment Deployments (codinglatte.com)</a></li><li><a href="https://nodejs.org/en/docs/guides/nodejs-docker-webapp/">Dockerizing a Node.js web app | Node.js</a></li><li><a href="https://markfknight.dev/posts/angular-in-docker/">Angular Development in Docker &#8211; Part 1 | Electric Sheep (markfknight.dev)</a></li></ul>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/docker-compose-build-and-deployment-for-angular/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
