Skip to main content

Express Docker Deployment

Introduction

Docker has revolutionized how we deploy and run applications by providing consistent environments across development, testing, and production. For Express.js applications, Docker offers an excellent way to package your application with all its dependencies into a standalone, portable container that can run anywhere Docker is installed.

In this guide, you'll learn how to containerize your Express application using Docker, which allows you to:

  • Ensure consistency between development and production environments
  • Simplify deployment processes
  • Improve scalability and resource utilization
  • Make your application portable across different hosting platforms

Let's dive into the world of containerization with Express and Docker!

Prerequisites

Before we start, make sure you have:

  1. An Express.js application (or you can use our example)
  2. Docker installed on your machine
  3. Basic understanding of Express.js
  4. A text editor or IDE

Understanding Docker Concepts

Before diving into the code, let's briefly explore some key Docker concepts:

  • Docker Image: A read-only template containing your application code, runtime, libraries, and dependencies
  • Docker Container: A running instance of an image
  • Dockerfile: A text document with instructions to build a Docker image
  • Docker Hub: A registry service where you can find and share Docker images

Step 1: Creating a Simple Express Application

If you already have an Express application, you can skip this step. Otherwise, let's create a simple Express application:

First, initialize a new Node.js project:

bash
mkdir express-docker-app
cd express-docker-app
npm init -y
npm install express

Now, create a file named app.js with the following content:

javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
res.send('Hello from Express in Docker!');
});

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Let's also create a proper package.json file with start scripts:

json
{
"name": "express-docker-app",
"version": "1.0.0",
"description": "Simple Express app for Docker deployment",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"keywords": ["express", "docker"],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}

Step 2: Creating a Dockerfile

The Dockerfile contains instructions for building your Docker image. Create a file named Dockerfile (no extension) in your project root:

dockerfile
# Use Node.js LTS version as the base image
FROM node:16-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy the rest of the application code
COPY . .

# Expose the port your app runs on
EXPOSE 3000

# Command to run the application
CMD ["npm", "start"]

Let's understand each instruction:

  • FROM node:16-alpine: Starts with a lightweight Node.js image based on Alpine Linux
  • WORKDIR /usr/src/app: Sets the working directory inside the container
  • COPY package*.json ./: Copies package files first (for better caching)
  • RUN npm install --production: Installs only production dependencies
  • COPY . .: Copies all application files into the container
  • EXPOSE 3000: Documents that the container listens on port 3000
  • CMD ["npm", "start"]: Specifies the command to start the application

Step 3: Creating a .dockerignore File

Similar to .gitignore, the .dockerignore file specifies which files should not be copied into the Docker image:

node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md

This helps keep your Docker image clean and small by excluding unnecessary files.

Step 4: Building the Docker Image

Now, let's build the Docker image:

bash
docker build -t express-app .

This command builds a Docker image named express-app using the Dockerfile in the current directory. The build process may take a few minutes the first time as Docker downloads the base image and installs dependencies.

Step 5: Running the Docker Container

Once the image is built, you can run a container from it:

bash
docker run -p 3000:3000 express-app

This command:

  • Starts a container from the express-app image
  • Maps port 3000 in the container to port 3000 on your host machine using the -p flag

Now, you can access your Express application at http://localhost:3000 in your browser, and you should see "Hello from Express in Docker!"

Real-world Docker Deployment Example

In real-world scenarios, you might want to:

  1. Use environment variables for configuration
  2. Connect to external services like databases
  3. Set up multi-stage builds for smaller production images

Let's enhance our example:

Environment Variables and Configuration

Update the app.js file:

javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const ENVIRONMENT = process.env.NODE_ENV || 'development';

app.get('/', (req, res) => {
res.send(`Hello from Express in Docker! (Environment: ${ENVIRONMENT})`);
});

app.listen(PORT, () => {
console.log(`Server running in ${ENVIRONMENT} mode on port ${PORT}`);
});

Update the Dockerfile to use environment variables:

