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]) 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

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