Containers with Brainstorm
Authors: Takfarinas Medani, Malte Höltershinken, and Raymundo Cassani
Some Brainstorm functions and plugins rely on external software that is distributed as containers (for example: duneuro 2026). These containers are managed in Brainstorm as container plugins.
Container plugins are not usually installed alone, their installation is commonly as a dependency for a code plugin.
This page explains how containers are used by Brainstorm, and how set up your system to use them.
Introduction
A container is a packaged version of a software tool that includes everything it needs to run.
With containers:
You do not need prior knowledge of containers to use Brainstorm.
To be able to use containers with Brainstorm it is necessary to have a supported container engine.
Container engines
Brainstorm supports the following container engines:
Docker Desktop (recommended for most users)
TODO Podman (Linux alternative to Docker)
TODO Apptainer / Singularity (recommended on HPC clusters)
Brainstorm automatically detects which runtime is available on your system.
You need to install at least one of them.
Docker Desktop (recommended)
To install, be sure of follow the instructions for your OS:
Interactive management
SHOW THE GUI FOR CONTAINER PLUGINS
Command-line management
The calls to install or manage containers plugins are the same than for (code) plugins, see the plugin tutorial.
An API for low-level interaction between Brainstorm and the container engine has been implemented in bst_containers.m
1 function varargout = bst_containers(varargin)
2 % BST_CONTAINERS: Manage containers for container-based plugins in Brainstorm
3 %
4 % USAGE:
5 % [errMsg, engineName] = bst_containers('GetEngine')
6 % [errMsg, imageList] = bst_containers('GetImages')
7 % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag])
8 % errMsg = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon], [containerArgs])
9 % [errMsg, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr)
10 % [errMsg, containerInfo] = bst_containers('GetContainerInfo', containerName)
11 % errMsg = bst_containers('StopContainer', containerName, [isForced=0])
12 % errMsg = bst_containers('RemoveImage', imageSha/Name, [isForced=0])
13
14 % @=============================================================================
15 % This function is part of the Brainstorm software:
16 % https://neuroimage.usc.edu/brainstorm
17 %
18 % Copyright (c) University of Southern California & McGill University
19 % This software is distributed under the terms of the GNU General Public License
20 % as published by the Free Software Foundation. Further details on the GPLv3
21 % license can be found at http://www.gnu.org/copyleft/gpl.html.
22 %
23 % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
24 % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
25 % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
26 % MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
27 % LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
28 %
29 % For more information type "brainstorm license" at command prompt.
30 % =============================================================================@
31 %
32 % Authors: Raymundo Cassani, 2026
33 % Takfarinas Medani, 2026
34
35 eval(macro_method);
36 end
37
38
39 %% ===== GET CONTAINER ENGINE =====
40 function [errMsg, engineName] = GetEngine(engineName)
41 % USAGE: [errMsg, engineName] = bst_containers('GetEngine') % Find, test and set a supported container engine
42 % [errMsg, engineName] = bst_containers('GetEngine', engineName) % Test the requested container engine
43 errMsg = '';
44
45 % Get and test all the supported container engines
46 if nargin < 1 || isempty(engineName) || strcmpi(engineName, 'auto-detect')
47 [~, engineNames] = bst_get('ContainerEngine');
48 % Remove 'auto-detect', first element in engineNames
49 engineNames(1) = [];
50 engineName = '';
51 isSetDefault = 1;
52 % Test only the requested container engine
53 else
54 engineNames = {engineName};
55 isSetDefault = 0;
56 end
57
58 % Tests container engines
59 isFound = 0;
60 for iEngine = 1 : length(engineNames)
61 switch engineNames{iEngine}
62 case {'docker'}
63 if ispc
64 [status, cmdout] = system(['where ' engineNames{iEngine}]);
65 if status == 0
66 cmdout = strsplit(strtrim(cmdout), '\n');
67 if ~isempty(cmdout)
68 isFound = 1;
69 enginePath = strtrim(cmdout{1});
70 end
71 end
72 else
73 [status, cmdout] = system(['which ' engineNames{iEngine}]);
74 if status == 0
75 isFound = 1;
76 enginePath = strtrim(cmdout);
77 end
78 end
79 end
80 % Break loop if found
81 if isFound
82 engineName = engineNames{iEngine};
83 break
84 end
85 end
86 % Return if not found
87 if ~isFound
88 if isempty(engineName)
89 errMsg = 'No valid container engine was found';
90 else
91 errMsg = ['Container engine ' engineName ' was not found'];
92 end
93 return
94 % Set as default the container engine found
95 elseif isSetDefault
96 bst_set('ContainerEngine', engineName);
97 end
98
99 % Check the container engine status
100 switch engineName
101 case 'docker'
102 [status, cmdout] = system([engineName ' info']);
103 cmdout = strtrim(cmdout);
104 if status == 1 || ~isempty(strfind(lower(cmdout), 'failed')) || ~isempty(strfind(lower(cmdout), 'error'))
105 errMsg = cmdout;
106 return
107 end
108 end
109 end
110
111
112 %% ===== GET AVAILABLE IMAGES =====
113 function [errMsg, imageList] = GetImages()
114 % USAGE: [errMsg, imageList] = bst_containers('GetImages')
115 imageList = cell(0,3); % [Name:Tag, ImageSHA, ManifestSHA]
116
117 % Check status of default container engine
118 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
119 if ~isempty(errMsg)
120 return
121 end
122
123 % List of available images
124 switch engineName
125 case 'docker'
126 [status, cmdout] = system('docker images --all --digests --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}} {{.Digest}}"');
127 if status == 0 && ~isempty(cmdout)
128 imageList = strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' ');
129 if mod(length(imageList), 3) ~= 0
130 errMsg = 'Error parsing Docker image list';
131 return
132 end
133 imageList = reshape(imageList, 3, [])';
134 elseif status ~= 0
135 errMsg = cmdout;
136 end
137 end
138 end
139
140
141 %% ===== IMPORT IMAGE =====
142 function [errMsg, imageSha] = ImportImage(imageSource, imageTag)
143 % Import container image into container engine, and create a tag
144 % USAGE: [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag])
145 imageSha = '';
146
147 if (nargin < 2) || isempty(imageTag)
148 imageTag = '';
149 end
150
151 % Check status of default container engine
152 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
153 if ~isempty(errMsg)
154 return
155 end
156
157 % Default: imageSource is an image reference
158 imageType = 'reference';
159 % If imageSource is a URL, download image file
160 if ~isempty(regexp(imageSource, '^http[s]*://', 'once'))
161 % Get tmp dir to bind container
162 tmpDir = bst_get('BrainstormTmpDir', 0, 'pull_image');
163 imageFile = bst_fullfile(tmpDir, 'image.tgz');
164 disp(['BST> Downloading URL : ' imageSource]);
165 disp(['BST> Saving to file : ' imageFile]);
166 errMsg = gui_brainstorm('DownloadFile', imageSource, imageFile, 'Download container image: ');
167 if ~isempty(errMsg)
168 errMsg = ['Impossible to download container image automatically:' 10 errMsg];
169 return
170 end
171 imageSource = imageFile;
172 imageType = 'file';
173 end
174
175 % Get current available images
176 if ~isempty(imageTag)
177 [errMsg, imageListOld] = GetImages();
178 if ~isempty(errMsg)
179 return
180 end
181 end
182
183 % Import image
184 switch engineName
185 case 'docker'
186 manifestSha = '';
187 switch imageType
188 case 'reference'
189 [status, cmdout] = system(['docker pull ' imageSource]);
190 if status == 0
191 % If new or existent image, returned SHA256 corresponds to:
192 % Manifest list or Image manifest
193 manifestSha = regexp(cmdout, 'sha256:[a-f0-9]+', 'match', 'once');
194 end
195
196 case 'file'
197 [status, cmdout] = system(['docker load --input ' imageSource]);
198 if status == 0
199 % If new or existent image, Image name (or SHA256 for nameless image) is returned in output
200 token = regexp(cmdout, '[a-z0-9._-]+:[a-zA-Z0-9._-]+', 'match', 'once');
201 parts = strsplit(token, ':');
202 if strcmp(parts{1}, 'sha256') && ~isempty(regexp(parts{2}, '^[a-f0-9]+$', 'once'))
203 manifestSha = token;
204 else
205 [~, imageListNew] = GetImages();
206 manifestSha = imageListNew{strcmpi(imageListNew(:,1), token), 3};
207 end
208 end
209 end
210 if status ~= 0
211 errMsg = cmdout;
212 return
213 end
214 % Get image SHA from its Manifest list or Image manifest
215 [~, imageListNew] = GetImages();
216 iImage = find(strcmpi(imageListNew(:,3), manifestSha));
217 if ~isempty(iImage)
218 imageSha = imageListNew{iImage,2};
219 else
220 imageSha = manifestSha;
221 end
222
223 % Tag image
224 if status == 0 && ~isempty(imageTag)
225 % Find newly added image by its Image ID
226 iOld = find(strcmpi(imageListOld(:,2), imageSha));
227 iNew = find(strcmpi(imageListNew(:,2), imageSha));
228 % Tag image
229 [status, cmdout] = system(['docker tag ', imageSha, ' ', imageTag]);
230 % Keep only the tag image IF the image was added in this call to ImportImage()
231 if status == 0 && (length(iNew) - length(iOld)) == 1
232 if ~isempty(imageListOld)
233 [imageDel, iNew] = setdiff(imageListNew(iNew, 1), imageListOld(iOld, 1));
234 else
235 imageDel = imageListNew{iNew, 1};
236 end
237 if ~strcmpi(imageDel, '<none>:<none>')
238 [status, cmdout] = system(['docker rmi ', imageListNew{iNew, 1}]);
239 end
240 end
241 end
242 if status ~= 0
243 errMsg = cmdout;
244 return
245 end
246 end
247 end
248
249
250 %% ===== RUN CONTAINER AS DAEMON =====
251 function errMsg = RunContainer(containerName, imageSha, volumes, isDaemon, containerArgs)
252 % USAGE: errMsg = bst_containers('RunContainer', containerName, imageSha, volumes, isDaemon, containerArgs)
253 % Validate inputs
254 if nargin < 5 || isempty(containerArgs)
255 containerArgs = '';
256 end
257 if nargin < 4 || isempty(isDaemon)
258 isDaemon = 0;
259 end
260 if nargin < 3 || ~iscell(volumes) || size(volumes,2) ~=2
261 volumes = [];
262 end
263
264 % Check status of default container engine
265 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
266 if ~isempty(errMsg)
267 return
268 end
269
270 % Create volumes pairs
271 volumesStr = '';
272 if ~isempty(volumes)
273 nPairs = size(volumes, 1);
274 pairs = cell(nPairs, 1);
275 for iPair = 1 : nPairs
276 pairs{iPair} = ['-v ' volumes{iPair, 1} ':' volumes{iPair, 2}];
277 end
278 volumesStr = strjoin(pairs, ' ');
279 end
280
281 % Use GPU with container engine
282 gpuStr = '';
283 if bst_get('ContainerUseGpu') && system('which nvidia-smi') == 0
284 gpuStr = '--gpus all';
285 end
286
287 % Run container
288 switch engineName
289 case 'docker'
290 if ~isDaemon
291 % Run ENTRYPOINT
292 cmdStr = sprintf('docker run --rm --name %s %s %s %s %s', containerName, gpuStr, volumesStr, imageSha, containerArgs);
293 else
294 % Replace ENTRYPOINT (if any) with `sleep infinity`
295 cmdStr = sprintf('docker run -d --name %s %s %s --entrypoint sleep %s infinity', containerName, gpuStr, volumesStr, imageSha);
296 end
297 [status, cmdout] = system(cmdStr);
298 end
299 if status ~= 0
300 errMsg = cmdout;
301 end
302 end
303
304
305 %% ===== EXECUTE COMMAND IN CONTAINER =====
306 function [errMsg, cmdout] = ExecInContainer(containerName, cmdStr)
307 cmdout = '';
308
309 % Check status of default container engine
310 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
311 if ~isempty(errMsg)
312 return
313 end
314 % Check if container is running
315 [errMsg, containerInfo] = GetContainerInfo(containerName);
316 if ~isempty(errMsg) || ~containerInfo.isRunning
317 return
318 end
319
320 % Flag to track interruption
321 processState = containers.Map({'isInterruptCleanup'}, {1});
322 % Clean up on function end, errors or Ctrl+C is pressed
323 cleanupObj = onCleanup(@() ProcessInterrupted(containerName, processState));
324 % Run command
325 switch engineName
326 case 'docker'
327 if ispc
328 commandWrapper = '"'; % Double quote
329 else
330 commandWrapper = ''''; % Single quote
331 end
332 % Execute the running container
333 commandExec = ['docker exec ' containerName ' sh -c ' commandWrapper cmdStr commandWrapper];
334 [status, cmdout] = system(commandExec, '-echo');
335 if status ~= 0
336 errMsg = strtrim(cmdout);
337 end
338 end
339 % Code in container ended normally
340 processState('isInterruptCleanup') = 0;
341 end
342
343
344 %% ===== CHECK CONTAINER STATUS =====
345 function [errMsg, containerInfo] = GetContainerInfo(containerName)
346 % [containerNameOut, isRunning, volumePairs, imageSha]
347 containerInfo = struct();
348 containerInfo.name = '';
349 containerInfo.isRunning = 0;
350 containerInfo.volumes = [];
351 containerInfo.imageSha = '';
352
353 % Check status of default container engine
354 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
355 if ~isempty(errMsg)
356 return
357 end
358
359 % Search for existent container with the same name and image reference
360 switch engineName
361 case 'docker'
362 % Find containers with same name
363 [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']);
364 if status ~= 0
365 errMsg = strtrim(cmdout);
366 return
367 end
368 containerInfo.name = strrep(strtrim(cmdout), '/', '');
369 [status, cmdout] = system(['docker inspect ' containerName ' --format "'...
370 '{{.State.Status}} # {{.HostConfig.Binds}} # {{.Image}}"']);
371 if status ~= 0
372 errMsg = strtrim(cmdout);
373 return
374 end
375 cmdout = strsplit(strtrim(cmdout), '#');
376 containerInfo.isRunning = strcmpi('running', strtrim(cmdout{1}));
377 volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', '');
378 volumes = regexprep(volumes, ':\', ';\');
379 volumePairs = strsplit(volumes, ':');
380 volumePairs = cellfun(@(x) regexprep(x, ';\', ':\'), volumePairs, 'UniformOutput', 0);
381 containerInfo.volumes = reshape(volumePairs, 2, [])';
382 tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match');
383 containerInfo.imageSha = strtrim(tokens{1});
384 end
385 end
386
387
388 %% ===== STOP CONTAINER =====
389 function errMsg = StopContainer(containerName, isForce)
390 % Validate inputs
391 if nargin < 2 || isempty(isForce)
392 isForce = 0;
393 end
394
395 % Check status of default container engine
396 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
397 if ~isempty(errMsg)
398 return
399 end
400
401 % Stop container
402 switch engineName
403 case 'docker'
404 if ~isForce
405 % Stop and remove
406 [status, cmdout] = system(['docker stop ' containerName ' && docker rm ' containerName]);
407 else
408 % Kill
409 [status, cmdout] = system(['docker rm -f ' containerName]);
410 end
411 if status ~=0
412 errMsg = strtrim(cmdout);
413 end
414 end
415 end
416
417
418 %% ===== REMOVE IMAGE =====
419 function errMsg = RemoveImage(imageSha, isForce)
420 % Validate inputs
421 if nargin < 2 || isempty(isForce)
422 isForce = 0;
423 end
424
425 % Check status of default container engine
426 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
427 if ~isempty(errMsg)
428 return
429 end
430
431 % Remove image
432 switch engineName
433 case 'docker'
434 if ~isForce
435 % Remove image
436 [status, cmdout] = system(['docker rmi ' imageSha]);
437 else
438 % Force remove image
439 [status, cmdout] = system(['docker rmi -f ' imageSha]);
440 end
441 if status ~=0
442 errMsg = strtrim(cmdout);
443 end
444 end
445 end
446
447
448 %% ===== PROCESS INTERRUPTED =====
449 function ProcessInterrupted(containerName, processState)
450 if processState('isInterruptCleanup')
451 bst_plugin('Unload', regexprep(containerName, '^bst_', ''));
452 bst_error('The process running in the container was interrupted', 'Container', 0);
453 end
454 end
455
456
457 %% ===== GET ONLINE MANIFEST DIGEST =====
458 function [errMsg, manifestSha] = GetOnlineManifest(imageSource)
459 manifestSha = '';
460
461 % Check status of default container engine
462 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
463 if ~isempty(errMsg)
464 return
465 end
466
467 % Get Manifest list or Image manifest
468 switch engineName
469 case 'docker'
470 [status, cmdout] = system(['docker buildx imagetools inspect ' imageSource ' --format "{{.Manifest}}"']);
471 if status == 0
472 % Digest
473 manifestSha = regexp(cmdout, 'sha256:[a-f0-9]+', 'match', 'once');
474 end
475 end
476 if status ~= 0
477 errMsg = strtrim(cmdout);
478 return
479 end
480 end
481
For additional help, please consult the Brainstorm Forum