Limiting Docker Resources

Thu, Apr 7, 2022


I use locust to load test two different configurations of these containers to show the effect of customizing and constraining the resources used by the containers. This is recommended good practise for security as this can help to prevent denial of service on the host machine if the containers are overrun.

Longer version

The best way to avoid DoS attacks is by limiting resources. You can limit memory, CPU, maximum number of restarts (–restart=on-failure:<number_of_restarts>), maximum number of file descriptors (–ulimit nofile=) and maximum number of processes (–ulimit nproc=).

For this post I am going to constrain the containers CPU and Memory and then use Locust to generate some load and see some difference in performance. I will configure values for CPU, Memory, File Descriptors, Processes and failure restarts.

My intention is not to test docker here it is simply to show the effect of using these methods.

NOTE: It is useful to test different values for these configurations against your performance targets and requirements in order to find a sweet spot as it might not be immediately obvious what values you need from the outset.

I will update the Makefile and use the following values for two runs of this test:

Run 1 the Makefile will use:

--ulimit nofile=4096
--ulimit nproc=50

Run 2 the Makefile will use:

--ulimit nofile=4096 
--ulimit nproc=50 

The test will hit the list endpoint of the todos which will return [] every time as there are no todos. A better load test would exercise different endpoints to test the different functionality under load.

For the load test I am using Locust which is a popular and flexible tool for load testing. . Here is the locustfile:

from locust import HttpUser, task, constant

class TodosApiUser(HttpUser):
    wait_time = constant(0)

    def list_todos(self):

Here are the results with 0.5 CPU and 1GB Memory


Here are the results with 2 CPU and 1GB Memory


The updated Makefile is below. To make it more convenient I have added another target (loadtest_1m) to run a load test with 60 users, ramping up 2 users every second and to run for 1 minute. Each of the two tests I did:

make kill
make run

The Makefile

.PHONY: build
	docker build -t reaandrew/nginx-secure ./proxy
	(cd todos && go build -o todos main.go)
	docker build -t reaandrew/todos-secure ./todos

.PHONY: run
run: build
	docker network create --driver bridge appz

	docker run --name todos-secure --read-only \
		--network appz \
		--restart=on-failure:3 \
		--ulimit nofile=4096 \
		--ulimit nproc=50 \
		--memory="1g" \
        --cpus="2" \
		-d -t reaandrew/todos-secure

	docker run --name nginx-secure --read-only \
		--mount type=tmpfs,destination=/tmp/proxy_temp,tmpfs-size=2m \
		--mount type=tmpfs,destination=/tmp/client_temp,tmpfs-size=2m \
		--mount type=tmpfs,destination=/tmp/fastcgi_temp,tmpfs-size=2m \
		--mount type=tmpfs,destination=/tmp/uwsgi_temp,tmpfs-size=2m \
		--mount type=tmpfs,destination=/tmp/scgi_temp,tmpfs-size=2m \
		--mount type=tmpfs,destination=/tmp/nginx,tmpfs-size=1m \
		--network appz \
		--restart=on-failure:3 \
		--ulimit nofile=4096 \
		--ulimit nproc=50 \
		--memory="1g" \
		--cpus="2" \
		-d -p 8080:8080 -t reaandrew/nginx-secure

.PHONY: kill
	docker kill nginx-secure 2> /dev/null || :
	docker rm nginx-secure 2> /dev/null || :
	docker kill todos-secure 2> /dev/null || :
	docker rm todos-secure 2> /dev/null || :
	docker network rm appz 2> /dev/null || :

.PHONY: logs
	docker logs nginx-secure
	docker logs todos-secure

.PHONY: loadtest_1m
	locust --headless \
		-u 60 \
		-r 2 \
		-t 1m \
		-f loadtest/ \
		--host http://localhost:8080 \
		--html "loadtest_report_$(shell date +"%Y%m%d%H%M%S").html"