Docker container directly accessible from Docker Host?

Hello, I am deeply confused. What I am trying to accomplish is to have 2 docker containers:

  • service 1 (radicale): only accessible by service 2 (nginx)
  • service 2 (nginx): accessible through Docker host

This means that from Docker host, you shouldn’t be able to connect to service 1 directly, only using nginx reverse proxy. I have a setup below that I thought should isolate me from directly accessing service 1 from host, but it doesn’t as I can simply curl the docker IP. Anyone know why this is the behaviour, and how I can prevent host from accessing Docker container directly?

test:

~ curl http://172.18.0.2:5232 -L
Radicale works!

this is my service 1 (docker-compose.yaml)

services:
  radicale:
    image: tomsquest/docker-radicale:3.2.3.1
    container_name: radicale
    command: ["/venv/bin/radicale", "--config", "/config/radicale_config"]
    deploy:
      resources:
        limits:
          cpus: 0.2
          memory: 256M
    user: 2999:2999
    networks:
      - radicale_net
    restart: unless-stopped
    volumes:
      - ./volumes/data:/data:cached
      - ./volumes/config:/config:ro

networks:
  radicale_net:
    name: radicale_net
    driver: bridge
    attachable: false
    external: false

container:

~ sudo docker inspect radicale

[
    {
        "Id": "51193c7be82a53a3924054ff76d1f959ce861423f8c8d5fa7166351a08c87197",
        "Created": "2024-09-29T09:13:57.979920621Z",
        "Path": "docker-entrypoint.sh",
        "Args": [
            "/venv/bin/radicale",
            "--config",
            "/config/radicale_config"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 66643,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2024-09-29T09:13:58.095158211Z",
            "FinishedAt": "0001-01-01T00:00:00Z",
            "Health": {
                "Status": "healthy",
                "FailingStreak": 0,
                "Log": [
                    {
                        "Start": "2024-09-29T10:14:28.294052075+01:00",
                        "End": "2024-09-29T10:14:28.351100474+01:00",
                        "ExitCode": 0,
                        "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0Redirected to /.web\r100    19  100    19    0     0  10662      0 --:--:-- --:--:-- --:--:-- 19000\n"
                    }
                ]
            }
        },
        "Image": "sha256:308232673ce3556a85140162fdee0a0549450dbe5c41c732b6f9669f44231647",
        "ResolvConfPath": "/mnt/dietpi_userdata/docker-data/containers/51193c7be82a53a3924054ff76d1f959ce861423f8c8d5fa7166351a08c87197/resolv.conf",
        "HostnamePath": "/mnt/dietpi_userdata/docker-data/containers/51193c7be82a53a3924054ff76d1f959ce861423f8c8d5fa7166351a08c87197/hostname",
        "HostsPath": "/mnt/dietpi_userdata/docker-data/containers/51193c7be82a53a3924054ff76d1f959ce861423f8c8d5fa7166351a08c87197/hosts",
        "LogPath": "",
        "Name": "/radicale",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": [
                "/home/dietpi/docker-compose/projects/radicale/volumes/data:/data:rw",
                "/home/dietpi/docker-compose/projects/radicale/volumes/config:/config:ro"
            ],
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "journald",
                "Config": {}
            },
            "NetworkMode": "radicale_net",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "unless-stopped",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "ConsoleSize": [
                0,
                0
            ],
            "CapAdd": null,
            "CapDrop": null,
            "CgroupnsMode": "private",
            "Dns": null,
            "DnsOptions": null,
            "DnsSearch": null,
            "ExtraHosts": [],
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 268435456,
            "NanoCpus": 200000000,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": null,
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": null,
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "MemoryReservation": 0,
            "MemorySwap": 536870912,
            "MemorySwappiness": null,
            "OomKillDisable": null,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware",
                "/sys/devices/virtual/powercap"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/mnt/dietpi_userdata/docker-data/overlay2/2c252fdfbc00b89b487f52939f7915ea6a557230cced740103ab64fbb310bcdb-init/diff:/mnt/dietpi_userdata/docker-data/overlay2/52ca03d3d536e9ea9b0419b552be7c782c996759df995a915a4b191cc26eb220/diff:/mnt/dietpi_userdata/docker-data/overlay2/ef2b1240e7e14e4dd46f9193b5b4673c8d39c753164737fc850f1c3194886cc3/diff:/mnt/dietpi_userdata/docker-data/overlay2/5961873ab3c288c93613737e1c380075aba4a88cfe90bff80a2830c633439931/diff:/mnt/dietpi_userdata/docker-data/overlay2/b7d98527ca16701ede7270c2a35bb19e980f8d017bb5051fe8d654d77644d859/diff",
                "MergedDir": "/mnt/dietpi_userdata/docker-data/overlay2/2c252fdfbc00b89b487f52939f7915ea6a557230cced740103ab64fbb310bcdb/merged",
                "UpperDir": "/mnt/dietpi_userdata/docker-data/overlay2/2c252fdfbc00b89b487f52939f7915ea6a557230cced740103ab64fbb310bcdb/diff",
                "WorkDir": "/mnt/dietpi_userdata/docker-data/overlay2/2c252fdfbc00b89b487f52939f7915ea6a557230cced740103ab64fbb310bcdb/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/home/dietpi/docker-compose/projects/radicale/volumes/data",
                "Destination": "/data",
                "Mode": "rw",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Type": "bind",
                "Source": "/home/dietpi/docker-compose/projects/radicale/volumes/config",
                "Destination": "/config",
                "Mode": "ro",
                "RW": false,
                "Propagation": "rprivate"
            }
        ],
        "Config": {
            "Hostname": "51193c7be82a",
            "Domainname": "",
            "User": "2999:2999",
            "AttachStdin": false,
            "AttachStdout": true,
            "AttachStderr": true,
            "ExposedPorts": {
                "5232/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "COMMIT_ID=",
                "VERSION=3.2.3",
                "BUILD_UID=2999",
                "BUILD_GID=2999",
                "TAKE_FILE_OWNERSHIP=true"
            ],
            "Cmd": [
                "/venv/bin/radicale",
                "--config",
                "/config/radicale_config"
            ],
            "Healthcheck": {
                "Test": [
                    "CMD-SHELL",
                    "curl --fail http://localhost:5232 || exit 1"
                ],
                "Interval": 30000000000,
                "Retries": 3
            },
            "Image": "tomsquest/docker-radicale:3.2.3.1",
            "Volumes": {
                "/config": {},
                "/data": {}
            },
            "WorkingDir": "",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {
                "com.docker.compose.config-hash": "62cc6e06622e82715e4a2c794c64cfe16cfc9d5822a07714d476872d14f09ab9",
                "com.docker.compose.container-number": "1",
                "com.docker.compose.depends_on": "",
                "com.docker.compose.image": "sha256:308232673ce3556a85140162fdee0a0549450dbe5c41c732b6f9669f44231647",
                "com.docker.compose.oneoff": "False",
                "com.docker.compose.project": "radicale",
                "com.docker.compose.project.config_files": "/home/dietpi/docker-compose/projects/radicale/docker-compose.yaml",
                "com.docker.compose.project.working_dir": "/home/dietpi/docker-compose/projects/radicale",
                "com.docker.compose.service": "radicale",
                "com.docker.compose.version": "2.29.7",
                "maintainer": "Thomas Queste <tom@tomsquest.com>",
                "org.label-schema.description": "Enhanced Docker image for Radicale, the CalDAV/CardDAV server",
                "org.label-schema.name": "Radicale Docker Image",
                "org.label-schema.schema-version": "1.0",
                "org.label-schema.url": "https://github.com/Kozea/Radicale",
                "org.label-schema.vcs-ref": "",
                "org.label-schema.vcs-url": "https://github.com/tomsquest/docker-radicale",
                "org.label-schema.version": "3.2.3"
            }
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "07c273ea946588aa703f561641dd67200a323b363b9d870e7d58590e26cd34a6",
            "SandboxKey": "/var/run/docker/netns/07c273ea9465",
            "Ports": {
                "5232/tcp": null
            },
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "",
            "Gateway": "",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "",
            "IPPrefixLen": 0,
            "IPv6Gateway": "",
            "MacAddress": "",
            "Networks": {
                "radicale_net": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": [
                        "radicale",
                        "radicale"
                    ],
                    "MacAddress": "<redacted>",
                    "DriverOpts": null,
                    "NetworkID": "1bfe1ee5a97868daa2b8014083565e34fe0b04914969e42d6e79db10d793d868",
                    "EndpointID": "b93ec72ae57798cffe84cda63b437ec394f2020a1163fd1fa36b27f97ff0694e",
                    "Gateway": "172.18.0.1",
                    "IPAddress": "172.18.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "DNSNames": [
                        "radicale",
                        "51193c7be82a"
                    ]
                }
            }
        }
    }
]

