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. bstContainer.jpg

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:

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

Tutorials/Containers (last edited 2026-04-29 20:57:37 by RaymundoCassani)