Ever wished your app "just worked" on every machine? Docker gives you consistent, isolated environments — no more "it works on my machine" drama. In this hands-on guide, you'll go from zero to a running container in five minutes flat.
Prerequisites
- Docker Desktop (macOS/Windows) or Docker Engine (Linux) installed
- A simple sample app. We'll use a minimal ASP.NET Core "Hello World" API, but you can swap in Node.js or static files.
# Check you have Docker:
docker --version
Crafting Your Dockerfile
Create a file named Dockerfile in your project root:
# 1. Base runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
# 2. Build stage
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
# 3. Final image
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
tip Replace
MyApp.dllwith your project's DLL name.
Building the Image
docker build -t my-hello-app .
-t my-hello-apptags your image.tells Docker to use the current folder as the build context- You'll see logs of each layer being built and cached
Running & Testing
Start your container and map port 80 → 8080:
docker run --rm -p 8080:80 my-hello-app
--rmcleans up the container when you stop it- Navigate to
http://localhost:8080or run:
curl http://localhost:8080
Quick Wins & Optimizations
.dockerignore — keep your build context lean:
bin/
obj/
.vs/
Layer caching — put infrequently changing instructions (e.g., RUN dotnet restore) early so Docker can cache them.
Multi-stage builds — keeps the final image slim. The example above already uses one.
Understanding the Container Lifecycle
Docker containers transition through well-defined states. Knowing these helps you manage, debug, and automate effectively.
| State | What it means |
|---|---|
| Created | Container exists, not running |
| Running | Main process is executing |
| Paused | Process temporarily frozen |
| Exited | Process finished or crashed |
| Removed | Container deleted |
Common lifecycle commands
# List all containers (running + stopped)
docker ps -a
# Start a stopped container
docker start <container_id>
# Stop gracefully
docker stop <container_id>
# Force restart
docker restart <container_id>
# Remove
docker rm <container_id>
# Pause / unpause
docker pause <container_id>
docker unpause <container_id>
Why it matters:
- CI/CD automation — detect "Exited" states to trigger alerts or restarts
- Resource cleanup — proactively remove unused containers to free disk space
- Debugging — start a crashed container interactively to inspect logs or shell in
Troubleshooting Tips
# Verbose build output
docker build --progress=plain .
# Shell into a running or broken image
docker run --rm -it my-hello-app /bin/bash
Common pitfalls: wrong file paths, missing dependencies, port mismatches.
Next Steps
# Scan for vulnerabilities
docker scout cves my-hello-app
# Tag and push to a registry
docker tag my-hello-app myregistry/my-hello-app:v1.0
docker push myregistry/my-hello-app:v1.0
From here: automate with GitHub Actions — integrate docker build and docker push on every PR.