G'day:
OK so I have this docker-swarm-ified (is that a word?) app. See "On the other hand… Docker Swarm" and "Getting my brain around Docker Secrets". This is great but I'll need users to stay on the same session when they switch PHP containers, so this is the exercise I'm undertaking this afternoon.
For the sake of learning, I decided to use Redis as the backing storage for this. I could have used the DB but somehow it seems not wrong, but "less good than it could be" to have session data (so: infrastructure domain) in a data store that's for business domain data. Plus also just for the hell of it.
Creating a Redis container
This was dead easy:
# docker-compose.yml
services:
#[...]
redis:
container_name: redis
image: redis:8.0-bookworm
ports:
- "6379:6379"
stdin_open: true
tty: true
volumes:
- redis-data:/data
command: ["redis-server", "--appendonly", "yes"]
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 3s
retries: 3
volumes:
# [...]
redis-data:
# [...]
For what we need it to do, we don't even need any specific config. This will work out of the box.
Configuring PHP
There's a bit more to this, but not much. I needed to add two lines to docker/php/Dockerfile.base
# [...]
COPY docker/php/usr/local/etc/php/conf.d/redis.ini /usr/local/etc/php/conf.d/redis.ini
# [...]
RUN pecl install redis && docker-php-ext-enable redis
# [...]
Where redis.ini is this:
session.save_handler = redis
session.save_path = "tcp://host.docker.internal:6379"
And also update composer.json to add the requirement for redis:
"ext-redis": "*",
And that is it. PHP will now use Redis for session storage, so whichever container services my request will use the same session I established when I first hit the app.
The Symfony side of things
I'm gonna back up slightly and cover the "enable sessions in Symfony" bit too, for the sake of completeness.
A lot of this requires an extension to be added, and then all "just works", but I added some code in to check things.
composer.json needs this dependency:
"symfony/http-foundation": "7.3.*",
That's it.
I wanted to see something session-ish on my test page, so I decided to stick a GUID into session when the session starts.
class SessionStartListener implements EventSubscriberInterface
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly GuidFactory $guidFactory
) {
}
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
$session = $this->requestStack->getSession();
if ($session->has('guid')) {
return;
}
$session->set('guid', $this->guidFactory->create());
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
}
That's a chunk of code, but it's mostly boilerplate. My bit is highlighted. I stick a key guid into session if it's not there. And its value is a GUID (duh).
That GuidFactory class is just a wrapper to a third-party lib.
And we output that:
class HomeController extends AbstractController
{
public function __construct(
private readonly VersionService $versionService,
private readonly RequestStack $requestStack,
) {
}
#[Route('/', name: 'home')]
public function index(): Response
{
return $this->render(
'home/index.html.twig',
[
'environment' => $this->getParameter('kernel.environment'),
'instanceId' => getenv('POD_NAME') ?: getenv('HOSTNAME') ?: 'unknown',
'dbVersion' => $this->versionService->getVersion(),
'sessionGuid' => $this->requestStack->getSession()->get('guid'),
]
);
}
}
{# index.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
Hello world from Symfony<br>
Mode: {{ environment }}<br>
Instance ID: {{ instanceId }}<br>
DB version: {{ dbVersion }}<br>
Session GUID: {{ sessionGuid }}<br>
{% endblock %}
When I first spun up the swarm with sessions on, but none of the Redis stuff: the session GUID was changing every request (as was the PHPSESSID cookie), because each container - on first hit - was starting a new session, sending back a PHPSESSID, but the next request was sending that PHPSESSID and the next container was going "never heard of ya… have a new PHPSESSID". Rinse and repeat. As soon as I integrated Redis into the mix, it "just worked". Sessions being distributed across the swarm.
Righto.
--
Adam