The client authenticates, PPP negotiates an address, and then nothing moves. Ping fails, the browser fails, and the container looks connected the whole time. That pattern is not a routing-table problem, because routing problems usually break the control connection too. It is the payload path, and in PPTP the payload is a different protocol from the one that just succeeded.

Five checks, in the order that saves the most time.

Check 1: separate the control channel from the payload

PPTP authenticates over TCP 1723 and then carries the PPP payload inside GRE, IP protocol 47. GRE is neither TCP nor UDP, so every rule that forwards the control port says nothing about it. A NAT or firewall that passes 1723 can drop protocol 47 without complaint, and the session stays half-built in a way that looks like success.

If the login works and no packet comes back, assume GRE first and prove it second.

Check 2: confirm the helper on the host kernel

Connection tracking cannot link a TCP control connection to a GRE flow without a helper. The module is nf_conntrack_pptp, it lives in the host kernel, and loading it inside the container image achieves nothing because modules are host-wide. lsmod | grep nf_conntrack_pptp on the Docker host is the check. If it is missing, modprobe nf_conntrack_pptp loads it for the running kernel and tells you nothing about the next boot, so add it to the host configuration too.

A module that disappears at reboot becomes the same ticket next month.

Check 3: read the kernel policy that disables it

Even with the module present, kernels since 4.7 default net.netfilter.nf_conntrack_helper to 0, which switches off automatic helper assignment for every protocol. That default is why a correctly installed module still does nothing: the code is there and the policy says not to use it. sysctl net.netfilter.nf_conntrack_helper reports the current value, and setting it to 1 at runtime makes the helper available again. Persist the value through the host sysctl configuration if the design permits it.

Automatic assignment lets traffic create expectations implicitly, which is exactly why the default changed. Scope it to the trusted host, or use an explicit rule that targets the helper.

Check 4: look at the container path separately

A loaded helper does not create a route. The container needs a path to the PPTP server, and host forwarding has to permit GRE in both directions: iptables -L FORWARD -v -n shows whether protocol 47 is accepted and whether the counters move. Docker masquerade rules translate TCP and UDP, not protocol 47, so a bridged container may need an explicit forward accept and, on some hosts, its own translation rule. Confirm as well that the container has the PPP device support its image expects.

Read the counters rather than the rule text. A rule that exists and matches nothing is decoration.

Check 5: prove GRE is tracked, then decide about PPTP

With the client running, read the tracking table. An established PPTP control entry plus a related GRE entry means the helper is linking the flows and the remaining fault is elsewhere. A control entry with no GRE entry means the payload is dropped or untracked, which puts you back at checks 2 and 3.

PPTP itself is legacy and MS-CHAPv2 is weak. If the server also offers L2TP/IPsec, IKEv2 or WireGuard, moving to one of those removes this whole class of problem.

Where PPTP has to stay, running the client on the host instead of inside a container removes one layer of NAT and filtering, which is often the cheaper decision.

འབྲེལ་བའི་རྩོམ་ཡིག