Preparing a server
Everything else in the Blueprint runs on top of this. Two decisions here are hard to reverse once services are running, so they come first.
Before you start
Section titled “Before you start”Needs: a Linux host you may reconfigure (Debian 13 is what this is tested on), Docker with the Compose plugin, ports 80 and 443 free, and a domain whose DNS you control.
docker --version && docker compose versionss -tlnp | grep -E ':80 |:443 ' || echo "ports free"Decide: will anyone connect through a VPN?
Section titled “Decide: will anyone connect through a VPN?”If yes — Tailscale, WireGuard, anything where the client reaches the server directly — enable IPv6 in Docker before starting any service.
A VPN client connects with no proxy in between, so nothing records the original address in a header. The source address of the connection is the only record there is, and VPN clients prefer IPv6. On an IPv4-only Docker network that address is lost before any access policy can read it, and a policy that allows “only my VPN” will reject your VPN.
The symptom is 403 Forbidden that looks like a permissions problem.
-
Write the daemon configuration:
Terminal window sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'{"log-driver": "json-file","log-opts": { "max-size": "10m", "max-file": "3" },"ipv6": true,"ip6tables": true,"userland-proxy": false}EOF -
Check it parses, then restart:
Terminal window sudo python3 -m json.tool /etc/docker/daemon.jsonsudo systemctl restart docker
The log-opts bound container logs — without them they grow until the disk is full.
Point DNS at the server
Section titled “Point DNS at the server”A wildcard record covers every service you add later:
*.apps.example.com A 203.0.113.10*.apps.example.com AAAA 2001:db8::1Add the AAAA record only if the host has working public IPv6:
curl -6 https://api64.ipify.orgUsing split DNS on a VPN? Add the same wildcard there, pointing at the server’s VPN addresses — both families. An IPv4-only entry sends all VPN traffic over IPv4 and leaves the IPv6 path untested.
Installation
Section titled “Installation”-
Clone into its own directory:
Terminal window sudo mkdir -p /srv/blueprint && sudo chown "$USER" /srv/blueprintcd /srv/blueprintgit clone --branch main https://github.com/rubennati/secure-docker-blueprint.git . -
Set up Traefik — it terminates TLS and routes everything that follows. Follow the Traefik guide, then come back here.
-
If you enabled IPv6, add the two network ranges to
core/traefik/.envand generate your own IPv6 prefix:Terminal window RANDOM_HEX=$(openssl rand -hex 5)printf 'PUBLIC_NETWORK_SUBNET_V4=172.30.0.0/16\n'printf 'PUBLIC_NETWORK_SUBNET_V6=fd%s:%s:%s::/64\n' \"${RANDOM_HEX:0:2}" "${RANDOM_HEX:2:4}" "${RANDOM_HEX:6:4}"Generate rather than copy an example — these are private ranges, and reusing someone else’s is the IPv6 equivalent of everyone picking
192.168.1.0/24. -
Start Traefik with the dual-stack overlay:
Terminal window cd core/traefikdocker compose -f docker-compose.yml -f network-dual-stack.yml up -d
Verify
Section titled “Verify”docker compose psdocker network inspect proxy-public --format '{{range .IPAM.Config}}{{.Subnet}} {{end}}'ss -tlnp | grep -E ':80 |:443 'Expected: containers healthy, two subnets from the second command, and
listeners on both 0.0.0.0 and [::].
One subnet means the overlay was missed.
Confirm the server sees who is calling
Section titled “Confirm the server sees who is calling”Access policies, rate limits and intrusion detection all depend on this. Deploy the diagnostic endpoint:
cd ../whoamicp .env.example .env# set APP_TRAEFIK_HOST, and APP_TRAEFIK_ACCESS=acc-public for the testdocker compose up -dOpen it and read X-Real-Ip. What it should say depends on the path you came in
on, so test each one you care about and check it against the right expectation:
| Arrived over | X-Real-Ip should be |
|---|---|
| public IPv4 | your public IPv4 — what curl -4 https://api64.ipify.org reports |
| public IPv6 | your public IPv6 — curl -6 https://api64.ipify.org |
| Tailscale, directly | a Tailscale address: 100.x.x.x, or fd7a:115c:a1e0::… |
| another VPN | that tunnel’s client address |
| a CDN or upstream proxy | whatever that service forwards — check its own documentation |
The failure to look for is the same in every row: an address from the Docker network rather than the client’s.
172.30.0.1 or similar means the real address is being lost — recheck the daemon
configuration and the overlay.
# close it again afterwardssed -i 's/^APP_TRAEFIK_ACCESS=.*/APP_TRAEFIK_ACCESS=acc-private/' .envdocker compose up -dGoing further
Section titled “Going further”Rotating the proxy’s logs
Section titled “Rotating the proxy’s logs”Traefik writes access logs to disk, and Docker’s own rotation does not cover them.
sudo cp core/traefik/config/logrotate/traefik /etc/logrotate.d/traefiksudo sed -i 's|/path/to/secure-docker-blueprint|/srv/blueprint|' /etc/logrotate.d/traefiksudo logrotate -d /etc/logrotate.d/traefikThe last command is a dry run — check the path matches before relying on it.
Troubleshooting
Section titled “Troubleshooting”403 Forbidden on a service that should be reachable — rule out your browser
first:
dig +short traefik.apps.example.comIf that returns the right address but the page stays forbidden, open it in a
private window. Browsers cache DNS independently of the system, and a corrected
record is the worst case: the old host answers 403, which is indistinguishable
from an access policy rejecting you. In Chrome, clear it at
chrome://net-internals/#dns.
Certificate not issued — check the DNS provider token has the permissions the
Traefik guide lists, and watch docker compose logs traefik.
Repository files
Section titled “Repository files”core/traefik/README.md— full proxy setupcore/traefik/docs/ipv6-dual-stack.md— why IPv6 matters for VPN accessdocs/standards/logrotate.md
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content licence not yet decided