Module 8: Docker Security Hardening — The Production Checklist
🎯 What You'll Build: A production-ready, security-hardened Docker Compose deployment using defense-in-depth principles.
⏱️ Estimated Time: 6 hours 📋 Prerequisites: Module 7 — Docker Compose Networking, Volumes & Production Deployment 🎯 Level: Intermediate → Advanced
Introduction
Running an application inside Docker does not automatically make it secure.
Docker provides isolation, namespaces, resource controls, networking, and other security features, but your application still needs to be configured correctly. A production deployment should assume that one security layer may eventually fail.
This is where defense in depth becomes important.
Instead of relying on a single security mechanism, we protect the application at multiple layers:
1 Internet 2 │ 3 ▼ 4 ┌─────────────┐ 5 │ Nginx │ 6 │ TLS + Rate │ 7 │ Limiting │ 8 └──────┬──────┘ 9 │ 10 ▼ 11 ┌─────────────┐ 12 │ Application │ 13 │ Validation │ 14 │ Auth / CSRF │ 15 └──────┬──────┘ 16 │ 17 ┌────────┴────────┐ 18 ▼ ▼ 19 ┌─────────────┐ ┌─────────────┐ 20 │ Database │ │ Redis │ 21 │ Internal │ │ Internal │ 22 │ Network │ │ Network │ 23 └─────────────┘ └─────────────┘
The goal is not simply to make a container "secure."
The goal is to make the entire deployment resilient against common attacks and configuration mistakes.
1. What Is Docker Security Hardening?
Docker security hardening means reducing the privileges, attack surface, and unnecessary exposure of containers and their services.
A default development deployment might look like this:
1services: 2 app: 3 build: . 4 ports: 5 - "8000:8000"
This may work perfectly.
However, production security requires asking questions such as:
- Does the container run as root?
- Does it really need a writable filesystem?
- Which Linux capabilities does it need?
- Is the database exposed to the internet?
- Are secrets stored in Git?
- Can one compromised container access another?
- Are images scanned for vulnerabilities?
- Is Nginx limiting abusive requests?
- Is HTTPS enabled?
- Are containers consuming unlimited CPU or memory?
Security hardening addresses these questions systematically.
2. The Apartment Building Analogy
Think of your production infrastructure as an apartment building.
Each security mechanism provides another layer of protection.
| Docker Security Layer | Apartment Analogy | Purpose |
|---|---|---|
| Non-root user | Tenant has their own key | Limits privileges |
| Read-only filesystem | Tenant cannot modify building walls | Prevents unwanted filesystem changes |
| Capabilities | Tenant gets only required permissions | Reduces Linux privileges |
| Internal network | Restricted hallway | Prevents unnecessary access |
| Firewall | Building entrance security | Controls external traffic |
| Rate limiting | Security guard limits repeated entry | Mitigates abuse |
| Secrets management | Locked safe | Protects credentials |
| Image scanning | Building inspection | Finds known vulnerabilities |
| Resource limits | Electricity/water limits | Prevents resource exhaustion |
| TLS | Secure communication | Protects data in transit |
The important principle is:
Never give a container more access than it actually needs.
This is the principle of least privilege.
3. Security Layer 1 — Run Containers as Non-Root
One of the most important Docker security improvements is avoiding unnecessary root privileges.
Check your container:
1docker compose exec app whoami
If it returns:
1root
the application is running as root.
That does not automatically mean the container is vulnerable, but it gives a compromised process more privileges than necessary.
Create a Dedicated User
For a Python application:
1FROM python:3.12-slim 2 3RUN useradd \ 4 --create-home \ 5 --uid 1000 \ 6 appuser 7 8WORKDIR /app 9 10COPY requirements.txt . 11 12RUN pip install --no-cache-dir -r requirements.txt 13 14COPY . . 15 16RUN chown -R appuser:appuser /app 17 18USER appuser 19 20CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Now the application runs as:
1appuser
rather than root.
Compose-Level User
You can also specify the UID and GID in Compose:
1services: 2 app: 3 user: "1000:1000"
Verify it:
1docker compose exec app id
You should see something similar to:
1uid=1000 gid=1000
Important
Do not blindly use 1000:1000.
The UID/GID must match a user that can actually run the application and access the required files.
4. Security Layer 2 — Read-Only Filesystem
A compromised application may attempt to modify files inside its container.
For example, an attacker could attempt to:
- install malicious files
- modify application code
- create persistence files
- modify configuration
- write malware
- alter executable files
A read-only root filesystem makes this considerably harder.
1services: 2 app: 3 read_only: true
Now the container's root filesystem cannot normally be modified.
However, some applications legitimately need writable locations.
For example:
1/tmp 2/cache 3/uploads
You can provide temporary writable storage using tmpfs:
1services: 2 app: 3 read_only: true 4 5 tmpfs: 6 - /tmp
This creates an in-memory temporary filesystem.
Why This Matters
Without a read-only filesystem:
1Compromised application 2 │ 3 ▼ 4Write malicious file 5 │ 6 ▼ 7Modify container filesystem
With a read-only filesystem:
1Compromised application 2 │ 3 ▼ 4Attempt filesystem modification 5 │ 6 ▼ 7 DENIED
This is another layer of defense.
5. Security Layer 3 — Linux Capabilities
Linux capabilities divide traditionally powerful root privileges into smaller permissions.
Instead of giving a process broad privileges, you can remove capabilities it does not need.
For example:
1services: 2 app: 3 cap_drop: 4 - ALL
This removes all optional Linux capabilities from the container.
If the application genuinely needs one, explicitly add it:
1services: 2 app: 3 cap_drop: 4 - ALL 5 cap_add: 6 - CHOWN
Principle
Start with:
1cap_drop: 2 - ALL
Then add only the capabilities required by the application.
Do not automatically add capabilities just because an application fails.
First determine why the application needs the capability.
6. Security Layer 4 — Protect the Docker Socket
One of the most dangerous mistakes is exposing the Docker socket unnecessarily:
1volumes: 2 - /var/run/docker.sock:/var/run/docker.sock
The Docker socket provides powerful control over the Docker daemon.
If an attacker compromises a container that has unrestricted access to the Docker socket, the attacker may be able to control other containers or create privileged containers.
Therefore:
Do not mount
/var/run/docker.sockunless the workload genuinely requires Docker API access.
For most application containers:
1services: 2 app: 3 # No docker.sock mount
is the safer configuration.
7. Security Layer 5 — Network Isolation
One of the biggest production mistakes is exposing every service to the public internet.
For example:
1services: 2 nginx: 3 ports: 4 - "80:80" 5 - "443:443" 6 7 app: 8 ports: 9 - "8000:8000" 10 11 db: 12 ports: 13 - "5432:5432" 14 15 redis: 16 ports: 17 - "6379:6379"
This exposes more services than necessary.
A better architecture is:
1Internet 2 │ 3 ▼ 4 Nginx 5 │ 6 ▼ 7 App 8 │ 9 ├──────────► PostgreSQL 10 │ 11 └──────────► Redis
Only Nginx needs public ports.
8. Create an Internal Database Network
Docker Compose supports internal networks.
1networks: 2 public: 3 driver: bridge 4 5 backend: 6 driver: bridge 7 internal: true
Then:
1services: 2 3 nginx: 4 networks: 5 - public 6 - backend 7 8 app: 9 networks: 10 - backend 11 12 db: 13 networks: 14 - backend
The database is now isolated from the public-facing network.
The application communicates with PostgreSQL using the Compose service name:
1db:5432
not:
1localhost:5432
and not a publicly exposed IP.
9. Never Expose Your Database Publicly
Avoid:
1db: 2 ports: 3 - "5432:5432"
unless you have a specific operational requirement and have secured that exposure separately.
For normal production deployments:
1db: 2 networks: 3 - backend
is enough.
The database should be reachable by the services that require it, not by the entire internet.
10. Security Layer 6 — Nginx Rate Limiting
Even a properly secured application can be abused through excessive requests.
For example:
1Attacker 2 │ 3 ├── Request 4 ├── Request 5 ├── Request 6 ├── Request 7 ├── Request 8 └── thousands more 9 │ 10 ▼ 11 Django
This can consume:
- CPU
- memory
- database connections
- application workers
- bandwidth
Nginx can provide an additional rate-limiting layer.
Example:
1http { 2 limit_req_zone $binary_remote_addr 3 zone=api_limit:10m 4 rate=10r/s; 5 6 server { 7 location /api/ { 8 limit_req zone=api_limit burst=20 nodelay; 9 10 proxy_pass http://app:8000; 11 } 12 } 13}
Here:
110r/s
means approximately ten requests per second per client key.
The burst value allows a temporary burst before requests are rejected.
Important
Rate limits should match your application.
Do not blindly copy 10r/s into every production system.
Login, password-reset, search, upload, and public API endpoints may need different limits.
11. Rate Limit Sensitive Endpoints
Authentication endpoints deserve special attention.
For example:
1location /api/login/ { 2 limit_req zone=login_limit burst=5 nodelay; 3 4 proxy_pass http://app:8000; 5}
You might use a stricter policy for:
1/login 2/register 3/password-reset 4/api/token
while allowing higher limits for static content.
12. Security Layer 7 — HTTPS and TLS
Production applications should use HTTPS.
The architecture should look like:
1Browser 2 │ 3 │ HTTPS 4 ▼ 5Nginx 6 │ 7 │ HTTP inside private network 8 ▼ 9Application
Nginx can terminate TLS:
1server { 2 listen 443 ssl http2; 3 server_name example.com; 4 5 ssl_certificate /etc/nginx/ssl/fullchain.pem; 6 ssl_certificate_key /etc/nginx/ssl/privkey.pem; 7 8 location / { 9 proxy_pass http://app:8000; 10 } 11}
Redirect HTTP to HTTPS:
1server { 2 listen 80; 3 server_name example.com; 4 5 return 301 https://$host$request_uri; 6}
For real production systems, use a trusted certificate authority and keep certificates renewed automatically.
13. Security Headers
Nginx can add additional HTTP security headers.
For example:
1add_header X-Content-Type-Options "nosniff" always; 2 3add_header X-Frame-Options "SAMEORIGIN" always; 4 5add_header Referrer-Policy "strict-origin-when-cross-origin" always;
A Content Security Policy can also provide strong protection against certain classes of browser-side attacks, but it should be designed around the application's actual scripts, styles, APIs, and third-party resources rather than copied blindly.
Example starting point:
1add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'self';" always;
Test CSP carefully before enforcing a strict production policy.
14. Security Layer 8 — Application Input Validation
Infrastructure security cannot replace application security.
Your application must validate user input.
For Django:
1from django import forms 2 3 4class UserForm(forms.Form): 5 username = forms.CharField(max_length=150) 6 email = forms.EmailField()
Validation should happen on the server.
Never assume that because the frontend validates something, the backend is safe.
For example:
1Next.js validation 2 │ 3 ▼ 4 Django 5 │ 6 ▼ 7Server-side validation 8 │ 9 ▼ 10 Database
The backend should always treat incoming data as untrusted.
15. Frontend Validation Is Not Security
Suppose your Next.js frontend checks:
1if (username.length < 3) { 2 return; 3}
An attacker can completely bypass this by sending an HTTP request directly:
1curl -X POST https://example.com/api/users/
Therefore:
Client-side validation improves user experience. Server-side validation provides security.
Use both, but never depend only on the frontend.
16. Security Layer 9 — Protect Secrets
Never commit secrets like this:
1environment: 2 SECRET_KEY: "my-super-secret-key" 3 DB_PASSWORD: "password123"
And never commit:
1.env
to Git.
Your .gitignore should include:
1.env 2.env.* 3!.env.example
An example configuration can be committed safely:
1DATABASE_HOST=db 2DATABASE_PORT=5432 3DATABASE_NAME=app 4DATABASE_USER=app 5DATABASE_PASSWORD=change-me
But .env.example must not contain real production credentials.
17. Docker Secrets
For sensitive production environments, Docker secrets can provide a better mechanism than ordinary environment variables.
Example:
1services: 2 app: 3 secrets: 4 - db_password 5 6secrets: 7 db_password: 8 file: ./secrets/db_password.txt
Inside the container, the secret is made available as a file under the Docker secrets mechanism.
Your application can read the secret from that file rather than embedding the credential directly into the Compose configuration.
For larger production environments, dedicated secret managers such as cloud secret-management systems or Vault may be more appropriate.
18. Security Layer 10 — Image Security
Your application can be perfectly configured while the underlying image contains known vulnerabilities.
For example:
1FROM python:3.12
may contain more packages than your application actually requires.
Prefer smaller, maintained base images where appropriate:
1FROM python:3.12-slim
However:
Small does not automatically mean secure.
The image still needs to be maintained and scanned.
19. Scan Docker Images with Trivy
One popular approach is to scan images before deployment.
Example:
1trivy image myapp:latest
You can also scan the filesystem:
1trivy fs .
A CI pipeline can enforce a policy such as:
1Build image 2 │ 3 ▼ 4Run tests 5 │ 6 ▼ 7Security scan 8 │ 9 ├── Critical vulnerability → FAIL 10 │ 11 └── Acceptable → Deploy
This moves security checks earlier in the development lifecycle.
20. Pin Production Images
Avoid relying entirely on moving tags such as:
1image: postgres:latest
A new image can change unexpectedly.
Prefer a known version:
1image: postgres:16-alpine
For stronger supply-chain control, production pipelines can pin images by immutable digest.
The trade-off is that digest pinning requires an explicit update process when you want to move to a newer image.
21. Security Layer 11 — Resource Limits
Security is also about availability.
A compromised container may consume enormous amounts of CPU or memory.
For example:
1services: 2 app: 3 deploy: 4 resources: 5 limits: 6 cpus: "1.0" 7 memory: 512M
Depending on the Compose/runtime environment, resource controls should be verified against the actual Docker deployment mode being used.
The goal is to prevent one service from consuming the entire host.
22. Health Checks
Health checks allow Docker to determine whether a service is functioning.
Example:
1services: 2 app: 3 healthcheck: 4 test: 5 [ 6 "CMD", 7 "python", 8 "-c", 9 "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" 10 ] 11 interval: 30s 12 timeout: 5s 13 retries: 3 14 start_period: 20s
A good health endpoint should perform a lightweight application check.
Do not make the health check unnecessarily expensive.
23. Hardened Docker Compose Example
Here is a practical production-style example combining the major concepts:
1services: 2 3 nginx: 4 image: nginx:alpine 5 restart: unless-stopped 6 7 ports: 8 - "80:80" 9 - "443:443" 10 11 read_only: true 12 13 tmpfs: 14 - /var/cache/nginx 15 - /var/run 16 - /tmp 17 18 cap_drop: 19 - ALL 20 21 cap_add: 22 - NET_BIND_SERVICE 23 24 depends_on: 25 app: 26 condition: service_healthy 27 28 networks: 29 - public 30 - backend 31 32 app: 33 build: 34 context: . 35 dockerfile: Dockerfile 36 37 restart: unless-stopped 38 39 user: "1000:1000" 40 41 read_only: true 42 43 tmpfs: 44 - /tmp 45 46 cap_drop: 47 - ALL 48 49 expose: 50 - "8000" 51 52 environment: 53 DATABASE_HOST: db 54 DATABASE_PORT: "5432" 55 56 depends_on: 57 db: 58 condition: service_healthy 59 60 healthcheck: 61 test: 62 [ 63 "CMD", 64 "python", 65 "-c", 66 "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" 67 ] 68 interval: 30s 69 timeout: 5s 70 retries: 3 71 start_period: 20s 72 73 networks: 74 - backend 75 76 db: 77 image: postgres:16-alpine 78 79 restart: unless-stopped 80 81 environment: 82 POSTGRES_DB: app 83 POSTGRES_USER: app 84 85 volumes: 86 - postgres_data:/var/lib/postgresql/data 87 88 expose: 89 - "5432" 90 91 healthcheck: 92 test: 93 [ 94 "CMD-SHELL", 95 "pg_isready -U app -d app" 96 ] 97 interval: 10s 98 timeout: 5s 99 retries: 5 100 101 networks: 102 - backend 103 104 105volumes: 106 postgres_data: 107 108 109networks: 110 111 public: 112 driver: bridge 113 114 backend: 115 driver: bridge 116 internal: true
What Changed?
The production configuration now includes:
1Nginx 2 ├── Public network 3 ├── HTTPS 4 ├── Rate limiting 5 ├── Read-only filesystem 6 └── Minimal capabilities 7 8Application 9 ├── Non-root user 10 ├── Read-only filesystem 11 ├── No public port 12 ├── Minimal capabilities 13 └── Health check 14 15Database 16 ├── No public port 17 ├── Private network 18 ├── Persistent volume 19 └── Health check
24. Important: Don't Copy Security Settings Blindly
A security configuration must match the application.
For example, this:
1read_only: true
may break an application that expects to write to:
1/app 2/tmp 3/cache 4/logs
Similarly:
1cap_drop: 2 - ALL
may break software that genuinely requires a specific capability.
The correct workflow is:
1Start application 2 │ 3 ▼ 4Apply security restriction 5 │ 6 ▼ 7Test application 8 │ 9 ├── Works → Keep restriction 10 │ 11 └── Fails 12 │ 13 ▼ 14 Identify requirement 15 │ 16 ▼ 17 Grant minimum access
This is much safer than adding broad privileges until everything works.
25. Production Security Checklist
Before deploying, review every layer.
Container Security
- Containers do not run as root unnecessarily
- Filesystem is read-only where practical
- Writable paths use explicit volumes or
tmpfs - Linux capabilities are minimized
- Docker socket is not unnecessarily mounted
- Privileged mode is avoided
- Containers have resource limits where appropriate
Network Security
- Only required ports are publicly exposed
- Database is not publicly accessible
- Redis is not publicly accessible
- Internal services use private Docker networks
- Nginx is the public entry point
- Firewall rules restrict unnecessary inbound traffic
Application Security
- Server-side input validation exists
- Authentication is enforced
- Authorization is enforced
- CSRF protections are configured where applicable
- Debug mode is disabled in production
- Secure cookies are enabled
- Error responses do not expose sensitive information
Secrets
- Production secrets are not committed to Git
-
.envfiles are protected -
.env.examplecontains no real credentials - Secrets are rotated periodically
- Production credentials differ from development credentials
Image Security
- Base images are maintained
- Images are vulnerability scanned
- Unnecessary packages are removed
- Production versions are controlled
- Images are rebuilt regularly
- CI blocks unacceptable vulnerabilities
Web Security
- HTTPS is enabled
- HTTP redirects to HTTPS
- Security headers are configured
- API rate limiting is configured
- Login endpoints have stricter rate limits
- TLS certificates are monitored and renewed
26. Security Testing
After deploying the hardened stack, verify the configuration.
Check Running Containers
1docker compose ps
Check Container Users
1docker compose exec app id
Check Network Configuration
1docker network ls
Then inspect the network:
1docker network inspect <network_name>
Check Exposed Ports
1docker compose ps
You should see public ports only where they are actually required.
For example:
1nginx 0.0.0.0:80->80/tcp 2nginx 0.0.0.0:443->443/tcp 3app 8000/tcp 4db 5432/tcp
Notice that 8000 and 5432 do not need to be published to the host.
27. Test the Read-Only Filesystem
Enter the application container:
1docker compose exec app sh
Then attempt:
1touch /test-file
With a correctly configured read-only filesystem, the operation should fail.
That is expected.
Test the writable temporary directory:
1touch /tmp/test-file
This should work if /tmp was intentionally configured as writable tmpfs.
28. Test Network Isolation
From the application container, test the database:
1nc -zv db 5432
It should be reachable.
From an unrelated container or external host, the database should not be publicly reachable if it has not been published.
This demonstrates the principle:
1Application ───────► Database 2 │ 3 └────────────X Internet
29. Security Validation with Docker Bench
For broader Docker host and container security checks, Docker Bench for Security can be useful.
It evaluates a range of Docker security configuration recommendations.
However, automated scanners should be treated as guidance rather than absolute truth.
A scanner finding does not automatically mean your deployment is exploitable, and a clean scan does not prove that your application is secure.
Security requires both automated checks and architectural review.
30. Common Docker Security Mistakes
Mistake 1 — Running Everything as Root
1user: root
Problem: unnecessary privileges.
Better:
1user: "1000:1000"
when compatible with the application.
Mistake 2 — Exposing PostgreSQL
1ports: 2 - "5432:5432"
Problem: unnecessarily exposes the database.
Better: keep PostgreSQL on an internal Docker network.
Mistake 3 — Exposing Redis
1ports: 2 - "6379:6379"
Problem: Redis becomes reachable outside the intended application network.
Better: expose it only to services that require it.
Mistake 4 — Mounting Docker Socket
1- /var/run/docker.sock:/var/run/docker.sock
Problem: grants powerful Docker daemon access.
Better: avoid it unless there is a documented requirement.
Mistake 5 — Using privileged: true
1privileged: true
Problem: gives the container broad access to host-level capabilities.
Better: use the smallest required permissions.
Mistake 6 — Storing Secrets in Git
1DB_PASSWORD: "production-password"
Problem: Git history can permanently retain credentials.
Better: use proper secret-management mechanisms.
Mistake 7 — Using latest
1image: nginx:latest
Problem: deployments can change unexpectedly.
Better: control versions and update them intentionally.
Mistake 8 — Trusting Frontend Validation
1if (email.includes("@")) { 2 // valid 3}
Problem: attackers can bypass the frontend completely.
Better: validate again on the server.
31. Defense in Depth
The most important lesson in this module is defense in depth.
Imagine an attacker successfully exploits a vulnerability in your application.
A weak deployment might look like:
1Application compromised 2 │ 3 ▼ 4Root container 5 │ 6 ▼ 7Writable filesystem 8 │ 9 ▼ 10Docker socket 11 │ 12 ▼ 13Host compromise
A hardened deployment creates multiple barriers:
1Application vulnerability 2 │ 3 ▼ 4Non-root container 5 │ 6 ▼ 7Limited capabilities 8 │ 9 ▼ 10Read-only filesystem 11 │ 12 ▼ 13No Docker socket 14 │ 15 ▼ 16Private network 17 │ 18 ▼ 19Restricted database access 20 │ 21 ▼ 22Monitoring + scanning
No single control is perfect.
The strength comes from multiple independent controls working together.
32. Mini Project — DeployMart v2.0
Now apply everything you learned to the DeployMart application.
Objective
Transform your previous Docker Compose deployment into a production-hardened architecture.
Your final architecture should look like:
1 Internet 2 │ 3 HTTPS :443 4 │ 5 ▼ 6 ┌─────────────────┐ 7 │ Nginx │ 8 │ TLS + Rate Limit│ 9 └────────┬────────┘ 10 │ 11 Private Network 12 │ 13 ▼ 14 ┌─────────────────┐ 15 │ Django API │ 16 │ Non-root │ 17 │ Read-only FS │ 18 │ Minimal caps │ 19 └───────┬─────────┘ 20 │ 21 ┌─────────┴─────────┐ 22 ▼ ▼ 23 ┌───────────────┐ ┌───────────────┐ 24 │ PostgreSQL │ │ Redis │ 25 │ Internal only │ │ Internal only │ 26 └───────────────┘ └───────────────┘
Requirements
DeployMart v2.0 must include:
- Nginx reverse proxy
- HTTPS
- HTTP → HTTPS redirect
- Rate limiting
- Non-root application container
- Read-only application filesystem
- Minimal Linux capabilities
- Internal database network
- No public PostgreSQL port
- No public Redis port
- Protected environment variables
- Health checks
- Image vulnerability scanning
- Resource limits where appropriate
- Production Django settings
- Secure cookies
- Security headers
- Firewall configuration
33. Deployment Workflow
Use this workflow:
1docker compose config
First validate the Compose configuration.
Then:
1docker compose build
Build your images.
Run vulnerability scanning:
1trivy image your-image:tag
Start the stack:
1docker compose up -d
Check:
1docker compose ps
Inspect logs:
1docker compose logs --tail=100
Then test:
1curl -I https://your-domain.com
Finally verify that internal services are not publicly exposed.
34. Final Production Validation
Before calling DeployMart v2.0 production-ready, answer these questions:
Container
Does the application run as root?
If yes, determine whether root is genuinely required.
Filesystem
Can the application modify arbitrary files inside the container?
If yes, consider read_only: true.
Capabilities
Does the container have unnecessary Linux capabilities?
If yes, drop them.
Network
Can the internet directly access PostgreSQL or Redis?
If yes, redesign the network.
Secrets
Are production credentials stored in Git?
If yes, remove them and rotate compromised credentials.
Images
Have your images been scanned?
If no, add vulnerability scanning to CI/CD.
Nginx
Are abusive requests rate-limited?
If no, configure appropriate limits.
HTTPS
Can users access the production site over plain HTTP?
If yes, redirect HTTP to HTTPS.
35. Key Takeaways
After completing this module, you should understand that Docker security is not a single setting.
A production deployment should combine:
1 Docker Security 2 │ 3 ┌────────────┼────────────┐ 4 ▼ ▼ ▼ 5 Container Network Secrets 6 Security Security Security 7 │ │ │ 8 ▼ ▼ ▼ 9 Non-root Internal No Git 10 Read-only Networks Secrets 11 Capabilities No DB Public Rotation 12 │ 13 └────────────┬────────────┘ 14 ▼ 15 Application 16 Security 17 │ 18 ▼ 19 Nginx + HTTPS 20 Rate Limiting 21 Security Headers 22 │ 23 ▼ 24 Image Security 25 │ 26 ▼ 27 Vulnerability Scan
The central principle is:
Reduce privileges, minimize exposure, protect secrets, validate input, and assume that individual security controls can fail.
A hardened container is useful.
A hardened system is better.
Module 8 Checkpoint
Run the following commands and verify your deployment:
1# Validate Compose 2docker compose config 3 4# Build images 5docker compose build 6 7# Start services 8docker compose up -d 9 10# Check service status 11docker compose ps 12 13# Check application identity 14docker compose exec app id 15 16# Inspect networks 17docker network ls 18 19# Inspect application logs 20docker compose logs --tail=100 app 21 22# Inspect Nginx logs 23docker compose logs --tail=100 nginx 24 25# Scan the application image 26trivy image your-app:latest
Your checkpoint is complete when:
1✓ Application starts successfully 2✓ Application runs as non-root 3✓ Database has no public port 4✓ Redis has no public port 5✓ Internal network works 6✓ Read-only filesystem works 7✓ Required writable paths work 8✓ Nginx serves HTTPS 9✓ HTTP redirects to HTTPS 10✓ Rate limiting works 11✓ Health checks pass 12✓ Secrets are outside Git 13✓ Images are vulnerability scanned 14✓ Production settings are enabled
What's Next?
In Module 9, you'll move from a single production server toward container orchestration and scaling.
You'll learn how concepts from Docker Compose translate into larger deployments:
1Docker 2 │ 3 ▼ 4Docker Compose 5 │ 6 ▼ 7Production Hardening 8 │ 9 ▼ 10Container Orchestration 11 │ 12 ▼ 13Kubernetes
You will build on the security principles from this module and apply them to a multi-service, scalable deployment.
🚀 Next Module: Container Orchestration — From Docker Compose to Kubernetes