All articles

Docker for Development and Production: A Practical Guide

What Docker solves for a development team, images and containers in plain terms, writing a good Dockerfile with multi-stage builds, Docker Compose for local development, security basics, running containers in production, and when to move to Kubernetes or a managed container service.

DevOps|Published |10 min read
A cargo ship loaded with shipping containers at sea

"It works on my machine" is one of the oldest problems in software teams. A new developer spends two days installing the right versions of PHP, Node.js, PostgreSQL, and Redis, and production runs something slightly different from both. Docker packages an application with everything it needs into an image that runs the same way on a laptop, a CI runner, and a production server. Used well, it shortens onboarding and makes deployments predictable. Used carelessly, it produces huge images, leaked secrets, and containers running as root.

Images and Containers in Plain Terms

  • An image is a read-only package containing the application, its runtime, libraries, and configuration defaults. It is built from a Dockerfile.
  • A container is a running instance of an image, with its own process, network, and file system view.
  • A registry stores and shares images, for example Docker Hub, GitHub Container Registry, GitLab Container Registry, or Amazon ECR.
  • A volume keeps data outside the container, so it survives when the container is replaced.
  • Docker Compose describes several containers that work together, such as an application, a database, and a cache, in one file.

Docker for Local Development

The quickest win is a Compose file that starts every dependency the application needs. A new developer clones the repository, runs one command, and has PostgreSQL, Redis, and a mail catcher running with the same versions as production. Many teams run only the dependencies in containers and keep the application itself on the host for fast reloads and easy debugging, while others run everything in containers for full consistency. Either works, as long as the versions are pinned in the Compose file and documented in the README.

Writing a Good Dockerfile

  • Start from an official, slim base image with a pinned version, such as a specific Node.js or Python release on Debian slim or Alpine.
  • Use a multi-stage build: compile and install build tools in the first stage, then copy only the result into a small runtime stage.
  • Copy dependency files and install dependencies before copying the rest of the code, so Docker can reuse that layer when only the code changes.
  • Add a .dockerignore file to keep .git, node_modules, local environment files, and logs out of the image.
  • Run the application as a non-root user, and write logs to standard output so the platform can collect them.
  • Add a health check, or a health endpoint the platform can call, so broken containers are restarted.

Multi-stage builds often make the biggest difference. An image that contains compilers, development dependencies, and the full source tree can easily be several times larger than one that contains only the compiled application and its runtime. Smaller images download faster during deployment and contain fewer packages with security vulnerabilities.

Keep Secrets out of Images

Anything copied into an image during the build stays in its layers, even if a later step deletes it, and anyone who can pull the image can read it. Never put API keys, database passwords, or .env files into an image. Pass configuration as environment variables or secrets at runtime, and use build secrets for private package registries during the build. The same image should run in staging and production, with only the configuration changing.

Security Basics for Containers

  1. 1Scan images for known vulnerabilities in CI with a tool such as Trivy or Docker Scout, and fail the build on critical findings that have a fix.
  2. 2Rebuild images regularly, even without code changes, so they pick up security updates in the base image.
  3. 3Tag images with the Git commit or version, and deploy by that tag, not by latest, so you always know what is running.
  4. 4Do not expose the Docker socket to containers, and avoid privileged mode, because either gives a container control of the host.
  5. 5Limit memory and CPU per container, so one misbehaving service cannot take down the whole server.

Running Containers in Production

OptionFitsTrade-offs
Docker Compose on one serverSmall applications and internal toolsSimple to run, but the server is a single point of failure and scaling is manual
Managed container service such as Amazon ECS with Fargate or Google Cloud RunMost business applications that need scaling and high availabilityNo servers to manage, with less flexibility than Kubernetes and some platform-specific configuration
KubernetesMany services, several teams, or a need for portability across environmentsVery flexible, but needs skills and ongoing effort to operate well

Whichever option you choose, keep state outside the containers. Databases are usually better on a managed database service than in a container next to the application, and uploaded files belong in object storage. Containers can then be replaced, scaled, and moved freely.

Docker Desktop Licensing

Docker Engine and the Docker CLI are open source. Docker Desktop, the application most developers use on Windows and macOS, is free for personal use, education, and small businesses, but larger companies need a paid subscription. Check the current terms for your company size. On Windows, Docker inside WSL 2 or alternatives such as Podman or Rancher Desktop are options some teams use.

Measure your onboarding time before and after introducing Docker: how long it takes a new developer to run the application locally. Teams often go from a day or two to under an hour, which is the easiest way to show the value to management.

Key takeaways

  • Use Docker Compose to give every developer the same dependencies with one command.
  • Write slim Dockerfiles with pinned base images, multi-stage builds, a .dockerignore, and a non-root user.
  • Keep secrets out of images and pass configuration at runtime, so one image runs in every environment.
  • Scan and rebuild images regularly, tag them by commit, and avoid privileged containers.
  • Start production on Compose or a managed container service, and move to Kubernetes when the number of services and teams justifies it.

Related articles

More articles on software development, AI, cloud, and infrastructure.

A desktop screen showing landing page designs next to a tablet and a phone
Web Development|

How to Build a Company Profile Website That Brings in Leads

What a company profile website needs to bring in enquiries: clear service pages, proof such as case studies and client logos, easy contact options, fast loading on mobile, SEO basics, the right platform, and tracking that shows which pages produce leads.

A customer paying with a phone at a shop counter
Software Development|

Integrating a Payment Gateway Like Midtrans or Xendit Safely

How to integrate an Indonesian payment gateway such as Midtrans or Xendit: choosing payment methods, hosted checkout versus direct API, verifying webhooks, handling duplicate notifications, order status design, expiry, refunds, testing in sandbox, and daily reconciliation.

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation