<?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" version="2.0"><channel><title><![CDATA[Passing secrets to Docker Container]]></title><description><![CDATA[Passing secrets to Docker Container]]></description><link>https://kunalnasa.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 13:24:59 GMT</lastBuildDate><atom:link href="https://kunalnasa.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build-time vs Run-time secrets in Docker — and how to pass them safely]]></title><description><![CDATA[In this article, we’ll explore how to securely pass build-time and run-time secrets to Docker containers in both development and production environments.

Before we jump into the implementation, let’s understand the difference between build-time and ...]]></description><link>https://kunalnasa.hashnode.dev/build-time-vs-run-time-secrets-in-docker-and-how-to-pass-them-safely</link><guid isPermaLink="true">https://kunalnasa.hashnode.dev/build-time-vs-run-time-secrets-in-docker-and-how-to-pass-them-safely</guid><category><![CDATA[Docker]]></category><category><![CDATA[secrets]]></category><category><![CDATA[build]]></category><category><![CDATA[runtime]]></category><category><![CDATA[containers]]></category><category><![CDATA[secrets management]]></category><dc:creator><![CDATA[Kunal Nasa]]></dc:creator><pubDate>Thu, 24 Apr 2025 09:25:41 GMT</pubDate><content:encoded><![CDATA[<p>In this article, we’ll explore how to securely pass build-time and run-time secrets to Docker containers in both development and production environments.</p>
<p><img src="https://www.zdnet.com/a/img/resize/61cceb5f61c1dcb4e2d7cbe4c1b5821aef5c9d72/2014/09/05/b2ed410c-34ce-11e4-9e6a-00505685119a/docker-logo.png?auto=webp&amp;width=1280" alt="Docker 1.0 brings container technology to the enterprise | ZDNET" /></p>
<p>Before we jump into the implementation, let’s understand the difference between build-time and run-time secrets.</p>
<p>Find github repo for this <a target="_blank" href="https://github.com/KunalNasa/docker-secrets">here</a></p>
<h2 id="heading-build-time-secrets">Build time secrets</h2>
<p>Build-time secrets are values your application needs during the build process, not during runtime.</p>
<p>For example, imagine you're building a blog application that fetches blog data from a database and generates static pages at build time. In this case, you’ll need to provide the database URL while the app is building.</p>
<p>Another example could be using an email service that requires public API keys to preload templates. These keys also need to be available during the build step.</p>
<h2 id="heading-run-time-secrets">Run time secrets</h2>
<p>Run-time secrets are needed when your application is up and running.</p>
<p>For instance, your Stripe private key is not required during the build phase. It is only needed when a user initiates a payment. Other examples include environment-specific credentials like JWT secrets or third-party service tokens that are only accessed during the actual execution of the app.</p>
<p>Now that we have a clear understanding, let's move on to how you can securely pass these secrets into Docker containers across different environments.</p>
<p>In this article I am using a NextJS application with Postgres to demonstrate how to pass build-time and run-time secrets securely. But the same can be applied to any application that requires build-time and run-time secrets.</p>
<h2 id="heading-setting-up-prisma-in-a-nextjs-app-optional">Setting Up Prisma in a Next.js App (Optional)</h2>
<blockquote>
<p><em>Feel free to skip this section if you're only interested in running the demo via Docker. You can clone the</em> <a target="_blank" href="https://github.com/KunalNasa/docker-secrets"><em>GitHub repo</em></a> <em>and spin it up directly using docker.</em></p>
</blockquote>
<p>Let’s start by creating a simple Next.js app and integrating it with a PostgreSQL database using Prisma.</p>
<h4 id="heading-step-1-create-a-nextjs-app">Step 1: Create a Next.js App</h4>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Once your app is ready, install Postgres locally or use a managed service like Supabase or Railway. If you want a local DB instance for development:</p>
<pre><code class="lang-bash">npm install postgres --save-dev
</code></pre>
<blockquote>
<p>(You can also use Docker to run a Postgres container if preferred.)</p>
</blockquote>
<h4 id="heading-step-2-initialize-prisma">Step 2: Initialize Prisma</h4>
<p>Install Prisma CLI:</p>
<pre><code class="lang-bash">npm install prisma --save-dev
</code></pre>
<p>Then initialize Prisma:</p>
<pre><code class="lang-bash">npx prisma init
</code></pre>
<p>This will create a <code>prisma/</code> folder with a <code>schema.prisma</code> file inside. It should look like this:</p>
<pre><code class="lang-javascript">generator client {
  provider = <span class="hljs-string">"prisma-client-js"</span>
}

datasource db {
  provider = <span class="hljs-string">"postgresql"</span>
  url      = env(<span class="hljs-string">"DATABASE_URL"</span>)
}
</code></pre>
<h4 id="heading-step-3-define-a-model">Step 3: Define a Model</h4>
<p>Below the <code>datasource</code> block, add a simple <code>Post</code> model to test things out:</p>
<pre><code class="lang-javascript">model Post {
  id        <span class="hljs-built_in">String</span>   @id @<span class="hljs-keyword">default</span>(cuid())
  title     <span class="hljs-built_in">String</span>
  content   <span class="hljs-built_in">String</span>
  published <span class="hljs-built_in">Boolean</span>  @<span class="hljs-keyword">default</span>(<span class="hljs-literal">false</span>)
}
</code></pre>
<h4 id="heading-step-4-setup-environment-and-migrate">Step 4: Setup Environment and Migrate</h4>
<p>Make sure your PostgreSQL database is running and you've updated the <code>DATABASE_URL</code> in your <code>.env</code> file.</p>
<p>Then, apply the migration:</p>
<pre><code class="lang-bash">npx prisma migrate dev --name init
</code></pre>
<p>Also, generate the Prisma client:</p>
<pre><code class="lang-bash">npx prisma generate
</code></pre>
<h4 id="heading-step-5-create-a-singleton-prisma-client">Step 5: Create a Singleton Prisma Client</h4>
<p>Install the Prisma Client package:</p>
<pre><code class="lang-bash">npm install @prisma/client
</code></pre>
<p>Create a new file: <code>src/lib/prisma.ts</code></p>
<pre><code class="lang-ts"><span class="hljs-comment">// src/lib/prisma.ts</span>
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> prisma =  <span class="hljs-keyword">new</span> PrismaClient()
</code></pre>
<p>This ensures a single Prisma instance is reused across hot reloads in development.</p>
<h4 id="heading-step-6-add-db-queries">Step 6: Add DB queries</h4>
<p>Update <code>src/app/page.tsx</code> with:</p>
<pre><code class="lang-tsx">import { prisma } from "@/lib/prisma";

