Skip to content
Hosting Operations10 min read

Docker Container Deployment Tutorial: Step-by-Step Guide for Production

Learn Docker container deployment from basics to production. Covers build, run, networking, volumes, and troubleshooting for reliable hosting operations.

Written by Abdul AbrorTechnical Hosting Support Engineer
Docker container deployment workflow showing build, test, and production stages
On this page

TL;DR — Key takeaways

  • A Docker container is a lightweight, isolated runtime environment that packages an application with its dependencies, ensuring consistent behavior across development and production systems.
  • Before deploying to production, always test containers locally with resource limits, health checks, and volume mounts to catch configuration issues early.
  • Use multi-stage builds to reduce image size by up to 80%, improving deployment speed and reducing attack surface for production workloads.
  • Implement proper logging with log drivers and restart policies to ensure containers recover automatically from failures without manual intervention.
  • Store sensitive data in environment variables or secrets management systems rather than embedding credentials directly in container images.

Docker containers have become the standard for deploying applications across hosting environments. Whether you're supporting client infrastructure, managing your own services, or troubleshooting deployment issues, understanding container deployment fundamentals is essential for modern hosting operations.

This guide walks through Docker container deployment from initial setup to production-ready configurations. You'll learn how to build images, configure networking, manage persistent data, and implement the safety checks that prevent common deployment failures.

Understanding Docker Containers and Images

A Docker container is a runnable instance of a Docker image. The image serves as a template containing the application code, runtime, libraries, and system tools. Containers provide process isolation without the overhead of full virtual machines, making them efficient for hosting multiple applications on shared infrastructure.

Images are built in layers. Each instruction in a Dockerfile creates a new layer, and Docker caches these layers to speed up subsequent builds. Understanding this layering system helps you write efficient Dockerfiles and troubleshoot build problems.

Before deploying any container, verify your Docker installation and check available resources:

  • Run `docker --version` to confirm Docker is installed (version 20.10 or later recommended)
  • Check available disk space with `df -h` (containers and images consume storage quickly)
  • Verify Docker daemon is running: `systemctl status docker` on Linux systems
  • Test basic functionality: `docker run hello-world` should complete without errors

Building Docker Images with Dockerfiles

A Dockerfile defines the steps to create a container image. Start with a minimal base image appropriate for your application stack. For production deployments, prefer official images from verified publishers to reduce security risks.

Here's a basic Dockerfile structure for a Node.js application. This example demonstrates multi-stage builds, which separate build dependencies from runtime requirements:

First stage builds the application with development tools. Second stage copies only the production artifacts, resulting in a smaller final image. The working directory, exposed ports, and startup command are clearly defined.

Build the image with `docker build -t myapp:1.0 .` from the directory containing your Dockerfile. The `-t` flag tags the image with a name and version. Always tag production images with specific versions rather than using `latest` to maintain deployment consistency.

  • Use `.dockerignore` to exclude unnecessary files (node_modules, .git, logs) from the build context
  • Pin base image versions (e.g., `node:18.20-alpine` instead of `node:latest`) for reproducible builds
  • Place frequently changing instructions (COPY application code) near the end to maximize layer cache effectiveness
  • Run `docker images` after building to verify image size and creation date

Running and Configuring Docker Containers

Running a container transforms an image into an active process with allocated resources. The `docker run` command accepts numerous options that control networking, storage, resource limits, and runtime behavior.

For a basic deployment, start with: `docker run -d --name myapp -p 8080:8080 myapp:1.0`. The `-d` flag runs the container in detached mode (background), `--name` assigns a friendly identifier, and `-p` maps host port 8080 to container port 8080.

Production deployments require additional configuration for reliability. Resource limits prevent a single container from consuming all available CPU or memory. Health checks enable automatic restarts when applications become unresponsive:

The `--memory` and `--cpus` flags set hard limits. The `--restart unless-stopped` policy ensures the container restarts after crashes or system reboots but respects manual stop commands. Health checks run periodic commands to verify application responsiveness.

  • Use environment variables with `-e` for configuration: `docker run -e DATABASE_URL=postgres://... myapp:1.0`
  • Mount configuration files with `-v`: `docker run -v /host/config.yml:/app/config.yml:ro myapp:1.0`
  • Set logging driver for centralized logs: `docker run --log-driver json-file --log-opt max-size=10m myapp:1.0`
  • Verify container is running: `docker ps` shows active containers, `docker ps -a` includes stopped ones

Managing Persistent Data with Volumes

Containers are ephemeral by default. Data written inside a container is lost when the container is removed. Docker volumes provide persistent storage that survives container lifecycle operations.

Create a named volume with `docker volume create myapp-data`, then mount it when running the container: `docker run -v myapp-data:/app/data myapp:1.0`. Named volumes are managed by Docker and stored in `/var/lib/docker/volumes/` on Linux hosts.

For sensitive data like databases, always use volumes rather than storing data in the container layer. This separation enables backups, upgrades, and container replacements without data loss.

Bind mounts map host directories directly into containers using absolute paths: `docker run -v /opt/myapp/uploads:/app/uploads myapp:1.0`. Use bind mounts when you need direct host filesystem access, such as for development or when integrating with existing backup systems.

  • List all volumes: `docker volume ls`
  • Inspect volume details including mount point: `docker volume inspect myapp-data`
  • Back up volume data: `docker run --rm -v myapp-data:/data -v /backup:/backup alpine tar czf /backup/myapp-data.tar.gz -C /data .`
  • Clean up unused volumes: `docker volume prune` (confirm before running in production)

Networking and Container Communication

Docker creates isolated networks for container communication. By default, containers on the same network can communicate using container names as hostnames, which Docker resolves through its internal DNS.