dockerfile
FROM node:16-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install --production

COPY . .

# Default environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["npm", "start"]

Now run the container with custom environment variables:

bash
docker run -p 3000:3000 -e NODE_ENV=staging -e PORT=3000 express-app

Using Docker Compose for Multi-container Applications

For applications with multiple services (like an Express app with a database), Docker Compose provides a way to define and run multi-container Docker applications.

Create a docker-compose.yml file:

yaml
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- MONGO_URI=mongodb://mongo:27017/mydatabase
depends_on:
- mongo

mongo:
image: mongo:4.4
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db

volumes:
mongo-data:

To run the application with Docker Compose:

bash
docker-compose up

This will start both your Express application and a MongoDB database, with the Express app configured to connect to the database.

Optimizing Your Docker Image

To create smaller, more efficient Docker images, you can use:

Multi-stage Builds

dockerfile
# Build stage
FROM node:16-alpine AS build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
# If you have a build process (like TypeScript compilation)
RUN npm run build

# Production stage
FROM node:16-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --production
# Copy only the built files from the build stage
COPY --from=build /usr/src/app/dist ./dist
EXPOSE 3000
CMD ["npm", "start"]

Using a Node-specific User

For security, it's a good practice to run your application as a non-root user:

dockerfile
FROM node:16-alpine

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install --production

COPY . .

# Create a user with no home directory and switch to it
RUN adduser -D -H -h /usr/src/app nodeuser && \
chown -R nodeuser:nodeuser /usr/src/app
USER nodeuser

EXPOSE 3000
CMD ["npm", "start"]

Deploying to Production

When deploying to production, you have several options:

Option 1: Docker Registry

  1. Push your image to a Docker registry:

    bash
    docker tag express-app username/express-app:latest
    docker push username/express-app:latest
  2. On your production server, pull and run the image:

    bash
    docker pull username/express-app:latest
    docker run -d -p 80:3000 username/express-app:latest

Option 2: Cloud Providers

Most cloud providers offer container services like:

  • AWS: Amazon ECS, Amazon EKS
  • Google Cloud: Google Kubernetes Engine (GKE), Cloud Run
  • Azure: Azure Kubernetes Service (AKS), Container Instances
  • Digital Ocean: App Platform, Kubernetes

Example deployment to Heroku:

bash
# Login to Heroku Container Registry
heroku container:login

# Build and push the Docker image
heroku container:push web -a your-app-name

# Release the image to your app
heroku container:release web -a your-app-name

Continuous Integration and Deployment (CI/CD)

For automated deployment workflows, you can use GitHub Actions or similar CI/CD tools. Here's a simplified GitHub Actions workflow file (.github/workflows/docker-deploy.yml):

yaml
name: Deploy Express Docker App

on:
push:
branches: [ main ]

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

steps:
- uses: actions/checkout@v2

- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: true
tags: username/express-app:latest

Summary

In this guide, you've learned:

  1. How to containerize an Express.js application using Docker
  2. Creating an optimized Dockerfile for Node.js applications
  3. Building and running Docker containers locally
  4. Using Docker Compose for multi-container applications
  5. Optimizing Docker images for production
  6. Deploying Docker containers to production environments
  7. Setting up continuous deployment workflows

Docker provides a consistent, portable, and scalable way to deploy Express applications across different environments. By containerizing your application, you ensure that it runs the same way everywhere, eliminating the "it works on my machine" problem and simplifying deployment processes.

Additional Resources

Exercises

  1. Extend the Express application to include a health check endpoint at /health that returns server status information.

  2. Modify the Docker Compose setup to include a Redis container for session storage.

  3. Create a development-specific Dockerfile that includes nodemon for hot reloading during development.

  4. Build a multi-stage Dockerfile for a TypeScript Express application that compiles the TypeScript code during the build process.

  5. Set up a CI/CD pipeline using GitHub Actions or GitLab CI to automatically build and deploy your Dockerized Express application.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)