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])
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,2); % [Name:Tag, SHA]
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 --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"');
127 if status == 0 && ~isempty(cmdout)
128 imageList = strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' ');
129 if mod(length(imageList), 2) ~= 0
130 errMsg = 'Error parsing Docker image list';
131 return
132 end
133 imageList = reshape(imageList, 2, [])';
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 switch imageType
187 case 'reference'
188 [status, cmdout] = system(['docker pull ' imageSource]);
189 if status == 0
190 % If new or existent image, SHA256 is returned in output
191 imageSha = regexp(cmdout, 'sha256:[a-f0-9]+', 'match', 'once');
192 end
193
194 case 'file'
195 [status, cmdout] = system(['docker load --input ' imageSource]);
196 if status == 0
197 % If new or existent image, Image name (or SHA256 for nameless image) is returned in output
198 token = regexp(cmdout, '[a-z0-9._-]+:[a-zA-Z0-9._-]+', 'match', 'once');
199 parts = strsplit(token, ':');
200 if strcmp(parts{1}, 'sha256') && ~isempty(regexp(parts{2}, '^[a-f0-9]+$', 'once'))
201 imageSha = token;
202 else
203 [~, imageListNew] = GetImages();
204 imageSha = imageListNew{strcmpi(imageListNew(:,1), token), 2};
205 end
206 end
207 end
208 % Tag image
209 if status == 0 && ~isempty(imageTag)
210 % Compare images before and after import
211 [~, imageListNew] = GetImages();
212 iOld = find(strcmpi(imageListOld(:,2), imageSha));
213 iNew = find(strcmpi(imageListNew(:,2), imageSha));
214 % Tag image
215 [status, cmdout] = system(['docker tag ', imageSha, ' ', imageTag]);
216 % Keep only the tag image IF the image was added in this call to ImportImage()
217 if status == 0 && (length(iNew) - length(iOld)) == 1
218 if ~isempty(imageListOld)
219 [imageDel, iNew] = setdiff(imageListNew(iNew, 1), imageListOld(iOld, 1));
220 else
221 imageDel = imageListNew{iNew, 1};
222 end
223 if ~strcmpi(imageDel, '<none>:<none>')
224 [status, cmdout] = system(['docker rmi ', imageListNew{iNew, 1}]);
225 end
226 end
227 end
228 if status ~= 0
229 errMsg = cmdout;
230 return
231 end
232 end
233 end
234
235
236 %% ===== RUN CONTAINER AS DAEMON =====
237 function errMsg = RunContainer(containerName, imageSha, volumes, isDaemon)
238 % USAGE: errMsg = bst_containers('RunContainer', containerName, imageSha, volumes, isDaemon)
239 % Validate inputs
240 if nargin < 4 || isempty(isDaemon)
241 isDaemon = 0;
242 end
243 if nargin < 3 || ~iscell(volumes) || size(volumes,2) ~=2
244 volumes = [];
245 end
246
247 % Check status of default container engine
248 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
249 if ~isempty(errMsg)
250 return
251 end
252
253 % Create volumes pairs
254 volumesStr = '';
255 if ~isempty(volumes)
256 nPairs = size(volumes, 1);
257 pairs = cell(nPairs, 1);
258 for iPair = 1 : nPairs
259 pairs{iPair} = ['-v ' volumes{iPair, 1} ':' volumes{iPair, 2}];
260 end
261 volumesStr = strjoin(pairs, ' ');
262 end
263
264 % Run container
265 switch engineName
266 case 'docker'
267 if ~isDaemon
268 % Run ENTRYPOINT
269 cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageSha);
270 else
271 % Replace ENTRYPOINT (if any) with `sleep infinity`
272 cmdStr = sprintf('docker run -d --name %s %s --entrypoint sleep %s infinity', containerName, volumesStr, imageSha);
273 end
274 [status, cmdout] = system(cmdStr);
275 end
276 if status ~= 0
277 errMsg = cmdout;
278 end
279 end
280
281
282 %% ===== EXECUTE COMMAND IN CONTAINER =====
283 function [errMsg, cmdout] = ExecInContainer(containerName, cmdStr)
284 cmdout = '';
285
286 % Check status of default container engine
287 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
288 if ~isempty(errMsg)
289 return
290 end
291 % Check if container is running
292 [errMsg, containerInfo] = GetContainerInfo(containerName);
293 if ~isempty(errMsg) || ~containerInfo.isRunning
294 return
295 end
296
297 % Run command
298 switch engineName
299 case 'docker'
300 if ispc
301 commandWrapper = '"'; % Double quote
302 else
303 commandWrapper = ''''; % Single quote
304 end
305 [status, cmdout] = system(['docker exec ' containerName ' sh -c ' commandWrapper cmdStr commandWrapper]);
306 if status ~= 0
307 errMsg = strtrim(cmdout);
308 end
309 end
310 end
311
312
313 %% ===== CHECK CONTAINER STATUS =====
314 function [errMsg, containerInfo] = GetContainerInfo(containerName)
315 % [containerNameOut, isRunning, volumePairs, imageSha]
316 containerInfo = struct();
317 containerInfo.name = '';
318 containerInfo.isRunning = 0;
319 containerInfo.volumes = [];
320 containerInfo.imageSha = '';
321
322 % Check status of default container engine
323 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
324 if ~isempty(errMsg)
325 return
326 end
327
328 % Search for existent container with the same name and image reference
329 switch engineName
330 case 'docker'
331 % Find containers with same name
332 [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']);
333 if status ~= 0
334 errMsg = strtrim(cmdout);
335 return
336 end
337 containerInfo.name = strrep(strtrim(cmdout), '/', '');
338 [status, cmdout] = system(['docker inspect ' containerName ' --format "'...
339 '{{.State.Status}} # {{.HostConfig.Binds}} # {{.Image}}"']);
340 if status ~= 0
341 errMsg = strtrim(cmdout);
342 return
343 end
344 cmdout = strsplit(strtrim(cmdout), '#');
345 containerInfo.isRunning = strcmpi('running', strtrim(cmdout{1}));
346 volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', '');
347 volumes = regexprep(volumes, ':\', ';\');
348 volumePairs = strsplit(volumes, ':');
349 volumePairs = cellfun(@(x) regexprep(x, ';\', ':\'), volumePairs, 'UniformOutput', 0);
350 containerInfo.volumes = reshape(volumePairs, 2, [])';
351 tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match');
352 containerInfo.imageSha = strtrim(tokens{1});
353 end
354 end
355
356
357 %% ===== STOP CONTAINER =====
358 function errMsg = StopContainer(containerName, isForce)
359 % Validate inputs
360 if nargin < 2 || isempty(isForce)
361 isForce = 0;
362 end
363
364 % Check status of default container engine
365 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
366 if ~isempty(errMsg)
367 return
368 end
369
370 % Stop container
371 switch engineName
372 case 'docker'
373 if ~isForce
374 % Stop and remove
375 [status, cmdout] = system(['docker stop ' containerName ' && docker rm ' containerName]);
376 else
377 % Kill
378 [status, cmdout] = system(['docker rm -f ' containerName]);
379 end
380 if status ~=0
381 errMsg = strtrim(cmdout);
382 end
383 end
384 end
385
386
387 %% ===== REMOVE IMAGE =====
388 function errMsg = RemoveImage(imageSha, isForce)
389 % Validate inputs
390 if nargin < 2 || isempty(isForce)
391 isForce = 0;
392 end
393
394 % Check status of default container engine
395 [errMsg, engineName] = GetEngine(bst_get('ContainerEngine'));
396 if ~isempty(errMsg)
397 return
398 end
399
400 % Remove image
401 switch engineName
402 case 'docker'
403 if ~isForce
404 % Remove image
405 [status, cmdout] = system(['docker rmi ' imageSha]);
406 else
407 % Force remove image
408 [status, cmdout] = system(['docker rmi -f ' imageSha]);
409 end
410 if status ~=0
411 errMsg = strtrim(cmdout);
412 end
413 end
414 end
415
For additional help, please consult the Brainstorm Forum