Create a custom network: `docker network create myapp-network`. Then run containers attached to this network: `docker run --network myapp-network --name web myapp:1.0` and `docker run --network myapp-network --name db postgres:15`. The web container can now connect to the database using the hostname `db`.

The default bridge network provides basic isolation but requires IP addresses for container-to-container communication. Custom bridge networks enable automatic DNS resolution and better isolation for multi-container applications.

For production hosting, publish only necessary ports to the host. Use `-p 127.0.0.1:8080:8080` to bind to localhost, preventing external access. Use a reverse proxy like Nginx or Traefik to handle external traffic, SSL termination, and request routing.

  • List networks: `docker network ls`
  • Inspect network configuration: `docker network inspect myapp-network`
  • Connect running container to additional network: `docker network connect myapp-network container-name`
  • Test connectivity between containers: `docker exec web ping db`

Monitoring and Troubleshooting Container Deployments

Monitoring container health prevents undetected failures. Use `docker stats` for real-time resource usage across all running containers. The output shows CPU percentage, memory usage, network I/O, and block I/O for each container.

View container logs with `docker logs myapp`. Add `-f` to follow logs in real-time, or `--tail 100` to show only recent entries. Structured logging (JSON format) enables integration with centralized logging systems like ELK or Grafana Loki.

When a container fails to start or behaves unexpectedly, systematic troubleshooting identifies the root cause. Check logs first, then verify environment variables, volume mounts, and network connectivity.

Common deployment issues include port conflicts (another process using the target port), permission errors (container user cannot write to mounted volumes), and resource exhaustion (insufficient memory or CPU allocation). Always test configuration changes in a non-production environment before applying them to live systems.

  • View detailed container configuration: `docker inspect myapp`
  • Execute commands inside running container: `docker exec -it myapp /bin/sh`
  • Copy files from container to host: `docker cp myapp:/app/logs/error.log ./error.log`
  • Check container exit code: `docker inspect myapp --format='{{.State.ExitCode}}'`
  • View Docker daemon logs on Linux: `journalctl -u docker.service`

Production Deployment Checklist

Production deployments require additional hardening beyond basic container operation. Security, reliability, and maintainability considerations ensure stable long-term operation.

Always run containers with non-root users. Add a user in your Dockerfile and switch to it before the final CMD or ENTRYPOINT. This limits the impact of container escape vulnerabilities.

Implement health checks in your Dockerfile using the HEALTHCHECK instruction. This enables Docker and orchestrators to automatically restart unhealthy containers without manual intervention.

Keep images updated to receive security patches. Rebuild images regularly and scan them for vulnerabilities using tools like Docker Scout or Trivy. Set up automated rebuilds when base images are updated.

  • Never embed secrets in images; use environment variables or external secrets management
  • Enable Docker Content Trust for image signature verification in production
  • Use read-only root filesystem when possible: `docker run --read-only myapp:1.0`
  • Limit container capabilities: `docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp:1.0`
  • Set up automated backups for volumes containing persistent data
  • Document rollback procedures before deploying changes

Quick troubleshooting checklist

  • Verify Docker installation and daemon status
  • Create Dockerfile with multi-stage build for optimal image size
  • Build and tag image with specific version number
  • Test container locally with resource limits and health checks
  • Create named volumes for persistent data
  • Configure custom network for container communication
  • Set environment variables for application configuration
  • Publish only necessary ports, preferably to localhost
  • Configure logging driver and log rotation
  • Set restart policy for automatic recovery
  • Run container with non-root user
  • Monitor resource usage with docker stats
  • Test health check endpoint responds correctly
  • Verify volume mounts have correct permissions
  • Document deployment configuration and rollback steps
  • Set up automated image rebuild process
  • Scan images for vulnerabilities before deployment

FAQ

What is the difference between a Docker image and a Docker container?

A Docker image is a read-only template containing application code, dependencies, and configuration files. A Docker container is a running instance of an image with its own isolated filesystem, network, and process space. Think of an image as a class definition and a container as an instantiated object. You can create multiple containers from the same image, each with independent state and configuration.

How do I persist data when a container is deleted?

Use Docker volumes or bind mounts to persist data outside the container's writable layer. Create a named volume with `docker volume create volume-name` and mount it when running the container with `-v volume-name:/path/in/container`. Data in volumes persists after the container is removed and can be attached to replacement containers. For databases and user uploads, always use volumes to prevent data loss during container updates or failures.

Why is my container not accessible from outside the host?

Containers must explicitly publish ports to accept external connections. Use the `-p` flag when running the container: `docker run -p 8080:8080 myapp:1.0` maps host port 8080 to container port 8080. Also verify firewall rules allow traffic on the published port and check that the application inside the container binds to 0.0.0.0 rather than 127.0.0.1. Use `docker logs container-name` to check for binding errors during startup.

How much memory and CPU should I allocate to a container?

Start with conservative limits based on the application's known requirements, then adjust based on monitoring data. For web applications, 512MB to 1GB memory and 0.5 to 1 CPU core is a common starting point. Use `docker stats` to monitor actual usage over time. Set limits with `--memory=1g --cpus=1.0` when running containers. Always leave headroom for traffic spikes and set limits below total available resources to prevent host system instability.

How do I update a running container to a new image version?

Stop the current container with `docker stop container-name`, then remove it with `docker rm container-name`. Pull the new image version with `docker pull myapp:1.1`, then start a new container using the same `docker run` command with updated version tag. If using volumes for persistent data, they automatically attach to the new container. For zero-downtime updates, run the new container on a different port, verify it works correctly, then update your reverse proxy configuration and stop the old container.