Essential Docker Security Tips for Self-Hosting
Secure your self-hosted services, from defense to monitoring!
Table of Contents
- 🧗♀️ For the brave
- 🔄 The
:latest
Dance - 🔐 Secrets Management: The Right Way
- 🌐 Network Hazard
- 🛡️ Access Controls
- 🔍 Monitoring & Verification
- ⏰ Often Overlooked Tips
- 🚀 Production Checklist
- 📚 Further Reading
🧗♀️ For the brave
If you’re self-hosting Docker services, security is your responsibility from top to bottom—no cloud provider to shield you from port scans or sloppy config. Whether you’re spinning up apps on your home network or renting VPSes from providers like Vultr, DigitalOcean, Linode, AWS, Azure, or Google Cloud, you’ll need to lock things down - and verify you did it right.
In this guide, we’ll walk through Docker security—from some lesser-known
to other difficult-to-get-right
techniques; we’ll explore canary tokens, read-only volumes, firewall rules, network segmentation & hardening, adding authenticated proxies, and more.
We’ll also compare home networks to public cloud setups and show you how to set up a basic auth proxy with Nginx. By the end, you’ll have several options to keep out the riff-raff (friends, family, and sometimes even yourself…)
That’s a ton of stuff! But much of it relates, and you can pick and choose what’s most relevant to your setup. 🍀
🔄 The :latest
Dance
Keeping images updated is crucial for security. However, relying on :latest
can lead to breaking changes or vulnerabilities creeping in unnoticed.
The Safe Way to Update
Combine startup commands with pull
or build
to ensure you’re always running the latest image.
Version Pinning vs Latest
Choosing the right version to pin to is a balancing act between stability and security. Here are some common strategies:
Use Dependabot or Renovate to automate version updates and ensure you’re reviewing changes before they break production.
Let me know about your favorite tools for keeping Docker images up-to-date!
🔐 Secrets Management
There are many ways to manage secrets, but one of the most important rules to stick to is: never hard-code secrets into your docker images or commit them to git. It’s one of the most common security mistakes, it presents a long-term risk, and it’s a pain to fix.
Securely storing secrets is a substantial topic with many options, from .env
files, Docker secrets, 1Password/Bitwarden, or a secrets manager like HashiCorp Vault or AWS Secrets Manager.
You’ll have to choose the “right” level of effort & security for your use case.
Generate Strong Secrets
Here’s a small script to generate new secrets for an .env
file:
Canary Tokens
Canary Tokens are a great way to detect if your secrets have been compromised (and used.) They’re like a tripwire you can add to any sensitive files, urls, and tokens.
Put them in every .env
file, CI platform, password and secrets manager you use!
There are many types of canary “tokens” to chose, from AWS tokens, fake credit card numbers, Excel & Word files, Kubeconfig files, VPN credentials, even sql dump files can have a tripwire!
Canary Token Best Practices
- Place Everywhere: In every
.env
file, CI/CD pipeline, and “secrets manager” you can think of.- Place a
passwords.xlsx
orpasswords.docx
file in your home directory. - Add an AWS profile
billing_prod
with a canary token as the secret. - Generate a
private.key
file for your~/.ssh
directory. - Create a Canary SQL dump
all_credit_cards.sql
for your~/backups
directory.
- Place a
- Monitor: Set up email rules/alerts to catch when a canary token is triggered.
Upgrade from .env
to MacOS Keychain
For Mac folks, one of the simplest options is to use Keychain.
Here’s a simple way to automate loading secrets from the OSX keychain, supports TouchID
, and is a bit more secure than .env
files.
The original credit goes to Brian Hetfield and Jan Schaumann
🌐 Network Hazard
Custom Networks & Internal Ports
Properly isolating services with Docker networks is an important way to reduce your attack surface area.
Be careful poking holes in your network! One misconfigured port forward can end very badly.
By default, services on a private LAN won’t be exposed to the internet-you have to explicitly forward ports from your router.
Docker on LAN
Whether you’re a developer running dev servers locally, or self-hosting services from your local network, assumptions about docker’s network model can lead to trouble.
Devs are often surprised to find the ‘traditional’ methods to secure linux servers (iptables
, restricting tcp/ip sysctl options) can fail silently on Docker hosts! This is especially the case when self-hosting-or running on a typical home network. (For the people in the back: This can allow access to dev containers on your MacBook!!!)
⚠️ Warning #1: By default, Docker (on Ubuntu/Debian) will bypass UFW/iptables rules, rendering your firewall useless. See issue #690: Docker bypasses ufw firewall rules.
⚠️ Warning #2: Binding ports to local IP addresses (e.g.,
-p 127.0.0.1:8080:80
) may offer limited protection in certain cases. See issue #45610: Publishing ports explicitly to private networks should not be accessible from LAN hosts. (Impacts Fedora, Ubuntu, and likely others.)
If you’re surprised to learn this, same!
Binding to local IPs is still a good practice and has a meaningful impact in managed cloud environments and specially configured networks.
Example Docker Compose
Here’s an example docker-compose.yml
file that binds the app
service to 127.0.0.1:8080
and connects both containers to the backend
custom network.
Network Best Practices
- 🏆 Don’t Publish ANY Ports Recently I learned this is more useful than you might expect! When using a named (bridge) network, containers have unfiltered access to each other. They behave as though they are behind a local network (NAT gateway.)
- While not possible in all use cases, this may be useful for containers running batch jobs, or primarily accessed via
attach
orexec
.
- While not possible in all use cases, this may be useful for containers running batch jobs, or primarily accessed via
- 🥇 Use Docker Networks to isolate and control which containers can talk to each other.
- 🥉 Use Localhost Binding: While imperfect, you’re generally better off binding ports to a loopback address (e.g.,
127.0.0.1:8080:80
). Just make sure you verify your setup.
🛡️ Access Controls
Access controls are a critical part of securing your Docker services. This includes limiting container capabilities & permissions, restricting access to the Docker socket, and more.
- Limiting Container Capabilities
- Docker Socket Access
- Blocking Country!
- Hardening CloudFlare Proxy Host
Limiting Container Capabilities
Another solid access control practice is to limit the capabilities of your containers. This can prevent several threats, from privilege escalation, data theft/exfiltration, traffic hijacking, and more.
What are capabilities? Linux kernel-defined, named permissions or abilities. (The capabilities
man page has a full list.) They include things like CAP_CHOWN
(change file ownership), CAP_NET_ADMIN
(configure network interfaces), CAP_KILL
(kill any process), and many more.
The two ways to determine needed capabilities are:
- Trial and Error: This slower-but-effective method has you start with no capabilities, then add them back one by one until your app works.
- Find prior work: Search for “
project-name
cap_drop
Dockerfile”, or “project-name
cap_drop
docker-compose.yml” to see if others have already done the work for you. Sometimes ChatGPT can conjure up the right configuration for you, too!
Capabilities Best Practice
- Drop All Capabilities: Use
cap_drop: [ ALL ]
to drop all Linux capabilities from the container. - No New Privileges: Use
security_opt: [ no-new-privileges=true ]
to prevent the container from gaining new privileges.
Now your services can communicate with each other via the db-network
network. The auto-net
network will be created automatically by Docker Compose.
Use the --external
/external:
option to join a pre-existing network. Omit it to create a new network.
Docker Socket Access
⚠️ Warning: docker.socket
is extremely powerful and dangerous dark magic!
⚠️ The `:ro` option doesn’t affect I/O sent over the socket!
It merely ensures the file (a Unix socket, not a file in the traditional sense) itself is read-only
.
Socket Best Practice
- 🥇 Avoid mounting a docker socket, there’s likely a better alternative!
- 🫣 If you must, use a Reverse Proxy to limit what API commands are available. Check out the
docker-socket-proxy
project originally from Tecnativa, docker-socket-proxy. - 🤢 Okay, maybe sharing it is okay in a very high-trust, low-risk test environment.
Blocking Country!
Another decent idea!
Talking about the geopolitical entity, not the music…
If you are hosting apps mostly for your local family & friends, you can block traffic from countries you don’t expect to receive traffic from. Or only allow traffic from countries you do expect.
Check out this script to block all traffic from China (sorry, China):
Similarly, you can allow only traffic from the US:
Hardening CloudFlare Proxy Host
If your home server is protected behind a CloudFlare IP (proxy,) you can restrict access to only CloudFlare IPs, and your local network.
This is a bit similar to Country blocking above, but with much tighter control.
To test geo-based changes a VPN with locations in the desired country can be useful. See more in Monitoring & Verification section.
App Layer Security
Once your network and host are security hardened, you may find there’s more to do.
Now we need to think about the “application” layer of our services themselves.
Does that database have a valid password? Does this container automate HTTPS/certs? Does the app include built-in auth? Are there limits on which emails can signup? Are there default credentials or environment variable to change?
The only way to know is to check. In this case, start with the README
and other key files like docker-compose.yml
, Dockerfile
, and .env.*
. In both the project, and ideally its supporting services as well. (e.g. Postgres, Redis, etc.)
Reverse Proxy
Another layer of defense is basic auth. I know it’s dangerous to use without HTTPS, but sometimes it’s the best you can do (legacy services), and it’s often enough to stop automated Cross-Site-Request-Forgery attacks.
Generate credentials:
With a basic auth proxy, attackers have an extra hurdle—username and password—before hitting your internal service.
Another option is to use a service like Traefik or Caddy that can automate HTTPS and basic auth for you.
If you want to manage many domains & services with a GUI, I’d recommend Nginx Proxy Manager.
🔍 Monitoring & Verification
This is the most important & most overlooked step. You can have the best firewall, the best network, and the best practices, but if you don’t verify, you have no idea if it’s working.
Plus, knowing just a handful of commands-or where to look them up-can mean the difference in preventing a breach. The feeling of being a hacker is just a bonus. (For details and examples, jump ahead to the Monitoring & Verification section.)
Don’t Trust, Verify Twice
Check Your Ports
⚠️ IMPORTANT: Do not scan hosts you do not own.
Whether you’re on a home network or a VPS, you will want to know what ports are open to the world.
There are 2 ways to do this:
- Check the Network (
nmap
,masscan
) - Ask the Operating System (
lsof
,netstat
,ss
)
Testing Outside Your Network
You’ll need your current (public) IP, easily with services like ifconfig.me
: curl https://ifconfig.me
. Or look it up in your hosting provider’s dashboard.
Once you have your public IP, you now need to connect to an external network. You can use a friend’s computer, a phone/5G hot spot, or a dedicated server host.
Test Inside Your Network
Practice using nmap
, scan your local network or one of your servers, check your router, printer, smart fridge.
Example Scan Commands
View Open Ports
Get familiar with lsof
- it’s available on MacOS & Linux. It shows granular network state and disk activity.
Example Output
File monitoring
To identify which processes are using the most hard drive bandwidth, you can use iotop
:
To see individual file changes, you can use inotifywait
on Linux or fswatch
on MacOS:
This can be useful to detect unauthorized or strange behavior per folder or system wide.
On MacOS you can use fswatch
:
Install with brew install fswatch
⏰ Often Overlooked Tips
-
Rate Limiting for authentication attempts & any other key endpoints. Whether via Nginx’s
limit_req
module orfail2ban
for SSH access, throttling brute-force is probably a good idea. I say probably because in the age of IPv6 and botnets-for-cheap, well, it’s not what it used to be. -
Use Read-Only Volumes where possible:
Combined with other best practices (non-root users, minimal folder permissions), the `:ro` volume mount option provides additional safe-guards against accidental (or malicious) changes to critical files.
-
Audit Container Access regularly. If a container doesn’t need it a secret, port or mount, remove it!
-
Beware of WiFi Riff-Raff I’m sure you’d never give out your WiFi password, especially to any weirdos, right? Well, except some friends… Okay, maybe family too. You never know what apps they have and which might share your SSID & password with the world.
Home Network vs. Public Provider vs. Tunneling
-
Virtual Isolation/DMZ: For home servers, put them on a separate VLAN or DMZ if possible. This keeps your internal devices off-limits to potential compromise from the server side.
- Use a separate router or VLAN for your home server.
- Use a separate WiFi network for your home server.
- Use a separate subnet for your home server.
-
Cloud Providers: Hetzner, Vultr, DigitalOcean, Linode, AWS, Azure, and Google Cloud all provide different firewall features.
- Some providers & services block ports by default. Some offer opt-ins or add-ons. Check your service provider’s documentation.
- Many providers offer advanced monitoring and threat detection services.
-
VPNs & Tunneling: Consider using a VPN-like option or tunneling service to securely connect services across the internet without exposing them to the public internet.
- TailScale, ngrok, ZeroTier.
- WireGuard, OpenVPN.
🚀 Production Checklist
- Secrets: All secrets randomly generated and securely stored
- Updates: Container update strategy documented and automated. (Okay if it’s just a few commands in a text file.)
- Network: Only necessary ports exposed, internal networks set up.
- Firewall Rules: Default deny, explicit allows, country blocks if needed.
- Reverse Proxy: Nginx, Caddy or Traefik can add a layer of basic auth
- Canary Tokens: Place them alongside all sensitive keys. In every
.env
you have floating around. - Monitoring Know thy systems with
nmap
,lsof
,inotifywait
,glances
, etc. - Backup Strategy: Tested, preferably automated, and off-site.
- Least Privilege: Non-root container users, read-only volumes.
📚 Further Reading
- Docker Security Best Practices
- OWASP Docker Security Cheat Sheet
- CIS Docker Benchmark
- Canarytokens.org for Canary Tokens
Thanks
A shout-out to some keen Redditors:
Thanks for reading! I hope you found this guide helpful. If you have any questions or suggestions, feel free to reach out on my socials below, or feel free to click the Edit on GitHub
link to create a PR! ❤️