In 2016, I built an app that relied on Docker to provide a VPN deployment service. Part of the automation allowed me to automatically generate, and serve up to users deploying the VPN containers to their servers the ability to download their config through a single-use UI to ensure only a single copy can be downloaded.
I won’t go into the details of the application itself, and focus solely on this one function (which will inform the lifecycle of the container, and thus the service), however, for the curious, you can check out the application component here:
Like the title suggests, the config will mostly rely on a shell script opening and terminating connections to the configuration provider service, and it does this using the socat Linux utility. This is a very configurable utility, and one with a lot of options available via the Socket interface:
The relevant ones for us in this example are SO_REUSEADDR and the Linux fork()system call to create a new child process (we won't be using this, but I'll explain why you might in other circumstances--if you're not already familiar with process management in Unix-like systems).
Typically, to relay data with socat, you would use a command like:
socat -d -d TCP-LISTEN:8080,reuseaddr,fork EXEC:"cat config"
where config is the file you want to read from disk into the response in my example.
In your TCP-LISTEN block (you can use many other bind options, but because this will primarily serve traffic between Docker containers using an overlay network, and the ingress to the single use file will, itself, use TLS on the frontend application, I used TCP port), you specifed the port (8080), and then reuseaddr (which is optional for us because we only plan to use the interface once with no retry--this primarily ensures we can bind local addresses for this service), and then fork, to create a new process when the request completes.
and then we can make requests, and see that the socat process stays bound to port 8080:
Okay, great, right? So, in our case, we dont want to be able to request this data a second time, which as you can see below from our output, our process exited 0, a child process is created, and binds to port 8080 again for a new connection:
... 2018/07/07 04:35:22 socat N exiting with status 0 2018/07/07 04:35:22 socat N childdied(): handling signal 17 2018/07/07 04:35:23 socat N accepting connection from AF=2 100.115.92.193:60238 on AF=2 100.115.92.197:8080 2018/07/07 04:35:23 socat N forked off child process 466 2018/07/07 04:35:23 socat N listening on AF=2 0.0.0.0:8080 ...
so let’s modify our command on the server side, to look like:
without fork, to prevent a new process to replace the completed request from running:
and you’ll see that after this process exits, every subsequent request fails:
Like I said, this TCP port will only be exposed over the Docker container network overlay, where a frontend service will make a request like this:
get "/config" do response = HTTParty.get("http://dockvpn_serveconfig_1:8080") tempfile = Tempfile.new('client.ovpn') File.open(tempfile.path,'w') do |f| f.write response.body end send_file(tempfile) end
to create a downloadable copy of the config and provide it to the UI, and with our socat command as the backend in _servconfig, only the first request will be successful.
So, back on our serveconfig service, we’ll create a script, entrypoint.sh, like this:
#!/bin/bash socat -d -d \ TCP-LISTEN:8080,reuseaddr \ EXEC:'cat client.http' \ 2\>\> http8080.log
and a Dockerfile like:
FROM ubuntu:trusty RUN apt-get update ; \ apt-get install -y socat COPY entrypoint.sh serveconfig ENTRYPOINT serveconfig
Our ENTRYPOINT here is effectively just our socat command, so as soon as the process exits, the container will stop, and in the context of a Composefile like:
the serveconfig service will cease being available (all containers having completed), and like in our manual CLI example, subsequent requests will fail to connect! This means, if the service spins up, and you find that the service is unavailable, this is a solid indicator that either the service has failed, or the asset has already been requested, and you should re-provision this resource, something Docker makes incredible easy to duplicate and reproduce procedurally using declarations like the above composefile.