Docker for Developers Who Are Not DevOps Engineers
Docker tutorials always start with "containers are like lightweight VMs" and then immediately dive into networking, volumes, multi-stage builds, and orchestration. Thirty minutes later, you still cannot get your Node.js app running in a container.
I am a developer who uses Docker daily but does not manage infrastructure. I do not know (or need to know) how container networking works at the kernel level. I know how to put my app in a container, run it locally, and hand it to someone who handles production. Here is what you actually need.
What Docker Solves for Developers
The real problem Docker solves is not "deployment" — it is "it works on my machine." Before Docker, setting up a development environment meant: install the right Node.js version, install the right Python version, install PostgreSQL, configure it, install Redis, configure it, install 15 system dependencies, pray.
With Docker: clone the repo, run docker compose up, everything works. The database is running, Redis is running, the app is running, all with the right versions and configurations. No "install PostgreSQL 15 and configure it with these settings" instructions.
The Essential Dockerfile
Every application starts with a Dockerfile. Here is what 90% of Node.js applications need:
Start with a specific Node.js version (node:20-alpine for small images). Set a working directory. Copy package.json and package-lock.json first (this lets Docker cache the dependency installation step — if your deps have not changed, this step is instant on subsequent builds). Run npm install. Copy the rest of your application code. Expose the port. Define the start command.
The key optimization: copy package files before source code. Docker caches each layer. If only your source code changed, Docker reuses the cached node_modules layer. This makes rebuilds fast (seconds instead of minutes).
For production, add a multi-stage build: first stage installs dependencies and builds the application. Second stage copies only the built output into a minimal image. This produces smaller images (100-300MB instead of 1-2GB).
Docker Compose for Local Development
Docker Compose is where the real developer value lives. Instead of installing and configuring every service manually, you define them in a docker-compose.yml:
Your application, your database (PostgreSQL/MySQL/SQLite), your cache (Redis), your search engine (Meilisearch), your email service (Mailhog for local email testing) — all defined declaratively and started with one command.
Each service gets its own container, its own network, and its own configuration. They communicate via service names (your app connects to postgres://db:5432 where "db" is the service name).
Volumes persist data across container restarts. Without a volume, your database data disappears every time you restart the container. Mount a volume for your database's data directory and your data survives restarts.
The Commands You Actually Use
docker compose up — start all services. Add -d for detached (background) mode.
docker compose down — stop all services. Add -v to also remove volumes (reset database).
docker compose logs [service] — view logs for a service. Add -f to follow live.
docker compose exec [service] sh — open a shell inside a running container (useful for debugging).
docker compose build — rebuild images after Dockerfile changes.
That is 95% of what you need daily.
Environment Variables in Docker
Do not hardcode configuration in Dockerfiles. Use environment variables:
In docker-compose.yml, define them under the environment key for each service. Or reference an env_file that contains them. For secrets (database passwords, API keys), use the env_file approach so they are not in version control.
A common pattern: .env.docker with development-friendly defaults (database passwords that are fine to have in plain text because the database only exists in Docker and is not accessible from outside).
Networking (The Minimum You Need)
Docker Compose creates a network automatically. All services in the same compose file can reach each other by service name. Your app container connects to the database at hostname "db" (or whatever you named the database service), not "localhost."
From outside Docker (your browser, your terminal), you reach services through published ports. ports: "5432:5432" makes the database accessible at localhost:5432 from your host machine. ports: "3000:3000" makes your app accessible at localhost:3000.
The common gotcha: your application inside the container must listen on 0.0.0.0, not 127.0.0.1. Inside a container, localhost refers to the container itself. If your app listens on 127.0.0.1, other containers and the host cannot reach it.
Debugging in Containers
When something does not work:
Check if the container is running: docker compose ps. If a service shows "Exit 1," it crashed. Check its logs with docker compose logs [service].
Check connectivity: docker compose exec app ping db. Can your app container reach the database container? If not, they might be on different networks (rare with Compose).
Check inside the container: docker compose exec app sh. Poke around. Is the file system correct? Are environment variables set? Can you manually run the application command?
Check resource limits: Docker Desktop has memory limits. If your containers are getting killed (OOMKilled), increase Docker's memory allocation in settings.
Hot Reload in Docker
For development, you want code changes to reflect immediately without rebuilding the container. Mount your source code as a volume:
In docker-compose.yml, add a volume that maps your local source directory into the container's working directory. When you edit files locally, the changes are visible inside the container immediately. Combined with a file watcher (Next.js dev server, nodemon), changes trigger automatic reloads.
The override pattern: create a docker-compose.override.yml that adds the source volume mount. The base docker-compose.yml is production-like (copies code into the image). The override adds development niceties (volume mounts, debug ports, different commands).
When NOT to Use Docker for Development
Docker adds overhead. The container runtime consumes memory. Volume mounts on macOS are slower than native filesystem access (though much improved with VirtioFS). For simple applications (one service, no external dependencies), running natively is faster and simpler.
Use Docker for development when: you have multiple services to coordinate, team members have different OS/environments, or your production environment uses Docker (so dev/prod parity matters).
Skip Docker for development when: you have a single-service application with no external dependencies, your team all runs the same OS and can install dependencies easily, or Docker's overhead noticeably slows your development loop.
Our Setup at AIDToolStack
We provide both options. For Docker development: docker compose up starts the Next.js app, a MySQL database, and Redis. New contributors can start developing in 2 minutes without installing anything beyond Docker.
For native development: install Node.js, run npm install, use SQLite for the database (no external service needed). This is faster for daily development and is what most contributors prefer.
The Dockerfile and docker-compose.yml exist primarily for production deployment and for contributors who want a consistent environment without local installation.
Docker is a tool, not a religion. Use it when it simplifies your workflow. Skip it when it complicates things. The goal is productive development, not container purity.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.