
Docker for Developers: From Your First Image to Production
Docker often becomes useful the first time an application runs on one developer’s machine and fails on another. A different runtime version, missing system package, or forgotten setup step can turn a simple project into an afternoon of troubleshooting. Docker lets you package an application with the files and dependencies it needs, making that environment easier to reproduce. Start by understanding the difference between an image and a container. An image is the packaged application and its runtime environment; a container is an instance created from that image. You can start several containers from the same image, each with its own writable layer. Your first image usually begins with a Dockerfile in the project directory. FROM selects the base image, WORKDIR sets the working directory, COPY adds files, RUN executes build instructions, and CMD defines the default command. For a small Python service, that might mean choosing a Python base image, installing dependencies, copying the application, and specifying its startup command. Running docker build -t my-app:dev . builds and tags the image using the current directory as the build context. If the application listens on port 8000, docker run --rm -p 127.0.0.1:8000:8000 my-app:dev makes it accessible through your machine’s localhost address. The application must listen on an appropriate interface inside the container, usually 0.0.0.0. An EXPOSE instruction alone does not publish a port. Understanding that distinction early prevents a common problem: a container appears to run correctly, but the browser cannot reach the application. Docker Docs
Once the application runs, improve the build before adding more complexity. A Dockerfile that copies the entire repository before installing dependencies may work, but ordinary source changes can force unnecessary dependency installation. Copy dependency manifests and lockfiles first, install the dependencies, and copy the remaining source afterward. This ordering gives Docker a better opportunity to reuse cached build steps. Add a .dockerignore file to exclude material the build does not need, such as local virtual environments, dependency directories, temporary files, and Git history. Keep credentials and local configuration containing secrets out of the build context too. Choose a maintained base image from a trusted publisher, and avoid installing packages simply because they might be useful someday. Multi-stage builds are particularly helpful for compiled applications. A Go service, for example, can compile in a stage containing the Go toolchain, then copy the resulting executable into a smaller runtime stage. The final image does not need the compiler or the complete source tree. However, the runtime stage still needs everything the application actually uses, which may include certificate authorities, timezone data, or native libraries. Smaller images are useful, but compatibility matters more than reaching the lowest possible size. Run the application as a non-root user where practical, and check file permissions rather than solving every access problem by granting broader privileges. If a build needs private repository credentials or package tokens, use BuildKit secret or SSH mounts. Putting credentials in build arguments or deleting them after copying them into an image does not reliably keep them out of image metadata or earlier layers. Docker Docs
Most applications need more than one process to be useful. An API might depend on PostgreSQL, Redis, or a background worker, and managing each container through separate terminal commands quickly becomes inconvenient. Docker Compose lets you describe those services, their networks, configuration, and storage in a compose.yaml file. Running docker compose up --build can build the application image and start the stack. Services on the same Compose network can normally reach one another using their service names, so an API connects to a database service named db through that hostname. Using localhost from inside the API container points back to the API container itself, which explains many initial connection failures. Startup order also deserves attention: a database container starting does not necessarily mean it is ready to accept connections. Health checks and appropriate dependency conditions help coordinate startup, while application retries handle temporary connection failures. Storage requires a separate decision. Files written only to a container’s writable layer disappear when that container is removed. Use named volumes for data that must survive container replacement, and bind mounts when local development requires sharing source files with the host. A volume provides persistence, but you still need backups and a tested recovery process. Commands such as docker compose logs -f and docker compose exec help you inspect services while developing. When finished, docker compose down removes the stack’s containers and networks while preserving named volumes by default. Adding --volumes removes those volumes too, so use that option only when you intend to discard the stored data. Docker Docs
Moving to production requires decisions that a successful local run cannot settle. Build the release image in CI, test that image, publish it to a registry, and promote the same artifact through subsequent environments. Record its digest so a deployment identifies an exact image; tags can be reassigned, even when their names look version-specific. Pinning base images by digest improves reproducibility, but schedule updates so reproducibility does not become an excuse to retain vulnerable dependencies. Scan release images and assess findings against the software you actually ship. Supply environment-specific configuration when containers start, and obtain sensitive values through the deployment platform’s secret mechanism. Remove development source mounts and automatic reloaders from the production configuration. Write application logs to standard output and standard error so the hosting platform can collect them, and configure log retention. Set resource limits, measure actual usage, and decide how the application should behave when dependencies fail. Shutdown matters too: the main process should receive termination signals and have time to finish requests or release resources. Health checks provide useful status information, but their effect depends on the platform; Docker does not automatically restart a standalone container merely because it becomes unhealthy. Compose can support a straightforward single-server deployment, while applications needing automated scheduling across machines may benefit from an orchestrator or managed container service. Keep a previous image available for rollback, and consider database migration compatibility before assuming an older release can safely return. Production readiness becomes convincing when you can deploy, observe, replace, and recover the application reliably.