{"id":3630,"date":"2026-10-29T15:30:17","date_gmt":"2026-10-29T07:30:17","guid":{"rendered":"http:\/\/www.tinuo-packagesolutions.com\/blog\/?p=3630"},"modified":"2026-10-29T15:30:17","modified_gmt":"2026-10-29T07:30:17","slug":"how-to-create-a-docker-image-for-an-express-application-46fc-91c899","status":"publish","type":"post","link":"http:\/\/www.tinuo-packagesolutions.com\/blog\/2026\/10\/29\/how-to-create-a-docker-image-for-an-express-application-46fc-91c899\/","title":{"rendered":"How to create a Docker image for an Express application?"},"content":{"rendered":"<p>Hey folks, if you\u2019re running an Express app as a vendor\u2014like I am, we build and deploy Express-powered tools for clients every day\u2014you know how much of a pain inconsistent environments are. One day it works on your dev machine, the next it\u2019s throwing dependency errors in QA, and production\u2026 well, production\u2019s a whole other mess. That\u2019s where Docker comes in. It\u2019s not some fancy tech toy; it\u2019s the fix for \u201cit works on my machine\u201d that we\u2019ve relied on for years. Today I\u2019m breaking down exactly how to build a Docker image for an Express app, from the basics to the little tricks that save us hours of debugging\u2014no overly complicated jargon, just what actually works for us as an Express app provider. <a href=\"https:\/\/www.fsshijielogistics.com\/express\/\">Express<\/a><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.fsshijielogistics.com\/uploads\/44940\/small\/express-ups-to-usd5cf2.jpg\"><\/p>\n<p>First, let\u2019s get this straight: I\u2019m not here to teach you Docker 101. I\u2019m assuming you\u2019ve got a working Express app already, right? The kind you\u2019ve tested locally, it listens on a port, maybe uses some npm packages, maybe a tiny bit of middleware, and you\u2019re ready to make it portable. We\u2019ve built dozens of these, so I know the mistakes people make here\u2014like using the wrong base image, or installing unnecessary packages that bloat the image. Let\u2019s skip those.<\/p>\n<h3>Step 1: Set Up the Files You Actually Need (No Junk Allowed)<\/h3>\n<p>First, go to the root of your Express project. You\u2019ll need two key files, and if you\u2019re skipping any, stop here: a <code>package.json<\/code> and <code>package-lock.json<\/code> (or <code>yarn.lock<\/code> if you use Yarn, but we\u2019re sticking to npm since most devs know it). These are non-negotiable because they tell Docker exactly what dependencies to install.<\/p>\n<p>Wait, and one more\u2014your Dockerfile. That\u2019s the blueprint for your image. No, you don\u2019t need a <code>docker-compose.yml<\/code> for a simple Express app (we only use that for multi-service setups, like if we pair our Express API with a Redis cache or MongoDB, but that\u2019s beyond this guide). Also, delete any <code>node_modules<\/code> folder right now. Do it. Docker doesn\u2019t need local node_modules because we\u2019ll install them inside the container, and having local ones can cause version mismatches. I\u2019ve seen new devs waste 2 hours because they forgot that. Trust me.<\/p>\n<h3>Step 2: Pick the Right Base Image (Size Matters)<\/h3>\n<p>Okay, next up: choosing a Node.js base image. This is where a lot of people go wrong. You don\u2019t need the biggest, beefiest Node image out of the box. For Express apps, we use slim Alpine images almost every time. Why? Because they\u2019re tiny\u2014way smaller than the full Node image\u2014so they pull faster, deploy faster, and have less attack surface (which is a big deal when we\u2019re shipping our app to clients who care about security).<\/p>\n<p>Let\u2019s make this concrete. When you pull a Node image, you\u2019ll see tags like <code>node:20-alpine<\/code>, <code>node:20-slim<\/code>, <code>node:20<\/code>. We use <code>node:20-alpine<\/code> right now because it\u2019s the sweet spot. It\u2019s based on Alpine Linux, which is super lightweight, and it has everything Node needs\u2014no extra fluff. If you need something with a few more system libraries (maybe if you\u2019re using a native Node module that needs some OS-level packages), you could use <code>node:20-slim<\/code>, but stick to Alpine first. That\u2019s what we use for 90% of our Express apps.<\/p>\n<p>Pro tip: Always pin the Node version, not just use <code>node:alpine<\/code>. Like, use <code>node:20.10.0-alpine<\/code> instead of <code>node:alpine<\/code>. If you don\u2019t, when Node 21 drops next month, your Docker build will pull the new version, and if there\u2019s a breaking change, your app will crash. We learned that the hard way with a client last year\u2014their Express app broke in prod because Node 21 changed a middleware method. Pinning versions avoids that. Easy win.<\/p>\n<h3>Step 3: Write the Dockerfile (Line by Line, No Copy-Paste Fluff)<\/h3>\n<p>Now let\u2019s build the Dockerfile line by line. I\u2019ll show you the exact one we use for every basic Express app, then explain each part so you know why it works, not just what to type.<\/p>\n<p>Here\u2019s the baseline Dockerfile:<\/p>\n<pre><code class=\"language-dockerfile\"># Use a pinned, lightweight Node base image\nFROM node:20.10.0-alpine\n\n# Set a working directory inside the container (no more messy paths!)\nWORKDIR \/app\n\n# Copy ONLY package files first\u2014this is a key caching trick\nCOPY package*.json .\/\n\n# Install dependencies\u2014clean install for consistency\nRUN npm ci --only=production\n\n# Copy the rest of your app code (after dependencies, to leverage caching)\nCOPY . .\n\n# Tell Docker what port your app listens on\nEXPOSE 3000\n\n# Command to start your Express app\nCMD [&quot;node&quot;, &quot;app.js&quot;]\n<\/code><\/pre>\n<p>Wait, let\u2019s break that down because that <code>npm ci<\/code> line is important. I see so many people use <code>npm install<\/code> here, and that\u2019s fine for local dev, but for Docker builds, <code>npm ci<\/code> is way better. Why? <code>npm ci<\/code> uses the exact versions listed in <code>package-lock.json<\/code>, so you get the same dependency tree every time. No \u201cthis package updated last night\u201d surprises. We\u2019ve had clients have production outages because <code>npm install<\/code> pulled a minor version that had a bug\u2014never again since we switched to <code>npm ci<\/code>.<\/p>\n<p>Also, notice we copy the package files before the rest of the app? That\u2019s the caching trick. Docker caches each layer of the image. If you change your app code but don\u2019t touch <code>package.json<\/code>, Docker will skip the dependency install layer entirely, which cuts build time from 10 seconds to 1 second. Game changer when you\u2019re building images 10 times a day for testing.<\/p>\n<p>Another thing: <code>--only=production<\/code> in <code>npm ci<\/code>. If you have dev dependencies (like testing frameworks, linters, etc.), you don\u2019t need them in the production image. They just add bloat. We had one client\u2019s Docker image go from 300MB to 80MB after adding that flag\u2014insane difference. Just make sure your app doesn\u2019t use any dev dependencies at runtime, which it shouldn\u2019t anyway.<\/p>\n<p>Quick note on the <code>EXPOSE<\/code> line: That\u2019s just documentation. It tells anyone looking at the Dockerfile that the app uses port 3000. You still have to map the port when you run the container (more on that in a sec). The <code>CMD<\/code> line is the actual command that starts your app\u2014adjust that if your entry point is different, like <code>server.js<\/code> instead of <code>app.js<\/code>.<\/p>\n<h3>Step 4: Build the Image and Test It (Don\u2019t Skip Testing!)<\/h3>\n<p>Okay, now we\u2019re ready to build the image. Open your terminal, navigate to your project root, and run:<\/p>\n<pre><code class=\"language-bash\">docker build -t my-express-app .\n<\/code><\/pre>\n<p>Replace <code>my-express-app<\/code> with a name that makes sense\u2014something your team recognizes, and that your client will know if you\u2019re sharing the image. The <code>-t<\/code> flag tags the image, so you don\u2019t have to reference its random ID later.<\/p>\n<p>This is where we test it before shipping it to clients, because you don\u2019t want to send a broken image. Run the container with:<\/p>\n<pre><code class=\"language-bash\">docker run -p 3000:3000 my-express-app\n<\/code><\/pre>\n<p>Wait, the <code>-p 3000:3000<\/code> part is port mapping. That\u2019s why <code>EXPOSE<\/code> wasn\u2019t enough\u2014you have to bind your local machine\u2019s port (the first number) to the container\u2019s port (the second, which is what your app is listening on). If your app uses port 4000, change that to <code>-p 4000:4000<\/code>.<\/p>\n<p>Now go to your browser or use curl to hit <code>http:\/\/localhost:3000<\/code> (adjust the port if needed). If your Express app loads, you\u2019re golden. If not, let\u2019s troubleshoot the common issues we run into:<\/p>\n<ol>\n<li>Check if the entry point in <code>CMD<\/code> is correct\u2014if you get a \u201ccannot find module app.js\u201d error, that\u2019s probably it.<\/li>\n<li>Did you forget to copy the app code? Make sure the <code>COPY . .<\/code> line is after the dependency install.<\/li>\n<li>Are you using a native module? If so, you might need extra system packages (like <code>python3<\/code> or <code>make<\/code>) in the image. For that, add a <code>RUN apk add --no-cache python3 make g++<\/code> line right after the <code>FROM<\/code> line, since Alpine uses <code>apk<\/code> for package management. We only do that if we need it\u2014native modules are rare for Express, but good to know.<\/li>\n<\/ol>\n<h3>Step 5: Extra Tweaks for Production (We Use These for Client Deployments)<\/h3>\n<p>Now, this is the part that turns a test image into something you can ship to a client. We\u2019ve shared basic images with clients, but adding these small tweaks makes them production-ready and way more reliable:<\/p>\n<ul>\n<li><strong>Non-root user<\/strong>: By default, Docker runs containers as root, which is a security risk. You don\u2019t want a breach to give root access to your host machine. Add these lines right after <code>WORKDIR \/app<\/code>:\n<pre><code class=\"language-dockerfile\"># Create a non-root user\nRUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup\n# Change ownership of the working directory to the non-root user\nRUN chown appuser:appgroup \/app\n# Switch to the non-root user\nUSER appuser\n<\/code><\/pre>\n<p>That\u2019s a quick security win, and we always add this for client-facing images.<\/li>\n<li><strong>Environment variables<\/strong>: If your Express app uses environment variables (like API keys, database URLs), don\u2019t hardcode them. You can pass them when you run the container with <code>-e<\/code> flags, or use a <code>.env<\/code> file for local testing. For production, clients usually use Docker secrets or their own environment management, but we include a note in our Dockerfile that they can pass vars easily.<\/li>\n<li><strong>Healthchecks<\/strong>: This is super useful for deployment tools like Kubernetes. Add a line to check if your app is responding:\n<pre><code class=\"language-dockerfile\">HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http:\/\/localhost:3000\/health || exit 1\n<\/code><\/pre>\n<p>Just make sure you have a <code>\/health<\/code> endpoint in your Express app that returns a 200 OK if everything\u2019s working. Clients love this because it lets their deployment tool know if the container is actually healthy, not just running.<\/li>\n<\/ul>\n<h3>Common Mistakes We See (And How to Avoid Them)<\/h3>\n<p>Let\u2019s cut to the chase\u2014these are the mistakes that make new vendors mess up their Express Docker images, and we\u2019ve made every single one:<\/p>\n<ol>\n<li><strong>Using the wrong Node image size<\/strong>: Don\u2019t use <code>node:latest<\/code> for production. It\u2019s huge, and you never know when it\u2019ll update to a breaking version. Stick to pinned slim or Alpine images.<\/li>\n<li><strong>Copying too much too soon<\/strong>: If you copy <code>node_modules<\/code> or <code>.env<\/code> before dependencies, Docker will re-install dependencies every time you build, slowing things down. The package files first trick is non-negotiable.<\/li>\n<li><strong>Using <code>npm install<\/code> instead of <code>npm ci<\/code><\/strong>: As I mentioned earlier, that leads to inconsistent dependency versions. <code>npm ci<\/code> is made exactly for Docker builds, use it.<\/li>\n<li><strong>Not testing locally before pushing<\/strong>: We always test the image locally with <code>docker run<\/code> before sending it to a client or a registry. Nothing\u2019s worse than pushing an image that crashes on the first request.<\/li>\n<\/ol>\n<p>By the way, we\u2019ve used this exact process for so many clients\u2014small startups needing a quick API deployment, mid-sized companies porting their internal tools, even enterprise clients needing compliant, lightweight images. The best part is that once you have this Dockerfile set up, you can reuse it for every Express app you build. No starting from scratch every time.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.fsshijielogistics.com\/uploads\/44940\/small\/express-ups-to-mexico34e2a.jpg\"><\/p>\n<p>If you\u2019re a fellow Express app vendor looking to standardize your deployment workflow, or if you have a custom Express project that needs a reliable Docker setup, reach out to us to discuss your needs. We tailor Docker configurations to fit your specific use case, whether you need a tiny, fast image for edge deployments or a more robust setup for complex applications.<\/p>\n<h3>References<\/h3>\n<p><a href=\"https:\/\/www.fsshijielogistics.com\/air-freight\/air-freight-air-express\/\">Air Freight Air Express<\/a> Docker Official Documentation, Node.js Official Docker Images, npm CI Documentation, Express.js Official Deployment Guide<\/p>\n<hr>\n<p><a href=\"https:\/\/www.fsshijielogistics.com\/\">Foshan Shijie International Logistics Co., Ltd.<\/a><br \/>With abundant experience, we are one of the most professional express service suppliers in China. We are committed to offering reliable express service and logistics solutions with low price. If you have any enquiry about quote, please feel free to email us.<br \/>Address: Room 701A, Office Building, Jinbo Commercial Center, No. 88, Guiye Road, Guicheng Street, Nanhai District, Foshan City, Guangdong Province<br \/>E-mail: alicesmith.tradingbusiness@gmail.com<br \/>WebSite: <a href=\"https:\/\/www.fsshijielogistics.com\/\">https:\/\/www.fsshijielogistics.com\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hey folks, if you\u2019re running an Express app as a vendor\u2014like I am, we build and &hellip; <a title=\"How to create a Docker image for an Express application?\" class=\"hm-read-more\" href=\"http:\/\/www.tinuo-packagesolutions.com\/blog\/2026\/10\/29\/how-to-create-a-docker-image-for-an-express-application-46fc-91c899\/\"><span class=\"screen-reader-text\">How to create a Docker image for an Express application?<\/span>Read more<\/a><\/p>\n","protected":false},"author":545,"featured_media":3630,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[3593],"class_list":["post-3630","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-industry","tag-express-4d2e-92185b"],"_links":{"self":[{"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/posts\/3630","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/users\/545"}],"replies":[{"embeddable":true,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/comments?post=3630"}],"version-history":[{"count":0,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/posts\/3630\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/posts\/3630"}],"wp:attachment":[{"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/media?parent=3630"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/categories?post=3630"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.tinuo-packagesolutions.com\/blog\/wp-json\/wp\/v2\/tags?post=3630"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}