If you have a Docker container which is running all the time but is used infrequently, it may be a good idea to start the container only when a connection is received. This is particularly useful for resource-intensive servers. In my case, I use this for a Minecraft server, which can take several gigabytes of memory even when idle, so I want to keep it running as little as possible.
This tutorial demonstrates how to start Docker containers on-demand on Linux using systemd.
1. Create the Docker container
First, we need to create the Docker container. I will be doing this using a Docker Compose file, because it makes it configuration simpler.
All you need is a docker-compose.yml
file which defines the container you need.
If you already have one, great!
We just need to change one thing: Make the container listen on a port other than the port you want the service on.
For example, if I want a Docker container running a web server hosted on port 80,
the container should actually listen on another port. In this case I’ll use 81.
Below is the docker-compose.yml
file which creates a container running an Nginx web server,
with port 81 on the host forwarded to port 80 on the container.
|
|
2. Understanding socket activation
Systemd’s socket activation can get a little confusing. To ease the confusion, let’s first take a look at what it is.
Socket activation is a feature provided by systemd. It allows you do make systemd listen on a socket instead of leaving your service running. When systemd receives a connection, it will start the service and hand the socket over to the now-running service so it can handle the connection.
This is great!
The only problem is that the service being started needs to know how to take the socket over from systemd.
Docker can’t do that.
Instead, we’ll need to use a proxy in between, which can take the socket from systemd and forward the traffic to Docker.
Luckily, systemd provides us with such a proxy: systemd-socket-proxyd
.
3. Create the systemd units
container.service
This is the simplest of the units. It’s just a regular systemd service which will start our Docker Compose stack.
Here’s the service file.
This should go in /etc/systemd/system/
and can be named anything, as long as it ends with .service
.
|
|
Normally, you’ll see an [Install]
section in systemd services.
Here, we actually don’t want one.
We don’t want systemd to keep our service running all the time.
You’ll see how it gets started later.
proxy-to-container.socket
This is the socket unit (see systemd.socket(5)
) which tells systemd to create a socket and listen for connections.
Here, use the port you want your service to appear on. For my example of a web server, I want it to listen on port 80, so I’ll use 80 here.
Like before, this file should be installed in /etc/systemd/system/
.
It can be named anything, as long as it ends with .socket
.
|
|
proxy-to-container.service
This is the most complicated of the units. This service will need to start the proxy daemon and connect it to our container.
The name of this service matters. It must have the same name as the socket unit created in the last step. This is how systemd knows they’re related.
|
|
Let’s take a look at the arguments to systemd-socket-proxyd
in the last line.
The last argument is the most important;
it tells the proxy what address to connect to and forward traffic to/from.
In our Docker Compose file, we forwarded port 81 on the host to 80 on the Docker container,
so we want to connect to port 81 on the current machine, i.e. localhost:81
.
Note: You could also use the container’s IP address and avoid publishing the port, but that requires either static IP configuration or dynamically detecting the container’s address, which is too complicated for this tutorial.
The other argument to the proxy daemon is --exit-idle-time=10min
.
This tells it to exit after 10 minutes of being idle, i.e. not receiving any connections.
You can change this duration to meet your needs,
or remove the option entirely to prevent the service from stopping automatically.
See systemd-socket-proxyd(8)
for details on how to format the time duration.
Again, we don’t give this service an [Install]
section, because we don’t want it running all the time.
We only want it to be activated when proxy-to-container.socket
is activated.
4. Starting and enabling our new services
Note: All of the commands in this section will likely need to be run as root.
First, run this command to prompt systemd to load the new units we’ve created:
# systemctl daemon-reload
Then, all we need to do is enable the socket unit:
# systemctl enable --now proxy-to-container.socket
The --now
option tells systemd to also start the unit.
It’s equivalent to running enable
followed by start
.
5. Understanding how the units work together
You may notice that we never told systemd when to start proxy-to-container.service
.
We enabled the socket, but not the service.
Well, you may have guessed the answer when I hinted earlier that the proxy service and socket must have the same name.
When systemd receives a connection for a .socket
unit, it automatically starts the .service
unit with the same name
and passes it the socket.
(This can be overridden using Service=
. See systemd.socket(5)
.)
Okay, let’s imagine our service is not running, and a user sends a request to port 80. Here’s what happens (in order):
- The socket systemd has created and listened to for
proxy-to-container.socket
receives a connection. - Systemd attempts to start
proxy-to-container.service
. - Because that service depends on
container.service
, systemd will first startcontainer.service
. This will bring up our Docker Compose stack, which starts listening on port 81. - Now that the dependencies are met,
proxy-to-container.service
starts. This runs thesystemd-socket-proxyd(8)
daemon. systemd-socket-proxyd
takes the socket from systemd and receives the connection from our user. It forwards all the data tolocalhost:81
.- The container is listening on port 81, so it receives the request and sends a response.
systemd-socket-proxyd
forwards traffic both directions, sending the response back to the user.
Great! We’ve started a Docker service on-demand when it is needed.
One more thing: what happens if we don’t get any requests for a while?
- Because we specified
--exit-idle-time
earlier,systemd-socket-proxyd(8)
will automatically stop after the configured amount of time. - Systemd notices this and marks
proxy-to-container.service
as stopped. - Systemd notices that because
proxy-to-container.service
is now stopped, nothing depends oncontainer.service
anymore. - Because we set
StopWhenUnneeded=true
oncontainer.service
, it will now be stopped by systemd. - Nothing happened to
proxy-to-container.socket
. Only the service exited, not the socket. Thus systemd is still listening for new connections and is ready to re-start the services when one is received.
That’s all…
If you found this article useful, consider checking out other articles I’ve written, or take a look at my public projects on GitHub.