How to use Docker containers on the Whonix workstation
TLDR: You can also use Docker on Whonix, forcing the connections to go through Tor.
How to install Docker
As usual we install docker according to this tutorial.
Example Docker Container
This tutorial guides you through running a Nowhere blog instance within a Docker container, hosted on the Whonix Workstation.
Refer to the main tutorial on running the OPSEC blog for general instructions. Due to the Whonix Workstation environment here, certain details will differ from a standard Linux setup in that tutorial.
The main way to get Docker to work on Whonix is to configure a custom network interface within the configuration file, assign a static IP address to the Docker container, and implement two firewall rules to permit traffic forwarding to and from the container.
First, clone the blog.
[workstation user ~]% git clone http://gdatura24gtdy23lxd7ht3xzx6mi7mdlkabpvuefhrjn4t5jduviw5ad.onion/nihilist/blog-deploy
Cloning into 'blog-deploy'...
...
[workstation user ~]%
Important
You'll now need to edit the docker-compose.yml file. Note the lines marked as important. (You may copy and paste the entire configuration below into your YML file.)
[workstation user ~]% cd blog-deploy
[workstation user ~/blog-deploy]% cat docker-compose.yml
services:
blogmk-puller:
image: alpine:latest
container_name: blogmk_puller
env_file:
- .env
volumes:
- ./repo:/repo
- ./servable:/servable
- ./entry.sh:/entry.sh:ro
extra_hosts:
- "host.docker.internal:host-gateway"
entrypoint: ["sh", "/entry.sh"]
restart: unless-stopped
networks:
blogmk-net:
ipv4_address: 10.5.0.2 # <<< Important
blogmk-server:
image: nginx:alpine
container_name: blogmk_server
volumes:
- ./nginx.conf:/etc/nginx/sites-enabled/default:ro
- ./servable:/usr/share/nginx/html:ro
ports:
# Don't use 127.0.0.1:7080 here, otherwise the Whonix Gateway cannot access the service.
- "7080:80"
restart: unless-stopped
networks:
blogmk-net:
driver:
bridge
ipam:
config:
- subnet: 10.5.0.0/24
gateway: 10.5.0.1
driver_opts:
com.docker.network.bridge.name: blog_server_net # <<< Important
Please note that we're assigning a static IP address of 10.5.0.2 to the blogmk-puller container. Furthermore, we've configured a custom name for the Linux network interface (blog_server_net) via the com.docker.network.bridge.name setting within driver_opts.
These two configurations are optional; you can utilize the default IP and interface provided by Docker. However, manually setting these configuration options simplifies creating a reliable firewall rule should either the IP or the interface name change.
Configuring the firewall for Docker
By default, the Whonix Workstation firewall blocks all traffic designated for forwarding. We need to permit only forward traffic originating from the Docker container and returning to it.
As per the Whonix documentation:
1. Boot into Sysmaint mode