export default async function Home() {
  await prisma.post.create({
    data: {
      title: "Hello " + Math.random(),
      content: "Hi there",
    },
  });

  const posts = await prisma.post.findMany();

  return (
    &lt;div&gt;
      &lt;pre&gt;{JSON.stringify(posts, null, 2)}&lt;/pre&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<blockquote>
<p>Note: <code>Home</code> is declared as an <strong>async function</strong> because we are using server-side data fetching with Prisma.</p>
</blockquote>
<p>Let’s move to the Docker implementation now.</p>
<h2 id="heading-dockerizing-the-app">Dockerizing the App</h2>
<p>Before we jump into writing a secure <code>Dockerfile</code>, let's first understand why a basic <code>Dockerfile</code> might fail when secrets are required at <strong>build time</strong>.</p>
<p>Start by creating a <code>Dockerfile</code> in the root of your project and add the following code:</p>
<pre><code class="lang-Dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">23</span>-alpine

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

<span class="hljs-keyword">COPY</span><span class="bash"> ./package.json ./package.json</span>
<span class="hljs-keyword">COPY</span><span class="bash"> ./package-lock.json ./package-lock.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">EXPOSE</span> <span class="hljs-number">3000</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">"start"</span> ]</span>
</code></pre>
<p>This is a simple Dockerfile that:</p>
<ul>
<li><p>Copies your app files into the container</p>
</li>
<li><p>Installs dependencies</p>
</li>
<li><p>Builds the app</p>
</li>
<li><p>Starts the production server</p>
</li>
</ul>
<p>Also, don’t forget to create a <code>.dockerignore</code> file to exclude sensitive files like <code>.env</code> and unnecessary folders like <code>node_modules</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># .dockerignore</span>
.env
node_modules
</code></pre>
<p>Now, build your Docker image using:</p>
<pre><code class="lang-bash">docker build -t next-app .
</code></pre>
<p>You'll likely encounter an error that ends with something like this:</p>
<pre><code class="lang-bash">&gt; Build error occurred
[Error: Failed to collect page data <span class="hljs-keyword">for</span> /]
------
Dockerfile:12
--------------------
...
RUN npm run build
...
--------------------
ERROR: failed to solve: process <span class="hljs-string">"/bin/sh -c npm run build"</span> did not complete successfully: <span class="hljs-built_in">exit</span> code: 1
</code></pre>
<p>This confirms that the basic Dockerfile fails when your application requires secrets like environment variables <strong>during build time</strong>.</p>
<h2 id="heading-passing-build-time-secrets">Passing Build-Time Secrets</h2>
<p>To securely pass secrets during the build phase, Docker provides a feature called <strong>Build Secrets</strong>.</p>
<p><strong>Build Secrets</strong> ensure that sensitive data like API keys or database URLs can be accessed securely during the build process <strong>without being saved in image layers</strong>.</p>
<p>This is a recommended approach for passing private credentials that your app needs <strong>at build time</strong>.</p>
<blockquote>
<p>Learn more: <a target="_blank" href="https://docs.docker.com/build/building/secrets/">Docker Build Secrets</a></p>
</blockquote>
<p>Now, update your Dockerfile like this</p>
<pre><code class="lang-bash"><span class="hljs-comment"># syntax=docker/dockerfile:1.4</span>

FROM node:23-alpine

WORKDIR /app

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

RUN npm install

COPY . .

RUN --mount=<span class="hljs-built_in">type</span>=secret,id=database_url \
    <span class="hljs-built_in">export</span> DATABASE_URL=$(cat /run/secrets/database_url) \
    npm run build

EXPOSE 3000

CMD [ <span class="hljs-string">"npm"</span>, <span class="hljs-string">"run"</span>, <span class="hljs-string">"start"</span> ]
</code></pre>
<p>There are two new additions in this Dockerfile compared to the basic version we saw earlier:</p>
<h3 id="heading-1-syntaxdockerdockerfile14">1. <code># syntax=docker/dockerfile:1.4</code></h3>
<p>This line specifies the Dockerfile syntax version. By default, Docker uses an older syntax that doesn’t support advanced features like build-time secrets. By explicitly setting version <code>1.4</code>, we unlock access to features like the <code>RUN --mount=type=secret</code> instruction, which we use to safely pass secrets during build time.</p>
<h3 id="heading-2-run-mounttypesecretiddatabaseurl">2. <code>RUN --mount=type=secret,id=database_url \ ...</code></h3>
<p>This command securely passes the <code>DATABASE_URL</code> secret to the build process. Let’s break it down:</p>
<ul>
<li><p><code>--mount=type=secret,id=database_url</code> tells Docker to mount a secret with the ID <code>database_url</code> at build time.</p>
</li>
<li><p><code>cat /run/secrets/database_url</code> reads the secret file from the mounted path.</p>
</li>
<li><p><code>export DATABASE_URL=$(...)</code> sets the environment variable using the value from the secret.</p>
</li>
<li><p><code>npm run build</code> then uses this environment variable during the build process.</p>
</li>
</ul>
<h2 id="heading-prepare-for-build">Prepare for build</h2>
<p>Now, before we build the Docker image, let’s take care of one important step.</p>
<p>Inside your project’s root directory, create a folder named <code>env</code>. Inside that folder, create a file called <code>database_url.txt</code> and paste your database URL into it. It should look something like this:</p>
<p>(I will add the explanation of this part while explaining the build command. For now, just follow together.)</p>
<pre><code class="lang-bash">postgresql://neondb_owner:npg_YZ9dFnE8QBxz@ep-soft-field-a1m23wcs-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
</code></pre>
<blockquote>
<p><em>(Don’t worry, I’m going to delete this DB URL after writing this blog 😄)</em></p>
</blockquote>
<p>Also, make sure to add <code>env/</code> to both your <code>.gitignore</code> and <code>.dockerignore</code> files because you don’t want this sensitive information to be committed to version control or included in your Docker context.</p>
<p>Now that we're set up, it’s time to build the Docker image. Here’s the command you’ll use:</p>
<pre><code class="lang-bash">DOCKER_BUILDKIT=1 docker build --secret id=database_url,src=env/database_url.txt -t next-app .
</code></pre>
<p>Let’s break it down:</p>
<h3 id="heading-dockerbuildkit1"><code>DOCKER_BUILDKIT=1</code></h3>
<p>This enables <strong>Docker BuildKit</strong>, a modern builder with advanced features like <strong>secret mounting during builds</strong> (which we are using).</p>
<p>Without this, you won’t be able to use the <code>--secret</code> flag in the build command.</p>
<h3 id="heading-docker-build"><code>docker build</code></h3>
<p>This is the core command to build your Docker image.</p>
<h3 id="heading-secret-iddatabaseurlsrcenvdatabaseurltxt"><code>--secret id=database_url,src=env/database_url.txt</code></h3>
<p>This is the most important part. Here’s what it does:</p>
<ul>
<li><p><code>id=database_url</code>: This is the <strong>identifier</strong> for the secret. It must match the ID you use in your Dockerfile (<code>--mount=type=secret,id=database_url</code>).</p>
</li>
<li><p><code>src=env/database_url.txt</code>: This points to the <strong>actual file</strong> containing your secret, and in our case it is the <code>database_url.txt</code> (Remember we created <strong>env/database_url.txt</strong>? Here is the use of that file)</p>
</li>
</ul>
<p>This securely mounts the secret during the build process <strong>without including it in the final image</strong>.</p>
<h3 id="heading-t-next-app"><code>-t next-app</code></h3>
<p>This tags the built image as <code>next-app</code>, so you can reference it easily later.</p>
<h3 id="heading-dot"><code>.</code> (dot)</h3>
<p>This sets the <strong>build context</strong> to the current directory, basically telling Docker where to find the Dockerfile and your app’s code.</p>
<p>With all that done, your image will be built with your database secret securely passed in during the build. Nice and clean</p>
<p>So, with this we have covered the most difficult part of this tutorial; now everything from here will be very easy and straightforward.</p>
<h2 id="heading-running-the-application">Running the application</h2>
<p>Finally run your container with command</p>
<pre><code class="lang-bash">docker run --name next-app -p 3000:3000 next-app
</code></pre>
<p>This will start your NextJS app inside the Docker container and will map to port 3000 of your local machine. Now, go to <strong><em>http://localhost:3000</em></strong> of your machine and you will see something like this there</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745441102951/2fa68723-5e78-4b74-a382-ad0d9ec12768.png" alt class="image--center mx-auto" /></p>
<p>With this, we have learned how to pass build secrets to the Docker container; let’s move on to run-time secrets.</p>
<h2 id="heading-runtime-secrets">Runtime Secrets</h2>
<p>Passing build-time secrets was a bit tricky and required some extra steps. But when it comes to <strong>runtime secrets</strong>, things are much simpler. All you need to do is slightly modify your <code>docker run</code> command.</p>
<p>To pass runtime secrets, run the following command:</p>
<pre><code class="lang-bash">docker run --env-file .env --name next-app -p 3000:3000 next-app
</code></pre>
<p>Let’s break this down:</p>
<ul>
<li><p><code>docker run</code>: Starts a Docker container.</p>
</li>
<li><p><code>--env-file .env</code>: Specifies the path to the <code>.env</code> file that contains your environment variables. This file should be present in your project’s root directory.<br />  If your <code>.env</code> file is located somewhere else (e.g., inside a <code>src</code> folder), adjust the path accordingly:</p>
<pre><code class="lang-bash">  --env-file ./src/.env
</code></pre>
</li>
<li><p><code>--name next-app</code>: Gives the container a name, in this case, "next-app".</p>
</li>
<li><p><code>-p 3000:3000</code>: Maps port 3000 of your local machine to port 3000 of the container.</p>
</li>
<li><p><code>next-app</code>: Refers to the Docker image you built earlier.</p>
</li>
</ul>
<p>With this command, your container will start up with the environment variables from the <code>.env</code> file injected at runtime.</p>
<h2 id="heading-how-to-pass-secrets-in-production">How to Pass Secrets in Production</h2>
<p>Now that we’ve learned how to pass both build-time and runtime secrets during development, let’s quickly look at how you might do this in production.</p>
<ul>
<li><p><strong>Build-time secrets</strong>: You can use <strong>GitHub Secrets</strong> in your CI/CD workflows to securely pass sensitive values during the build stage.</p>
</li>
<li><p><strong>Runtime secrets</strong>: Simply create the <code>.env</code> file manually on your virtual machine (e.g., EC2, DigitalOcean, etc.) before running your container.</p>
</li>
</ul>
<p>This is just one approach and there are several other ways to handle secrets securely in production, such as using secret management tools like AWS Secrets Manager, HashiCorp Vault, or Docker Swarm secrets.</p>
<p>I’ve kept this section brief since implementation can vary based on your deployment setup and preferences.</p>
<p>If you enjoyed this blog, consider following me on <strong>X (</strong><a target="_blank" href="https://x.com/_devkunal">https://x.com/_devkunal</a><strong>)</strong> where I share my daily learnings and dev tips!</p>
]]></content:encoded></item></channel></rss>