network

~ sudo docker network inspect radicale_net

[
    {
        "Name": "radicale_net",
        "Id": "1bfe1ee5a97868daa2b8014083565e34fe0b04914969e42d6e79db10d793d868",
        "Created": "2024-09-29T10:13:57.822674463+01:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "51193c7be82a53a3924054ff76d1f959ce861423f8c8d5fa7166351a08c87197": {
                "Name": "radicale",
                "EndpointID": "b93ec72ae57798cffe84cda63b437ec394f2020a1163fd1fa36b27f97ff0694e",
                "MacAddress": "<redacted>",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "radicale_net",
            "com.docker.compose.project": "radicale",
            "com.docker.compose.version": "2.29.7"
        }
    }
]

/edit I finally figured why docker host can communicate after typing this all down. You docker host is essentialy a gateway, you can check via ip a that you have been assigned docker interface, in my case its inet 172.18.0.1/24. I am still unsure on the solutions

Where did you run curl? Inside another container?

nope from docker host where the containers start

Why do you need to restrict access from the host? If you want, you could implement a feature in the application running in the container that rejects requests coming from the host, but as long as your app listens on the container IP, it will be available from the host And you need the app to listen on the container IP if you want nginx to be able to access it. That IP will not be available from other machines.

You could try to implement some hack with firewalls trying not to break Docker’s network, but otherwise the only way not to accept requests from the host would be listening on the container’s loopback IP while using the nginx proxy’s network namspace. Which would cause other issues like what port can be used for the application and also that features (like webadmin sometimes) accessible only from localhost would be accessible through the nginx proxy.

One other way during development would be using Docker Desktop which runs everything in a VM, so container IPs would not work, but it is not for running containers in production.

1 Like