Docker lets you package an application and its dependencies into an image, then run that image in a container. This is useful when you want to try a project without installing its language runtime and libraries directly on your computer. In this guide, you’ll build and run a small Python web app, publish its port, pass an environment variable, check its logs, and clean up the container. You’ll also learn what Docker does not isolate or preserve automatically.
Key takeaways
- A Dockerfile describes how to build an image; a container is a running instance of an image.
- Dependencies installed during the image build are available inside the container, not automatically on your host.
- Use port publishing to make a container service reachable from your computer.
EXPOSEalone does not publish a port. - A container’s writable layer is temporary. Use a volume or bind mount when data needs to persist.
- Docker reduces setup work, but it does not make untrusted code or images safe by itself.
What Docker changes about application setup
Without Docker, you might install a language runtime, install project packages, configure environment variables, and resolve version conflicts on your computer. With Docker, the project’s Dockerfile specifies an environment for building an image. Docker installs the project’s dependencies into that image, and a container runs from it.
You still need Docker installed and running on the host. Docker also downloads the base image and dependencies during the build, so an internet connection may be needed. Once the image has been built, running it usually does not require installing the application’s language dependencies on the host.
An image is a template; a container is an instance created from that template. Containers share the host’s operating-system kernel, so they are isolated environments, not full virtual machines.
Build a small Python application image
Create a new directory and add these two files. The example uses Flask to make a small web app. Flask will be installed in the image during the build, not on your computer’s Python environment.
app.py
import os
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
mode = os.environ.get("APP_MODE", "not set")
return f"Hello from Docker! APP_MODE={mode}\n"
if __name__ == "__main__":
port = int(os.environ.get("PORT", "8000"))
app.run(host="0.0.0.0", port=port)
The app binds to 0.0.0.0 inside the container so that Docker’s port forwarding can reach it. Binding only to 127.0.0.1 inside the container would make the service reachable only from within that container.
requirements.txt
Flask==3.1.0
Pinning a dependency version makes this example more predictable than installing an unspecified latest version. For a real project, choose versions that are compatible with your application and update them deliberately.
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV PORT=8000
EXPOSE 8000
CMD ["python", "app.py"]
FROM selects the base image, WORKDIR sets the working directory, and COPY adds project files to the image. RUN executes during the build, while CMD specifies the default command when a container starts. EXPOSE documents the port the app uses; it does not make that port accessible from the host on its own.
Build and run the container
Run these commands from the directory containing the three files. Docker must be running.
Build an image and give it a name and tag:
docker build -t demo-web:1.0 .
The final . is the build context: the directory Docker can use to find files referenced by the Dockerfile. The first build may take longer because Docker needs to download the base image and install Flask. Later builds can reuse unchanged layers.
Start the app in the background and publish its container port on your computer:
docker run -d --name demo-web -p 127.0.0.1:8080:8000 -e APP_MODE=demo demo-web:1.0
The options mean:
-druns the container in the background.--name demo-webgives it a convenient name for later commands.-p 127.0.0.1:8080:8000maps host port 8080 to container port 8000 and limits the published host address to the local machine.-e APP_MODE=demosets an environment variable inside the container.demo-web:1.0is the image to run.
Open http://127.0.0.1:8080/ in a browser, or check the response with:
curl http://127.0.0.1:8080/
You should see Hello from Docker! APP_MODE=demo. The host port and container port can differ; if port 8080 is already in use, choose another host port, such as 127.0.0.1:8081:8000.
Configure the app and inspect its logs
Environment variables are a convenient way to supply non-secret configuration when a container starts. For example, the app uses APP_MODE and allows PORT to be overridden. If you change the container port, make the published container port match it. For example, running with -e PORT=9000 requires a mapping such as -p 127.0.0.1:8080:9000.
Read the app’s output with:
docker logs demo-web
Follow new log output as it arrives with:
docker logs -f demo-web
Press Ctrl-C to stop following the logs. That does not stop the background container. Stop it separately with docker stop demo-web.
Clean up containers and images
Stop and remove the container when you are finished:
docker stop demo-web
docker rm demo-web
To also remove the image, use:
docker rmi demo-web:1.0
You can instead start a disposable container with docker run --rm ...; Docker removes that container after its main process exits. This option is useful for short-lived commands, but it does not remove the image. A web server keeps running until it is stopped, so a detached server container will not be removed immediately.
Data persistence: what happens when a container is removed?
Files written to a container’s writable layer are associated with that container. If you remove the container, those changes are not preserved as a general-purpose data store. The image itself also does not capture changes made after the container starts.
For data that needs to outlast a container, mount a volume or a host directory at the path where the app writes data. For example, a named volume can be mounted like this:
docker run -d --name demo-web -p 127.0.0.1:8080:8000 -v app-data:/app/data demo-web:1.0
This only preserves files the application writes under /app/data; it does not automatically persist files written elsewhere. A bind mount can be useful during development when you want container processes to access host files, but changes can flow between the host and container. Choose mounts carefully and avoid exposing sensitive host directories unnecessarily.
Common mistakes and troubleshooting
- The browser cannot connect: Confirm the container is running with
docker ps, check the published port, and verify that the app binds to0.0.0.0inside the container. - The host port is already in use: Change the left-hand port in
-p, for example127.0.0.1:8081:8000, then visit port 8081. - A dependency is missing: Check that it appears in
requirements.txtand that the build completed successfully. Rebuild the image after changing dependencies. - You changed a file but see old behavior: The running container uses the image it was created from. Build a new image, then recreate the container to use it.
- You expect
EXPOSEto publish the app: It does not. Use-pwhen starting the container to map a host port. - You expect files to survive removal: Use a volume or bind mount for the specific data directory that must persist.
Security and practical limitations
Docker is a packaging and isolation tool, not a guarantee that a program is safe. Only run images and application code you trust, and keep the Docker engine and base images maintained. For production, use a deliberately selected and maintained base image, and consider pinning it by digest when you need stronger control over exactly which image is used.
Avoid running the application as root when a non-root user is sufficient. Do not put passwords, API keys, or other secrets in a Dockerfile, image layers, or source control. Environment variables are useful for ordinary configuration, but they may be exposed through container inspection or operational tooling; use an appropriate secrets-management mechanism for sensitive values.
Publishing a port can make a service reachable beyond your computer depending on the address and network configuration. This example binds to 127.0.0.1 on the host to limit access to local connections. Review port bindings and firewall rules before exposing a service to a network. Also, the Flask development server is intended for development, not as a production web server; use a production-appropriate server and deployment setup for a real service.
Conclusion
Docker lets you build an application environment once and run it without installing the application’s language dependencies directly on your host. Start with a Dockerfile, build an image, publish the app’s port, and pass runtime configuration with environment variables. Before relying on a container, decide how its data will persist and apply appropriate security controls.