2. Open /usr/bin/user-firewall-script as root. If the file doesn't exist, create it.
[workstation user ~]% sudoedit /usr/bin/user-firewall-script
3. Append the following to the file:
#!/bin/bash
# Enable traffic forwarding to and from the single Docker container.
nft insert rule inet filter forward iifname "blog_server_net" ip saddr 10.5.0.2 counter accept
nft insert rule inet filter forward oifname "blog_server_net" ip daddr 10.5.0.2 counter accept
Please note that this allows traffic from the container's IP address, originating from the blog_server_net interface. We also permit traffic directed to the container, which exits the host via the blog_server_net interface and enters the container.
This applies to multiple Docker containers too. Simply modify the IP address and interface name within the other containers' compose.yml files. Subsequently, add new firewall rules incorporating the updated IP address and interface.
4. Enable the firewall script to run at boot
[workstation user ~]% sudo chmod +x /usr/bin/user-firewall-script
# Verify that it functions correctly (no typos etc.)
[workstation user ~]% sudo user-firewall-script
# Establish a directory for the systemd service
[workstation user ~]% sudo mkdir -p /lib/systemd/system/whonix-firewall.service.d
# Edit this new file
[workstation user ~]% sudoedit /lib/systemd/system/whonix-firewall.service.d/50_user.conf
# Place this text in 50_user.conf
[Service]
ExecStartPost=/usr/bin/user-firewall-script
# Reload unit files
[workstation user ~]% sudo systemctl daemon-reload
# Reload firewall
[workstation user ~]% sudo whonix_firewall
I recommend rebooting afterward to confirm the firewall rules are effective. Reboot, then run:
[workstation user ~]% sudo nft list chain inet filter forward
You'll observe:
table inet filter {
chain forward {
type filter hook forward priority filter; policy drop;
oifname "blog_server_net" ip daddr 10.5.0.2 counter packets 0 bytes 0 accept
iifname "blog_server_net" ip saddr 10.5.0.2 counter packets 0 bytes 0 accept
counter packets 0 bytes 0 drop
}
}
Configuring the firewall to open the port
Now, configure the Whonix Workstation firewall to permit the Gateway to connect to the port of the blog server. This enables you to host the blog as a hidden service, as the Tor daemon runs on the Gateway, not the Workstation. (Whonix docs)
# Edit the firewall configuration file
# If it does not exist, create it with:
# sudo mkdir -p /usr/local/etc/whonix_firewall.d/
# sudo touch /usr/local/etc/whonix_firewall.d/50_user.conf
[workstation user ~]% sudoedit /usr/local/etc/whonix_firewall.d/50_user.conf
# Append this line to the file:
EXTERNAL_OPEN_PORTS+=" 7080 "
# Reload the firewall
[workstation user ~]% sudo whonix_firewall
Now, running
[workstation user ~]% sudo nft list ruleset | grep 7080
should yield
tcp dport 7080 counter packets 0 bytes 0 accept
Running the container
Prior to running this container, modify the entry.sh file located at the root of the blog-deploy directory.
Change this line:
git config --global --add http.proxy socks5h://localhost:9050
git config --global --add http.proxy socks5h://<IP-of-Whonix-Gateway>:9050
Substitute ip a s on the Gateway to determine the IP address assigned to the eth1 interface (the interface that connect the Gateway and Workstation together).
Now, proceed to run the blog container.
# Navigate into the directory where you cloned the repository
[workstation user ~]% cd blog-deploy
# .env file holds environment variables related to your mirror configuration
[workstation user ~]% cat .env
SITE_URL=http://yourblogmirrorurl.onion
BRANCH=main
REPO_URL=http://gdatura24gtdy23lxd7ht3xzx6mi7mdlkabpvuefhrjn4t5jduviw5ad.onion/nihilist/the-opsec-bible
REFRESH_SEC=900
[workstation user ~/blog-deploy]% docker compose up -d
[+] up 18/18
β Image nginx:alpine Pulled 60.3s
β Image alpine:latest Pulled 21.2s
β Network blog-deploy_default Created 0.0s
β Network blog-deploy_blogmk-net Created 0.0s
β Container blogmk_puller Started 0.6s
β Container blogmk_server Started 0.5s
[workstation user ~/blog-deploy]%
Wait some time. View the container logs using docker container logs blogmk_puller. I had to restart the container twice after receiving logs concerning a timeout connecting to the Datura Git server (most likely caused by an unreliable Tor circuit). Before restarting the container, delete the repo/ and servable/ directories to have a clean environment.
First:
[workstation user ~/blog-deploy]% rm -rf repo/ servable/
Then:
[workstation user ~/blog-deploy]% docker container restart blogmk_puller
After, verify that the logs confirm the Git command is successfully cloning the blog.
Setup the Hidden Service
Whonix Docs for this section.
Open a terminal on the Whonix Gateway and edit this file:
[gateway user ~/blog-deploy]% sudoedit /usr/local/etc/torrc.d/50_user.conf
Append:
HiddenServiceDir /var/lib/tor/opsec-blog
HiddenServicePort 80 10.152.152.11:7080
(10.152.152.11 is the IP of the Whonix Workstation)
Reload tor:
[gateway user ~/blog-deploy]% sudo systemctl restart tor@default.service
Obtain the hostname of your hidden service:
[gateway user ~/blog-deploy]% sudo cat /var/lib/tor/opsec-blog/hostname
<your-hostname>.onion

Voila, we're hosting our own instance of the Opsec Bible, running within a Whonix Workstation VM for enhanced anonymity. This setup can also work with other Docker containers as mentioned.
Suggest changes
Potato 2026-03-13
Donate XMR to the author:
8B1fDn9NVYfeBH8FwyZ6Ls43diALwPvyvLJ3uUupVsB2QJHZTmJ3K2Dib37efpx7Rg3FKSNEd1sgLDqfi8Q1YLBrS8Se5Z5