├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── description.yml ├── Dockerfile ├── README.md ├── conf ├── monit-services └── xo-server.toml.j2 ├── docker-compose.yml ├── healthcheck.sh └── run.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | *.j2 linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Update GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | schedule: 5 | - cron: "00 3 * * 6" 6 | push: 7 | paths-ignore: 8 | - 'README.md' 9 | - 'docker-compose.yml' 10 | - '.github/**' 11 | branches: 12 | - 'master' 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build-and-push: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up QEMU-action 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Docker buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Dockerhub login 29 | uses: docker/login-action@v3 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | 34 | - name: Get xo-server version 35 | id: xo-version 36 | run: | 37 | echo "XO_VERSION=$(curl -s https://raw.githubusercontent.com/vatesfr/xen-orchestra/master/packages/xo-server/package.json | jq -r .version)" >> $GITHUB_ENV 38 | 39 | - name: Build image and push 40 | uses: docker/build-push-action@v6 41 | with: 42 | context: . 43 | platforms: linux/amd64 44 | push: true 45 | tags: | 46 | ronivay/xen-orchestra:latest 47 | ronivay/xen-orchestra:${{ env.XO_VERSION }} 48 | -------------------------------------------------------------------------------- /.github/workflows/description.yml: -------------------------------------------------------------------------------- 1 | name: dockerhub-description 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'README.md' 7 | - '.github/workflows/description.yml' 8 | branches: 9 | - 'master' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | dockerhubdescription: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Update Docker Hub description 19 | uses: peter-evans/dockerhub-description@v4 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 23 | repository: ronivay/xen-orchestra 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # builder container 2 | FROM node:20-bullseye as build 3 | 4 | # Install set of dependencies to support building Xen Orchestra 5 | RUN apt update && \ 6 | apt install -y build-essential python3-minimal libpng-dev ca-certificates git fuse 7 | 8 | # Fetch Xen-Orchestra sources from git stable branch 9 | RUN git clone -b master https://github.com/vatesfr/xen-orchestra /etc/xen-orchestra 10 | 11 | # Run build tasks against sources 12 | # Docker buildx QEMU arm64 emulation is slow, so we set timeout for yarn 13 | WORKDIR /etc/xen-orchestra 14 | RUN yarn config set network-timeout 200000 && yarn && yarn build 15 | 16 | # Builds the v6 webui 17 | RUN yarn run turbo run build --filter @xen-orchestra/web 18 | 19 | # Install plugins 20 | RUN find /etc/xen-orchestra/packages/ -maxdepth 1 -mindepth 1 -not -name "xo-server" -not -name "xo-web" -not -name "xo-server-cloud" -not -name "xo-server-test" -not -name "xo-server-test-plugin" -exec ln -s {} /etc/xen-orchestra/packages/xo-server/node_modules \; 21 | 22 | # Runner container 23 | FROM node:20-bullseye-slim 24 | 25 | LABEL org.opencontainers.image.authors="Roni Väyrynen " 26 | 27 | # Install set of dependencies for running Xen Orchestra 28 | RUN apt update && \ 29 | apt install -y redis-server libvhdi-utils python3-minimal python3-jinja2 lvm2 nfs-common netbase cifs-utils ca-certificates monit procps curl ntfs-3g 30 | 31 | # Install forever for starting/stopping Xen-Orchestra 32 | RUN npm install forever -g 33 | 34 | # Copy built xen orchestra from builder 35 | COPY --from=build /etc/xen-orchestra /etc/xen-orchestra 36 | 37 | # Logging 38 | RUN ln -sf /proc/1/fd/1 /var/log/redis/redis-server.log && \ 39 | ln -sf /proc/1/fd/1 /var/log/xo-server.log && \ 40 | ln -sf /proc/1/fd/1 /var/log/monit.log 41 | 42 | # Healthcheck 43 | ADD healthcheck.sh /healthcheck.sh 44 | RUN chmod +x /healthcheck.sh 45 | HEALTHCHECK --start-period=1m --interval=30s --timeout=5s --retries=2 CMD /healthcheck.sh 46 | 47 | # Copy xo-server configuration template 48 | ADD conf/xo-server.toml.j2 /xo-server.toml.j2 49 | 50 | # Copy monit configuration 51 | ADD conf/monit-services /etc/monit/conf.d/services 52 | 53 | # Copy startup script 54 | ADD run.sh /run.sh 55 | RUN chmod +x /run.sh 56 | 57 | WORKDIR /etc/xen-orchestra/packages/xo-server 58 | 59 | EXPOSE 80 60 | 61 | CMD ["/run.sh"] 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xen-Orchestra docker container 2 | 3 | [![image pulls](https://img.shields.io/docker/pulls/ronivay/xen-orchestra.svg)](https://hub.docker.com/r/ronivay/xen-orchestra) [![image size (tag)](https://img.shields.io/docker/image-size/ronivay/xen-orchestra/latest)](https://hub.docker.com/r/ronivay/xen-orchestra) 4 | 5 | [![](https://github.com/ronivay/xen-orchestra-docker/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/ronivay/xen-orchestra-docker/actions?query=workflow%3Abuild) 6 | 7 | This repository contains files to build Xen-Orchestra community edition docker container with all features and plugins installed 8 | 9 | Latest tag is weekly build from xen orchestra sources master branch. Images are also tagged based on xo-server version. 10 | 11 | Xen-Orchestra is a Web-UI for managing your existing XenServer infrastructure. 12 | 13 | https://xen-orchestra.com/ 14 | 15 | Xen-Orchestra offers supported version of their product in an appliance (not running docker though), which i highly recommend if you are working with larger infrastructure. 16 | 17 | #### Installation 18 | 19 | - Clone this repository 20 | ``` 21 | git clone https://github.com/ronivay/xen-orchestra-docker 22 | ``` 23 | 24 | - build docker container manually 25 | 26 | ``` 27 | docker build -t xen-orchestra . 28 | ``` 29 | 30 | - or pull from dockerhub 31 | 32 | ``` 33 | docker pull ronivay/xen-orchestra 34 | ``` 35 | 36 | - run it with defaults values for testing purposes. 37 | 38 | ``` 39 | docker run -itd -p 80:80 ronivay/xen-orchestra 40 | ``` 41 | 42 | Xen-Orchestra is now accessible at http://your-ip-address. Default credentials admin@admin.net/admin 43 | 44 | - Other than testing, suggested method is to mount data paths from your host to preserve data 45 | 46 | ``` 47 | docker run -itd -p 80:80 -v /path/to/data/xo-server:/var/lib/xo-server -v /path/to/data/redis:/var/lib/redis ronivay/xen-orchestra 48 | ``` 49 | 50 | I also suggest adding --stop-timeout since there are multiple services inside single container and we want them to shutdown gracefully when container is stopped. 51 | Default timeout is 10 seconds which can be too short. 52 | 53 | In recent versions docker containers run without privileges (root) or with reduced privileges. 54 | In those case XenOrchestra will not be able to mount nfs/smb shares for Remotes from within docker. 55 | To fix that you will have to run docker with privileges: `--cap-add sys_admin --cap-add dac_read_search` option or `--priviledged` for all privileges. 56 | In case your system is also using an application security framework AppArmor or SELinux you will need to take additional steps. 57 | 58 | For AppArmor you will have to add also `--security-opt apparmor:unconfined`. 59 | 60 | Below is an example command for running the app in a docker container with: 61 | 62 | * automatic container start on boot / crash 63 | * enogh capabilities to mount nfs shares 64 | * enough time to allow for proper service shutdown 65 | 66 | ``` 67 | docker run -itd \ 68 | --stop-timeout 60 \ 69 | --restart unless-stopped \ 70 | --cap-add sys_admin \ 71 | --cap-add dac_read_search \ 72 | --security-opt apparmor:unconfined \ 73 | -p 80:80 \ 74 | -v /path/to/data/xo-server:/var/lib/xo-server \ 75 | -v /path/to/data/redis:/var/lib/redis \ 76 | ronivay/xen-orchestra 77 | 78 | ``` 79 | 80 | You may also use docker-compose. Copy configuration from below of example docker-compose.yml from github repository 81 | 82 | ``` 83 | version: '3' 84 | services: 85 | xen-orchestra: 86 | restart: unless-stopped 87 | image: ronivay/xen-orchestra:latest 88 | container_name: xen-orchestra 89 | stop_grace_period: 1m 90 | ports: 91 | - "80:80" 92 | #- "443:443" 93 | environment: 94 | - HTTP_PORT=80 95 | #- HTTPS_PORT=443 96 | 97 | #redirect takes effect only if HTTPS_PORT is defined 98 | #- REDIRECT_TO_HTTPS=true 99 | 100 | #if HTTPS_PORT is defined and CERT/KEY paths are empty, a self-signed certificate will be generated 101 | #- CERT_PATH='/cert.pem' 102 | #- KEY_PATH='/cert.key' 103 | # capabilities are needed for NFS/SMB mount 104 | cap_add: 105 | - SYS_ADMIN 106 | - DAC_READ_SEARCH 107 | # additional setting required for apparmor enabled systems. also needed for NFS mount 108 | security_opt: 109 | - apparmor:unconfined 110 | volumes: 111 | - xo-data:/var/lib/xo-server 112 | - redis-data:/var/lib/redis 113 | # mount certificate files to container if HTTPS is set with cert/key paths 114 | #- /path/to/cert.pem:/cert.pem 115 | #- /path/to/cert.key:/cert.key 116 | # mount your custom CA to container if host certificates are issued by it and you want XO to trust it 117 | #- /path/to/ca.pem:/host-ca.pem 118 | # logging 119 | logging: &default_logging 120 | driver: "json-file" 121 | options: 122 | max-size: "1M" 123 | max-file: "2" 124 | # these are needed for file restore. allows one backup to be mounted at once which will be umounted after some minutes if not used (prevents other backups to be mounted during that) 125 | # add loop devices (loop1, loop2 etc) if multiple simultaneous mounts needed. 126 | #devices: 127 | # - "/dev/fuse:/dev/fuse" 128 | # - "/dev/loop-control:/dev/loop-control" 129 | # - "/dev/loop0:/dev/loop0" 130 | 131 | volumes: 132 | xo-data: 133 | redis-data: 134 | ``` 135 | 136 | #### Variables 137 | 138 | `HTTP_PORT` 139 | 140 | Listening HTTP port inside container 141 | 142 | `HTTPS_PORT` 143 | 144 | Listening HTTPS port inside container 145 | 146 | `REDIRECT_TO_HTTPS` 147 | 148 | Boolean value true/false. If set to true, will redirect any HTTP traffic to HTTPS. Requires that HTTPS_PORT is set. Defaults to: false 149 | 150 | `CERT_PATH` 151 | 152 | Path inside container for user specified PEM certificate file. Example: '/path/to/cert' 153 | Note: single quotes are part of the value and mandatory! 154 | 155 | If HTTPS_PORT is set and CERT_PATH not given, a self-signed certificate and key will be generated automatically. 156 | 157 | `KEY_PATH` 158 | 159 | Path inside container for user specified key file. Example: '/path/to/key' 160 | Note: single quotes are part of the value and mandatory! 161 | 162 | if HTTPS_PORT is set and KEY_PATH not given, a self-signed certificate and key will be generated automatically. 163 | 164 | #### Configuration 165 | 166 | xo-server configuration inside container is generated only once based on variables if config file is missing. 167 | 168 | If you wish to customize the xo-server configuration file manually. Mount some directory to `/etc/xo-server` path inside container, eq: 169 | 170 | ``` 171 | docker run -itd -p 80:80 -v /path/to/xo-config:/etc/xo-server ronivay/xen-orchestra 172 | ``` 173 | 174 | Once container has started for the first time, you'll now have a configuration file at `/path/to/xo-config/config.toml` which you can edit. 175 | Restarting container will apply the modified configuration. 176 | -------------------------------------------------------------------------------- /conf/monit-services: -------------------------------------------------------------------------------- 1 | set httpd port 2812 and 2 | use address localhost 3 | allow localhost 4 | 5 | check process xo-server with pidfile /var/run/xo-server.pid 6 | depends on redis 7 | start program = "/usr/bin/env NODE_EXTRA_CA_CERTS=/host-ca.pem /usr/local/bin/forever start -a --pidFile /var/run/xo-server.pid --sourceDir /etc/xen-orchestra/packages/xo-server -l /var/log/xo-server.log dist/cli.mjs" 8 | stop program = "/usr/local/bin/forever stop /etc/xen-orchestra/packages/xo-server/dist/cli.mjs" 9 | 10 | check process redis with pidfile /var/run/redis.pid 11 | start program = "/usr/bin/redis-server /etc/redis/redis.conf --bind 127.0.0.1 --pidfile /var/run/redis.pid" 12 | stop program = "/usr/bin/redis-cli shutdown" 13 | 14 | check process rpcbind matching "rpcbind" 15 | start program = "/sbin/rpcbind" 16 | -------------------------------------------------------------------------------- /conf/xo-server.toml.j2: -------------------------------------------------------------------------------- 1 | # Example XO-Server configuration. 2 | # 3 | # This file is automatically looking for at the following places: 4 | # - `$HOME/.config/xo-server/config.toml` 5 | # - `/etc/xo-server/config.toml` 6 | # 7 | # The first entries have priority. 8 | # 9 | # Note: paths are relative to the configuration file. 10 | 11 | #===================================================================== 12 | 13 | # HTTP proxy configuration used by xo-server to fetch resources on the Internet. 14 | # 15 | # See: https://github.com/TooTallNate/node-proxy-agent#maps-proxy-protocols-to-httpagent-implementations 16 | # httpProxy = 'http://jsmith:qwerty@proxy.lan:3128' 17 | 18 | #===================================================================== 19 | 20 | # It may be necessary to run XO-Server as a privileged user (e.g. `root`) for 21 | # instance to allow the HTTP server to listen on a 22 | # [privileged ports](http://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html). 23 | # 24 | # To avoid security issues, XO-Server can drop its privileges by changing the 25 | # user and the group is running with. 26 | # 27 | # Note: XO-Server will change them just after reading the configuration. 28 | 29 | # User to run XO-Server as. 30 | # 31 | # Note: The user can be specified using either its name or its numeric 32 | # identifier. 33 | # 34 | # Default: undefined 35 | #user = 'nobody' 36 | 37 | # Group to run XO-Server as. 38 | # 39 | # Note: The group can be specified using either its name or its numeric 40 | # identifier. 41 | # 42 | # Default: undefined 43 | # group = 'nogroup' 44 | 45 | #===================================================================== 46 | 47 | # Directory containing the database of XO. 48 | # Currently used for logs. 49 | # 50 | # Default: '/var/lib/xo-server/data' 51 | #datadir = '/var/lib/xo-server/data' 52 | 53 | #===================================================================== 54 | 55 | # Configuration of the embedded HTTP server. 56 | [http] 57 | # If set to true, all HTTP traffic will be redirected to the first HTTPs 58 | # configuration. 59 | # redirectToHttps = true 60 | {% if env['REDIRECT_TO_HTTPS'] == 'true' and env['HTTPS_PORT'] %} 61 | redirectToHttps = true 62 | {% endif %} 63 | 64 | 65 | # Settings applied to cookies created by xo-server's embedded HTTP server. 66 | # 67 | # See https://www.npmjs.com/package/cookie#options-1 68 | [http.cookies] 69 | #sameSite = true 70 | #secure = true 71 | 72 | # Basic HTTP. 73 | [[http.listen]] 74 | # Address on which the server is listening on. 75 | # 76 | # Sets it to 'localhost' for IP to listen only on the local host. 77 | # 78 | # Default: all IPv6 addresses if available, otherwise all IPv4 addresses. 79 | # hostname = 'localhost' 80 | 81 | # Port on which the server is listening on. 82 | # 83 | # Default: undefined 84 | port = {{ env['HTTP_PORT'] }} 85 | 86 | # Instead of `host` and `port` a path to a UNIX socket may be specified 87 | # (overrides `host` and `port`). 88 | # 89 | # Default: undefined 90 | # socket = './http.sock' 91 | 92 | # # Basic HTTPS. 93 | # # 94 | # # You can find the list of possible options there 95 | # # https://nodejs.org/docs/latest/api/tls.html#tls.createServer 96 | # # 97 | # # The only difference is the presence of the certificate and the key. 98 | 99 | {% if env['HTTPS_PORT'] %} 100 | [[http.listen]] 101 | port = {{ env['HTTPS_PORT'] }} 102 | 103 | autoCert = true 104 | cert = {{ env['CERT_PATH'] }} 105 | key = {{ env['KEY_PATH'] }} 106 | {% endif %} 107 | 108 | # List of files/directories which will be served. 109 | [http.mounts] 110 | #'/any/url' = '/path/to/directory' 111 | 112 | # List of proxied URLs (HTTP & WebSockets). 113 | [http.proxies] 114 | #'/any/url' = 'http://localhost:54722' 115 | 116 | #===================================================================== 117 | 118 | # Connection to the Redis server. 119 | [redis] 120 | # Unix sockets can be used 121 | # 122 | # Default: undefined 123 | #socket = '/var/run/redis/redis.sock' 124 | 125 | # Syntax: redis://[db[:password]@]hostname[:port][/db-number] 126 | # 127 | # Default: redis://localhost:6379/0 128 | #uri = 'redis://redis.company.lan/42' 129 | uri = 'redis://127.0.0.1:6379/0' 130 | 131 | # List of aliased commands. 132 | # 133 | # See http://redis.io/topics/security#disabling-of-specific-commands 134 | #renameCommands: 135 | # del = '3dda29ad-3015-44f9-b13b-fa570de92489' 136 | # srem = '3fd758c9-5610-4e9d-a058-dbf4cb6d8bf0' 137 | 138 | #===================================================================== 139 | 140 | # Configuration for remotes 141 | [remoteOptions] 142 | # Directory used to mount remotes 143 | # 144 | # Default: '/run/xo-server/mounts' 145 | #mountsDir = '/run/xo-server/mounts' 146 | 147 | # Use sudo for mount with non-root user 148 | # 149 | # Default: false 150 | #useSudo = false 151 | 152 | #===================================================================== 153 | 154 | # Configuration for plugins 155 | [plugins] 156 | # Each configuration is passed to the dedicated plugin instance 157 | # 158 | # Syntax: [plugins.] 159 | 160 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | xen-orchestra: 4 | restart: unless-stopped 5 | image: ronivay/xen-orchestra:latest 6 | container_name: xen-orchestra 7 | stop_grace_period: 1m 8 | ports: 9 | - "80:80" 10 | #- "443:443" 11 | environment: 12 | - HTTP_PORT=80 13 | #- HTTPS_PORT=443 14 | 15 | #redirect takes effect only if HTTPS_PORT is defined 16 | #- REDIRECT_TO_HTTPS=true 17 | 18 | #if HTTPS_PORT is defined and CERT/KEY paths are empty, a self-signed certificate will be generated 19 | #- CERT_PATH='/cert.pem' 20 | #- KEY_PATH='/cert.key' 21 | # capabilities are needed for NFS mount 22 | cap_add: 23 | - SYS_ADMIN 24 | # additional setting required for apparmor enabled systems. also needed for NFS mount 25 | security_opt: 26 | - apparmor:unconfined 27 | volumes: 28 | - xo-data:/var/lib/xo-server 29 | - redis-data:/var/lib/redis 30 | # to preserve xo-server config on host dir after generated by container 31 | #- /path/to/config:/etc/xo-server 32 | # mount certificate files to container if HTTPS is set with cert/key paths 33 | #- /path/to/cert.pem:/cert.pem 34 | #- /path/to/cert.key:/cert.key 35 | # mount your custom CA to container if host certificates are issued by it and you want XO to trust it 36 | #- /path/to/ca.pem:/host-ca.pem 37 | # logging 38 | logging: &default_logging 39 | driver: "json-file" 40 | options: 41 | max-size: "1M" 42 | max-file: "2" 43 | # these are needed for file restore. allows one backup to be mounted at once which will be umounted after some minutes if not used (prevents other backups to be mounted during that) 44 | # add loop devices (loop1, loop2 etc) if multiple simultaneous mounts needed. 45 | #devices: 46 | # - "/dev/fuse:/dev/fuse" 47 | # - "/dev/loop-control:/dev/loop-control" 48 | # - "/dev/loop0:/dev/loop0" 49 | 50 | volumes: 51 | xo-data: 52 | redis-data: 53 | -------------------------------------------------------------------------------- /healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/curl -s -k -L -I -m 3 http://127.0.0.1:${HTTP_PORT} >/dev/null 4 | 5 | if [[ "$?" == "0" ]]; then 6 | webcheck_retval="0" 7 | else 8 | webcheck_retval="1" 9 | fi 10 | 11 | /usr/bin/pgrep redis-server >/dev/null 12 | 13 | if [[ "$?" == "0" ]]; then 14 | redis_retval="0" 15 | else 16 | redis_retval="1" 17 | fi 18 | 19 | /usr/bin/pgrep rpcbind >/dev/null 20 | 21 | if [[ "$?" == "0" ]]; then 22 | rpcbind_retval="0" 23 | else 24 | rpcbind_retval="1" 25 | fi 26 | 27 | if [[ "$webcheck_retval" == "1" ]] || [[ "$redis_retval" == "1" ]] || [[ "$rpcbind_retval" == "1" ]]; then 28 | exit 1 29 | fi 30 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function StopProcesses { 4 | 5 | while [ $(/usr/bin/monit status | sed -n '/^Process/{n;p;}' | awk '{print $2}' | grep -c OK) != 0 ] ; do 6 | sleep 1 7 | /usr/bin/monit stop all 8 | done 9 | 10 | exit 0 11 | } 12 | 13 | 14 | if [[ ! -f /etc/xo-server/config.toml ]]; then 15 | # generate configuration 16 | set -a 17 | 18 | [[ ! -d /etc/xo-server ]] && mkdir /etc/xo-server 19 | 20 | HTTP_PORT=${HTTP_PORT:-"80"} 21 | CERT_PATH=${CERT_PATH:-\'./temp-cert.pem\'} 22 | KEY_PATH=${KEY_PATH:-\'./temp-key.pem\'} 23 | 24 | /usr/bin/python3 -c 'import os 25 | import sys 26 | import jinja2 27 | sys.stdout.write( 28 | jinja2.Template(sys.stdin.read() 29 | ).render(env=os.environ))' /etc/xo-server/config.toml 30 | 31 | set +a 32 | # start services 33 | fi 34 | 35 | trap StopProcesses EXIT TERM 36 | 37 | /usr/bin/monit && /usr/bin/monit start all 38 | 39 | while true 40 | do 41 | sleep 1d 42 | done & 43 | 44 | wait $! 45 | --------------------------------------------------------------------------------