├── .circleci ├── config.yml ├── test-connectivity.sh └── test-install.sh ├── .editorconfig ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── admin.sh ├── backup.sh ├── bitcart-cli.sh ├── build.sh ├── changedomain.sh ├── cleanup.sh ├── compose ├── Dockerfile-coin.template ├── admin-plugins.Dockerfile ├── backend-plugins.Dockerfile ├── backend.Dockerfile ├── bch.Dockerfile ├── bnb.Dockerfile ├── bsty.Dockerfile ├── btc.Dockerfile ├── coin-plugins.Dockerfile ├── eth.Dockerfile ├── grs.Dockerfile ├── ltc.Dockerfile ├── matic.Dockerfile ├── nginx.tmpl ├── pihole │ └── resolv.conf ├── plugins │ └── .gitkeep ├── sbch.Dockerfile ├── scripts │ ├── .jq-template.awk │ ├── cli-autocomplete.sh │ ├── docker-entrypoint.sh │ ├── generate-templates.sh │ ├── install-backend-plugins.sh │ ├── install-daemon-plugins.sh │ └── install-ui-plugins.sh ├── store-plugins.Dockerfile ├── torrc-relay.tmpl ├── torrc.tmpl ├── trx.Dockerfile ├── xmr.Dockerfile └── xrg.Dockerfile ├── contrib └── upgrades │ ├── README.md │ ├── upgrade-to-0500.sh │ ├── upgrade-to-0600.sh │ ├── upgrade-to-0610.sh │ ├── upgrade-to-0680.sh │ └── upgrade-to-0800.sh ├── dev-setup.sh ├── generator ├── .dockerignore ├── Dockerfile ├── __init__.py ├── __main__.py ├── constants.py ├── docker-components │ ├── admin.yml │ ├── backend.yml │ ├── binancecoin.yml │ ├── bitcoin.yml │ ├── bitcoincash.yml │ ├── cloudflare-tunnel.yml │ ├── ergon.yml │ ├── ethereum.yml │ ├── globalboost.yml │ ├── groestlcoin.yml │ ├── litecoin.yml │ ├── monero.yml │ ├── nginx-https.yml │ ├── nginx.yml │ ├── pihole.yml │ ├── polygon.yml │ ├── postgres.yml │ ├── redis.yml │ ├── smartbch.yml │ ├── store.yml │ ├── tor-relay.yml │ ├── tor.yml │ ├── tron.yml │ └── worker.yml ├── exceptions.py ├── generator.py ├── pyproject.toml ├── requirements.txt ├── rules │ ├── 01_no_image.py │ ├── 02_one_domain.py │ ├── 03_root_path.py │ ├── 04_no_reverseproxy.py │ ├── 05_frontend.py │ ├── 06_tor.py │ ├── 07_build_time_env.py │ ├── 08_scale.py │ ├── 09_custom_version.py │ ├── 10_local_rule.py │ └── __init__.py ├── settings.py ├── test-requirements.txt ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_core.py │ ├── test_rules.py │ ├── test_settings.py │ └── utils.py └── utils.py ├── helpers.sh ├── install-master.sh ├── load_env.sh ├── pyproject.toml ├── restart.sh ├── restore.sh ├── setup.sh ├── start.sh ├── stop.sh ├── update.sh └── uv.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | bitcart: bitcart/bitcart-shared@2 5 | 6 | jobs: 7 | test: 8 | executor: bitcart/docker-python 9 | 10 | working_directory: ~/repo 11 | 12 | steps: 13 | - checkout 14 | - run: 15 | name: install dependencies 16 | command: | 17 | curl -LsSf https://astral.sh/uv/install.sh | sh 18 | uv sync --frozen --compile-bytecode 19 | echo ". ~/repo/.venv/bin/activate" >> $BASH_ENV 20 | 21 | - run: 22 | name: run tests 23 | command: | 24 | make ci 25 | 26 | - bitcart/codecov: 27 | args: "" 28 | 29 | cansetup: 30 | executor: bitcart/build-ubuntu 31 | working_directory: ~/repo 32 | steps: 33 | - checkout 34 | - run: 35 | name: Test installation 36 | command: | 37 | cd .circleci 38 | sudo ./test-install.sh 39 | 40 | deploy: 41 | executor: bitcart/build-ubuntu 42 | 43 | environment: 44 | BUILD_PLATFORMS: linux/amd64,linux/arm64/v8 45 | 46 | working_directory: ~/repo 47 | 48 | steps: 49 | - checkout 50 | - bitcart/login-to-registries 51 | - bitcart/enable-buildx 52 | - run: 53 | name: build docker image 54 | command: | 55 | docker buildx build -f generator/Dockerfile --progress plain --push --platform ${BUILD_PLATFORMS} --tag bitcart/docker-compose-generator:latest \ 56 | --tag ghcr.io/bitcart/docker-compose-generator:latest . 57 | 58 | workflows: 59 | version: 2 60 | build: 61 | jobs: 62 | - bitcart/lint: 63 | name: lint 64 | executor: bitcart/build-ubuntu 65 | - test: 66 | requires: 67 | - lint 68 | - cansetup: 69 | requires: 70 | - test 71 | - deploy: 72 | context: global 73 | requires: 74 | - cansetup 75 | filters: 76 | branches: 77 | only: master 78 | -------------------------------------------------------------------------------- /.circleci/test-connectivity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Trying to connect to bitcart..." 4 | while true; do 5 | if [ "$(curl -sL -w "%{http_code}\\n" "http://localhost/api" -o /dev/null)" == "200" ]; then 6 | echo "Successfully contacted Bitcart" 7 | break 8 | fi 9 | sleep 1 10 | done 11 | -------------------------------------------------------------------------------- /.circleci/test-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # TODO: test that docker itself is installed without issues automatically (circleci issue with docker preinstalled) 6 | 7 | cd ../.. 8 | 9 | [ -d bitcart-docker ] || mv repo bitcart-docker 10 | 11 | cd bitcart-docker 12 | 13 | export BITCART_HOST=bitcart.local 14 | export REVERSEPROXY_DEFAULT_HOST=bitcart.local 15 | export BITCART_CRYPTOS=btc,ltc 16 | export BITCART_REVERSEPROXY=nginx 17 | export BTC_LIGHTNING=true 18 | # Use current repo's generator 19 | export BITCARTGEN_DOCKER_IMAGE=bitcart/docker-compose-generator:local 20 | ./setup.sh 21 | 22 | timeout 1m bash .circleci/test-connectivity.sh 23 | 24 | # Testing scripts are not crashing and installed 25 | ./start.sh 26 | ./stop.sh 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | 18 | [*.md] 19 | indent_size = 2 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .deploy 3 | .env 4 | .idea/ 5 | .images.json 6 | .mypy_cache 7 | .pytest_cache/ 8 | .vscode/ 9 | /compose/bitcart/ 10 | __pycache__/ 11 | compose/generated.yml 12 | coverage.xml 13 | htmlcov/ 14 | venv/ 15 | compose/plugins/* 16 | !compose/plugins/.gitkeep 17 | .prettiercache 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-merge-conflict 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | rev: v0.8.4 8 | hooks: 9 | - id: ruff 10 | - id: ruff-format 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v5.0.0 13 | hooks: 14 | - id: end-of-file-fixer 15 | - id: trailing-whitespace 16 | - id: mixed-line-ending 17 | - id: requirements-txt-fixer 18 | - id: check-case-conflict 19 | - id: check-shebang-scripts-are-executable 20 | - id: check-json 21 | - id: check-toml 22 | - id: check-yaml 23 | - id: check-symlinks 24 | - id: debug-statements 25 | - id: fix-byte-order-marker 26 | - id: detect-private-key 27 | - repo: https://github.com/pre-commit/mirrors-prettier 28 | rev: v4.0.0-alpha.8 29 | hooks: 30 | - id: prettier 31 | require_serial: true 32 | args: ["--cache-location=.prettiercache"] 33 | - repo: local 34 | hooks: 35 | - id: docker-shell-shfmt 36 | name: Run shfmt with docker 37 | entry: mvdan/shfmt:latest -d -i 4 38 | language: docker_image 39 | types: [shell] 40 | - repo: local 41 | hooks: 42 | - id: generate-images 43 | name: Generate docker images 44 | entry: compose/scripts/generate-templates.sh 45 | language: system 46 | files: >- 47 | ^compose/Dockerfile-coin.template$ 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MrNaif2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: ci 2 | 3 | lint: 4 | ruff check 5 | 6 | checkformat: 7 | ruff format --check 8 | 9 | format: 10 | ruff format 11 | 12 | test: 13 | pytest generator/tests/ ${ARGS} 14 | 15 | generate: 16 | @python3 -m generator 17 | 18 | ci: checkformat lint test 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcart Docker Deployment 2 | 3 | [![CircleCI](https://circleci.com/gh/bitcart/bitcart-docker.svg?style=svg)](https://circleci.com/gh/bitcart/bitcart-docker) 4 | ![Codecov](https://img.shields.io/codecov/c/github/bitcart/bitcart-docker?style=flat-square) 5 | 6 | ## Using provided scripts (easy) 7 | 8 | To install Bitcart, if you're on linux system(these scripts for windows will be added soon), 9 | to download, set up, and run your Bitcart instance, it is a matter of few commands: 10 | 11 | ```bash 12 | sudo su - 13 | git clone https://github.com/bitcart/bitcart-docker 14 | cd bitcart-docker 15 | # set needed environment variables, see below 16 | ./setup.sh 17 | ``` 18 | 19 | By default it will set up a systemd/upstart service to ensure your instance is running 24/7. 20 | To configure your installation you can set different environment variables. 21 | 22 | There are two types of environment variables: generator and app. 23 | To understand how generator works, see [Architecture](#architecture). 24 | 25 | Here is an example of the setup you will use in 90% cases. Replace `yourdomain.tld` with your actual domain (for bitcart demo, it was `bitcart.ai`): 26 | 27 | ```bash 28 | sudo su - 29 | git clone https://github.com/bitcart/bitcart-docker 30 | cd bitcart-docker 31 | export BITCART_HOST=yourdomain.tld 32 | ./setup.sh 33 | ``` 34 | 35 | This setup utilizes the [one domain mode](https://docs.bitcart.ai/guides/one-domain-mode). We recommend you to read about that. 36 | 37 | **Important: for everything to work, you will need to first set up DNS A records for** `BITCART_HOST`**,** `BITCART_ADMIN_HOST` **(if set) and** `BITCART_STORE_HOST` **(if set) to point to the server where you are deploying Bitcart.** 38 | 39 | ![DNS A records for bitcart demo](https://raw.githubusercontent.com/bitcart/bitcart-docs/master/.gitbook/assets/namecheap_dns_records.png) 40 | 41 | _**Tip:**_ All the `_HOST` environment variables determine on which host (domain, without protocol) to run a service. All the `_URL` environment variables are to specify the URL (with protocol, http:// or https://) of the Bitcart Merchants API to use. 42 | 43 | _**Tip:**_ if you want to try out Bitcart locally on your PC without a server, you can either enable [Tor support](https://docs.bitcart.ai/guides/tor) or use local deployment mode. 44 | 45 | For that, replace `yourdomain.tld` with `bitcart.local` (or any domain ending in `.local`), and it will modify `/etc/hosts` for you, for it to work like a regular domain. If using local deployment mode, of course your instance will only be accessible from your PC. 46 | 47 | If not using the [one domain mode](https://docs.bitcart.ai/guides/one-domain-mode), then you would probably run: 48 | 49 | ```bash 50 | sudo su - 51 | git clone https://github.com/bitcart/bitcart-docker 52 | cd bitcart-docker 53 | export BITCART_HOST=api.yourdomain.tld 54 | export BITCART_ADMIN_HOST=admin.yourdomain.tld 55 | export BITCART_STORE_HOST=store.yourdomain.tld 56 | export BITCART_ADMIN_API_URL=https://api.yourdomain.tld 57 | export BITCART_STORE_API_URL=https://api.yourdomain.tld 58 | ./setup.sh 59 | ``` 60 | 61 | Why was it done like so? It's because it is possible to run Merchants API on one server, and everything else on different servers. 62 | 63 | But in most cases, you can basically do: 64 | 65 | ```bash 66 | # if https (BITCART_REVERSEPROXY=nginx-https, default) 67 | export BITCART_ADMIN_API_URL=https://$BITCART_HOST 68 | export BITCART_STORE_API_URL=https://$BITCART_HOST 69 | # if http (BITCART_REVERSEPROXY=nginx, local deployments, other) 70 | export BITCART_ADMIN_API_URL=http://$BITCART_HOST 71 | export BITCART_STORE_API_URL=http://$BITCART_HOST 72 | ``` 73 | 74 | ## Configuration 75 | 76 | Configuration settings are set like so: 77 | 78 | export VARIABLE_NAME=value 79 | 80 | Here is a complete list of configuration settings: 81 | 82 | | Name | Description | Default | Type | 83 | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | --------- | 84 | | BITCART_HOST | Host where to run Bitcart Merchants API. Is used when merchants API (backend component) is enabled. | :x: | App | 85 | | BITCART_STORE_HOST | Host where to run Bitcart Ready Store. Is used when store component is enabled. | :x: | App | 86 | | BITCART_ADMIN_HOST | Host where to run Bitcart Admin Panel. Is used when admin component is enabled. | :x: | App | 87 | | BITCART_STORE_API_URL | URL of Bitcart Merchants API instance. It can be your instance hosted together with store or a completely separate instance. In case of default setup (store+admin+API at once), you need to set it to https://$BITCART_HOST or (http if nginx-https component is not enabled). | :x: | App | 88 | | BITCART_ADMIN_API_URL | Same as BITCART_STORE_API_URL, but for configuring your admin panel. | :x: | App | 89 | | BITCART_LETSENCRYPT_EMAIL | Email used for notifying you about your https certificates. Usually no action is needed to renew your certificates, but otherwise you'll get an email. Is used when nginx-https component is enabled. | :x: | App | 90 | | COINNAME_NETWORK | Used for configuring network of COINNAME daemon. Daemon can be run in only one network at once. Possible values are mainnet, testnet, and sometimes regtest and simnet. This setting affects only daemon of COINNAME, you need to set this value for each coin daemon you want to customize. | mainnet | App | 91 | | COINNAME_LIGHTNING | Used for enabling/disabling lightning support of COINNAME daemon. Some coins might not support lightning, in this case this setting does nothing. Possible values are true, false or not set. This setting affects only daemon of COINNAME, you need to set this value for each coin daemon you want to customize. | false | App | 92 | | BITCART_INSTALL | Used for enabling different ready installation presets, instead of including certain components manually. Currently possible values are: all (enable backend and frontend component groups), backend (backend group only), frontend (frontend group only), none (no preset enabled, by default only enabled component in that case is btc daemon. It is used in custom setups where merchants features aren't needed, and only daemons are needed to be managed by docker stack). Component groups include a few components to ensure all pieces work. Backend group currently includes postgres, redis and merchants API. If only this group is enabled it can be used as API for deployments on a different server for example. Frontend group includes admin and store. They either use built-in merchants API or a custom hosted one. | all | Generator | 93 | | BITCART_CRYPTOS | Used for configuring enabled crypto daemons. It is a comma-separated list of coin names, where each name is a coin code (for example btc, ltc). Each daemon component is enabled when it's respective coin code is in the list. | btc | Generator | 94 | | BITCART_REVERSEPROXY | Used for choosing reverse proxy in current docker-compose stack. Possible variants are: nginx-https(nginx+let's encrypt automatic ssl certificates), nginx(just nginx reverseproxy), none(no reverse proxy). Note that all HOST settings are applied only when nginx or nginx-https is enabled. When reverse proxy is none, few services expose their ports to the outside internet. By default they don't. List of those services: backend, admin, store and different coins if BITCART_COINAME_EXPOSE is true. | nginx-https | Generator | 95 | | BITCART_ADDITIONAL_COMPONENTS | A space separated list of additional components to add to docker-compose stack. Enable custom integrations or your own developed components, making your app fit into one container stack. (allows communication between containers, using same db, redis, etc.) | :x: | Generator | 96 | | BITCART_COINNAME_EXPOSE | Used only when no reverse proxy is enabled. By default daemons aren't exposed to outside internet and are accessible only from inside container network (from other containers). Note that exposing daemon port to outside is a security risk, as potentially your daemon might be using default credentials that can be viewed from source code. Only do that if you know what you're doing! Merchants API exists for many reasons, and one of those is to protect daemons from direct access. | :x: | Generator | 97 | | BITCART_COMPONENT_PORT | Used when no reverse proxy is enabled. By default certain services are exposed to outside by their internal ports (3000 for store, 4000 for admin, 8000 for merchants API, 500X for daemons). Use that to override external container port. Here component is the internal component name. It can be found in generator/docker-components directory. For example for store it is store, for admin it is admin, for merchants API-backend, for bitcoin daemon-bitcoin. When unset, default port is used. | :x: | Generator | 98 | | BITCART_COMPONENT_SCALE | Scale component up to X processes | 1 | Generator | 99 | | TOR_RELAY_NICKNAME | If tor relay is activated, the relay nickname | :x: | Extension | 100 | | TOR_RELAY_EMAIL | If tor relay is activated, the email for Tor to contact you regarding your relay | :x: | Extension | 101 | 102 | ## Live demo 103 | 104 | We have live demo available at: 105 | 106 | - [https://admin.bitcart.ai](https://admin.bitcart.ai/) Bitcart Admin Panel 107 | - [https://store.bitcart.ai](https://store.bitcart.ai/) Bitcart Ready Store POS 108 | - [https://api.bitcart.ai](https://api.bitcart.ai/) Bitcart Merchants API 109 | 110 | Note that it isn't designed for your production use, it is for testing and learning. 111 | 112 | ## Guide: how demo was set up 113 | 114 | Basically via deployment steps above (: 115 | 116 | Here are the commands used on our demo, as of October 2024, Bitcart Version 0.8.0.0: 117 | 118 | ```bash 119 | sudo su - 120 | git clone https://github.com/bitcart/bitcart-docker 121 | cd bitcart-docker 122 | # host settings 123 | export BITCART_HOST=api.bitcart.ai 124 | export BITCART_ADMIN_HOST=admin.bitcart.ai 125 | export BITCART_STORE_HOST=store.bitcart.ai 126 | export BITCART_ADMIN_API_URL=https://api.bitcart.ai 127 | export BITCART_STORE_API_URL=https://api.bitcart.ai 128 | # reverse proxy settings, we use none because we configure nginx manually 129 | export BITCART_REVERSEPROXY=none 130 | # cryptocurrency settings 131 | # we enable all currencies we support on the demo to test that they work 132 | export BITCART_CRYPTOS=btc,bch,ltc,xrg,eth,bnb,matic,trx,grs,xmr 133 | # lightning network for supported coins 134 | export BTC_LIGHTNING=true 135 | export LTC_LIGHTNING=true 136 | export GRS_LIGHTNING=true 137 | # tor support 138 | export BITCART_ADDITIONAL_COMPONENTS=tor 139 | ./setup.sh 140 | ``` 141 | 142 | ## Development builds 143 | 144 | Currently the testing of individual pieces of Bitcart is done via local development installation, see [Manual Deployment](https://docs.bitcart.ai/deployment/manual) about how it is done. 145 | 146 | When doing some changes in generator, it is usually tested via local python installation, like so: 147 | 148 | ```bash 149 | pip3 install oyaml 150 | make generate 151 | cat compose/generated.yml # see the generated output 152 | ``` 153 | 154 | If it is needed to test generator in docker, then run those commands: 155 | 156 | ```bash 157 | export BITCARTGEN_DOCKER_IMAGE=bitcart/docker-compose-generator:local 158 | ./build.sh # now uses local image 159 | ``` 160 | 161 | ## Architecture 162 | 163 | To provide a variety of deployment methods, to fit in every possible use case, we use a custom generator system. 164 | All the services are run by docker-compose. 165 | It makes it possible for containers to communicate between each other without exposing sensitive APIs to outside network. 166 | 167 | Usually, to launch docker-compose cluster a docker-compose.yml is required. 168 | 169 | In our case it is not present, as it is generated dynamically. 170 | 171 | When running generator manually, or as a part of easy deployment, generator is called (either dockerized or via local python). 172 | 173 | It runs a simple python script, it's purpose is to generated docker compose configuration file based on environment variables set. 174 | 175 | See [Configuration](#configuration) section above about how different configuration settings affect the choice of components. 176 | 177 | After getting a list of components to load, generator tries to load each of the components. 178 | It loads components from `generator/docker-components` directory. Each component is a piece of complete docker-compose.yml file, 179 | having service it provides, and any additional changes to other components. 180 | 181 | If no service with that name is found, it is just skipped. 182 | 183 | Each component might have services (containers), networks and volumes (for storing persistent data). 184 | 185 | All that data is collected from each component, and then all the services list is merged (it is done to make configuring one component from another one possible). 186 | 187 | After merging data into complete setup, generator applies a set of rules on them. 188 | Rules are python scripts, which can dynamically change some settings of components based on configuration settings or enabled components. 189 | 190 | Rules are loaded from `generator/rules` directory. Each rule is a python file. 191 | Each python file (.py) must define `rule` function, accepting two parameters - `services` (dictionary of all services loaded) and `settings` (loaded from env variables). 192 | 193 | If such function exists, it will be called with current services dictionary. 194 | Rules can modify that based on different settings. 195 | There are a few default settings bundled with Bitcart (for example, to expose ports to outside when no reverse proxy is enabled). 196 | You can create your own rules to add completely custom settings for your deployment. 197 | Your Bitcart deployment is not only Bitcart itself, but also a powerful and highly customizable docker-compose stack generator. 198 | 199 | After applying rules, the resulting data is written to compose/generated.yml file, which is final docker-compose.yml file used by startup scripts. 200 | -------------------------------------------------------------------------------- /admin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 4 | . "${SCRIPT_DIR}/helpers.sh" 5 | load_env 6 | 7 | policiesupdate() { 8 | docker exec -i $(container_name worker-1) python3 <" 48 | echo " set-user-admin " 49 | echo " reset-server-policy" 50 | ;; 51 | esac 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Add any volume names that need to be backed up here 6 | BACKUP_VOLUMES=(bitcart_datadir tor_servicesdir tor_datadir tor_relay_datadir) 7 | 8 | function display_help() { 9 | cat <<-END 10 | Usage: 11 | ------ 12 | Backup Bitcart files 13 | This script must be run as root 14 | -h, --help: Show help 15 | --only-db: Backup database only. Default: false 16 | --restart: Restart Bitcart (to avoid data corruption if needed). Default: false 17 | This script will backup the database as SQL script, essential volumes and put it to tar.gz archive 18 | It may optionally upload the backup to a remote server 19 | Environment variables: 20 | BACKUP_PROVIDER: where to upload. Default empty (local). See list of supported providers below 21 | SCP_TARGET: where to upload the backup via scp 22 | S3_BUCKET: where to upload the backup via s3 23 | S3_PATH: path to the backup on the remote server 24 | Supported providers: 25 | * local: keeps backups in backup_datadir docker volume (default) 26 | * scp: uploads the backup to a remote server via scp 27 | * s3: uploads to s3://bucket/path 28 | END 29 | } 30 | 31 | ONLY_DB=false 32 | RESTART_SERVICES=false 33 | 34 | # TODO: less duplication for args parsing 35 | 36 | while (("$#")); do 37 | case "$1" in 38 | -h) 39 | display_help 40 | exit 0 41 | ;; 42 | --help) 43 | display_help 44 | exit 0 45 | ;; 46 | --only-db) 47 | ONLY_DB=true 48 | shift 1 49 | ;; 50 | --restart) 51 | RESTART_SERVICES=true 52 | shift 1 53 | ;; 54 | --) # end argument parsing 55 | shift 56 | break 57 | ;; 58 | -* | --*=) # unsupported flags 59 | echo "Error: Unsupported flag $1" >&2 60 | display_help 61 | exit 1 62 | ;; 63 | *) 64 | shift 65 | ;; 66 | esac 67 | done 68 | 69 | . helpers.sh 70 | load_env true 71 | 72 | cd "$BITCART_BASE_DIRECTORY" 73 | 74 | deployment_name=$(volume_name) 75 | volumes_dir=/var/lib/docker/volumes 76 | backup_dir="$volumes_dir/backup_datadir" 77 | timestamp=$(date "+%Y%m%d-%H%M%S") 78 | filename="$timestamp-backup.tar.gz" 79 | dumpname="$timestamp-database.sql" 80 | 81 | backup_path="$backup_dir/_data/${filename}" 82 | dbdump_path="$backup_dir/_data/${dumpname}" 83 | 84 | echo "Dumping database …" 85 | bitcart_dump_db $dumpname 86 | 87 | if $ONLY_DB; then 88 | tar -cvzf $backup_path $dbdump_path 89 | else 90 | if $RESTART_SERVICES; then 91 | echo "Stopping Bitcart…" 92 | bitcart_stop 93 | fi 94 | 95 | echo "Backing up files …" 96 | files=() 97 | for fname in "${BACKUP_VOLUMES[@]}"; do 98 | fname=$(volume_name $fname) 99 | if [ -d "$volumes_dir/$fname" ]; then 100 | files+=("$fname") 101 | fi 102 | done 103 | # put all volumes to volumes directory and remove timestamps 104 | tar -cvzf $backup_path -C $volumes_dir --exclude="$(volume_name bitcart_datadir)/_data/host_authorized_keys" --exclude="$(volume_name bitcart_datadir)/_data/host_id_rsa" --exclude="$(volume_name bitcart_datadir)/_data/host_id_rsa.pub" --transform "s|^$deployment_name|volumes/$deployment_name|" "${files[@]}" \ 105 | -C "$(dirname $dbdump_path)" --transform "s|$timestamp-||" --transform "s|$timestamp||" $dumpname \ 106 | -C "$BITCART_BASE_DIRECTORY/compose" plugins 107 | 108 | if $RESTART_SERVICES; then 109 | echo "Restarting Bitcart…" 110 | bitcart_start 111 | fi 112 | fi 113 | 114 | delete_backup() { 115 | echo "Deleting local backup …" 116 | rm $backup_path 117 | } 118 | 119 | case $BACKUP_PROVIDER in 120 | "s3") 121 | echo "Uploading to S3 …" 122 | docker run --rm -v ~/.aws:/root/.aws -v $backup_path:/aws/$filename amazon/aws-cli s3 cp $filename s3://$S3_BUCKET/$S3_PATH 123 | delete_backup 124 | ;; 125 | 126 | "scp") 127 | echo "Uploading via SCP …" 128 | scp $backup_path $SCP_TARGET 129 | delete_backup 130 | ;; 131 | 132 | *) 133 | echo "Backed up to $backup_path" 134 | ;; 135 | esac 136 | 137 | # cleanup 138 | rm $dbdump_path 139 | 140 | echo "Backup done." 141 | 142 | set +e 143 | -------------------------------------------------------------------------------- /bitcart-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 4 | . "${SCRIPT_DIR}/helpers.sh" 5 | load_env 6 | docker exec $(container_name worker-1) bitcart-cli "$@" 7 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | : "${BITCARTGEN_DOCKER_IMAGE:=bitcart/docker-compose-generator}" 5 | if [ "$BITCARTGEN_DOCKER_IMAGE" == "bitcart/docker-compose-generator:local" ]; then 6 | docker build -f generator/Dockerfile . --tag $BITCARTGEN_DOCKER_IMAGE 7 | else 8 | set +e 9 | docker pull $BITCARTGEN_DOCKER_IMAGE 10 | docker rmi $(docker images bitcart/docker-compose-generator --format "{{.Tag}};{{.ID}}" | grep "^" | cut -f2 -d ';') >/dev/null 2>&1 11 | set -e 12 | fi 13 | 14 | docker run -v "$PWD/compose:/app/compose" \ 15 | --env-file <(env | grep BITCART_) \ 16 | --env NAME=$NAME \ 17 | --env REVERSEPROXY_HTTP_PORT=$REVERSEPROXY_HTTP_PORT \ 18 | --env REVERSEPROXY_HTTPS_PORT=$REVERSEPROXY_HTTPS_PORT \ 19 | --rm $BITCARTGEN_DOCKER_IMAGE $@ 20 | -------------------------------------------------------------------------------- /changedomain.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . helpers.sh 4 | load_env 5 | 6 | export NEW_HOST="$1" 7 | 8 | if [[ "$NEW_HOST" == https:* ]] || [[ "$NEW_HOST" == http:* ]]; then 9 | echo "The domain should not start by http: or https:" 10 | exit 1 11 | fi 12 | 13 | export OLD_HOST=$BITCART_HOST 14 | echo "Changing domain from \"$OLD_HOST\" to \"$NEW_HOST\"" 15 | 16 | export BITCART_HOST="$NEW_HOST" 17 | [[ "$OLD_HOST" == "$REVERSEPROXY_DEFAULT_HOST" ]] && export REVERSEPROXY_DEFAULT_HOST="$NEW_HOST" 18 | 19 | bitcart_update_docker_env 20 | apply_local_modifications 21 | bitcart_start 22 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker image prune -af --filter "label=org.bitcart.image" --filter "label!=org.bitcart.image=docker-compose-generator" 4 | -------------------------------------------------------------------------------- /compose/Dockerfile-coin.template: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine AS base 2 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 3 | 4 | ENV ELECTRUM_USER=electrum 5 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 6 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.{{ env.name }}{{ if env.custom == "false" and env.coin != "btc" then "-"+env.coin else "" end }} 7 | ENV IN_DOCKER=1 8 | ENV UV_COMPILE_BYTECODE=1 9 | ENV UV_NO_CACHE=1 10 | ENV UV_NO_SYNC=1 11 | ENV {{ env.coin | ascii_upcase }}_HOST=0.0.0.0 12 | LABEL org.bitcart.image={{ env.coin }}-daemon 13 | 14 | FROM base AS compile-image 15 | 16 | COPY bitcart $ELECTRUM_HOME/site 17 | 18 | {{ if env.bases == "btc" then ( -}} 19 | RUN apk add git python3-dev build-base libffi-dev{{ if env.coin == "ltc" then " openssl-dev" else . end }} && \ 20 | {{ ) elif env.bases == "bch" then ( -}} 21 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev openssl-dev rust cargo && \ 22 | {{ ) else ( -}} 23 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 24 | {{ ) end -}} 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group {{ env.coin }} 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/{{ env.coin }}.py"] 48 | -------------------------------------------------------------------------------- /compose/admin-plugins.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitcart/bitcart-admin:original 2 | 3 | USER root 4 | COPY plugins/admin modules 5 | COPY scripts/install-ui-plugins.sh /usr/local/bin/ 6 | RUN install-ui-plugins.sh 7 | USER node 8 | LABEL org.bitcart.plugins=true 9 | -------------------------------------------------------------------------------- /compose/backend-plugins.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitcart/bitcart:original 2 | 3 | COPY plugins/backend modules 4 | COPY scripts/install-backend-plugins.sh /usr/local/bin/ 5 | RUN bash install-backend-plugins.sh 6 | LABEL org.bitcart.plugins=true 7 | -------------------------------------------------------------------------------- /compose/backend.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-alpine AS go-builder 2 | 3 | RUN CGO_ENABLED=0 go install -ldflags '-X main.Version=docker -X main.envFile=/app/conf/.env' github.com/bitcart/bitcart-cli@master 4 | 5 | FROM python:3.11-slim-bullseye 6 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 7 | 8 | ARG TARGETPLATFORM 9 | ENV IN_DOCKER=1 10 | ENV GOSU_VERSION=1.16 11 | LABEL org.bitcart.image=backend 12 | ENV UV_COMPILE_BYTECODE=1 13 | ENV UV_NO_CACHE=1 14 | ENV UV_NO_SYNC=1 15 | 16 | COPY bitcart /app 17 | COPY scripts/docker-entrypoint.sh /usr/local/bin/ 18 | COPY --from=go-builder /go/bin/bitcart-cli /usr/local/bin/ 19 | WORKDIR /app 20 | RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client build-essential python3-dev libffi-dev ca-certificates wget libjemalloc2 && \ 21 | dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" && \ 22 | wget -qO /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" && \ 23 | chmod +x /usr/local/bin/gosu && \ 24 | groupadd --gid 1000 electrum && \ 25 | useradd --uid 1000 --gid electrum --shell /bin/bash --create-home electrum && \ 26 | uv sync --frozen --no-dev --group web --group production && \ 27 | apt-get purge -y build-essential python3-dev libffi-dev wget && \ 28 | apt-get autoremove -y && \ 29 | rm -rf /var/lib/apt/lists/* 30 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 31 | ENV PATH="/app/.venv/bin:$PATH" 32 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] 33 | CMD ["sh"] 34 | -------------------------------------------------------------------------------- /compose/bch.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.electron-cash 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV BCH_HOST=0.0.0.0 18 | LABEL org.bitcart.image=bch-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev openssl-dev rust cargo && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group bch 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/bch.py"] 48 | -------------------------------------------------------------------------------- /compose/bnb.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-bnb 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV BNB_HOST=0.0.0.0 18 | LABEL org.bitcart.image=bnb-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group bnb 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/bnb.py"] 48 | -------------------------------------------------------------------------------- /compose/bsty.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.electrum-bsty 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV BSTY_HOST=0.0.0.0 18 | LABEL org.bitcart.image=bsty-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git python3-dev build-base libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group bsty 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/bsty.py"] 48 | -------------------------------------------------------------------------------- /compose/btc.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.electrum 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV BTC_HOST=0.0.0.0 18 | LABEL org.bitcart.image=btc-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git python3-dev build-base libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group btc 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/btc.py"] 48 | -------------------------------------------------------------------------------- /compose/coin-plugins.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG COIN=btc 2 | 3 | FROM bitcart/bitcart-$COIN:original 4 | 5 | COPY plugins/daemon modules 6 | COPY scripts/install-daemon-plugins.sh /usr/local/bin/ 7 | RUN sh /usr/local/bin/install-daemon-plugins.sh 8 | LABEL org.bitcart.plugins=true 9 | -------------------------------------------------------------------------------- /compose/eth.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-eth 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV ETH_HOST=0.0.0.0 18 | LABEL org.bitcart.image=eth-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group eth 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/eth.py"] 48 | -------------------------------------------------------------------------------- /compose/grs.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.electrum-grs 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV GRS_HOST=0.0.0.0 18 | LABEL org.bitcart.image=grs-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git python3-dev build-base libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group grs 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/grs.py"] 48 | -------------------------------------------------------------------------------- /compose/ltc.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.electrum-ltc 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV LTC_HOST=0.0.0.0 18 | LABEL org.bitcart.image=ltc-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git python3-dev build-base libffi-dev openssl-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group ltc 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/ltc.py"] 48 | -------------------------------------------------------------------------------- /compose/matic.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-matic 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV MATIC_HOST=0.0.0.0 18 | LABEL org.bitcart.image=matic-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group matic 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/matic.py"] 48 | -------------------------------------------------------------------------------- /compose/nginx.tmpl: -------------------------------------------------------------------------------- 1 | {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} 2 | 3 | {{ define "upstream" }} 4 | {{ if .Address }} 5 | {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} 6 | {{ if and .Container.Node.ID .Address.HostPort }} 7 | # {{ .Container.Node.Name }}/{{ .Container.Name }} 8 | server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; 9 | {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} 10 | {{ else if .Network }} 11 | # {{ .Container.Name }} 12 | server {{ .Network.IP }}:{{ .Address.Port }}; 13 | {{ end }} 14 | {{ else if .Network }} 15 | # {{ .Container.Name }} 16 | {{ if .Network.IP }} 17 | server {{ .Network.IP }} down; 18 | {{ else }} 19 | server 127.0.0.1 down; 20 | {{ end }} 21 | {{ end }} 22 | 23 | {{ end }} 24 | {{ define "redirects" }} 25 | {{ $backendUsed := "false" }} 26 | {{ $adminUsed := "false" }} 27 | {{ if eq $.HostName "bitcart-store" }} 28 | {{ range $container := $.Containers }} 29 | {{ if (eq (or ($container.Env.ONE_DOMAIN_MODE) "") "true") }} 30 | {{ $serviceName := (index $container.Labels "com.docker.compose.service") }} 31 | {{ if (and (eq $backendUsed "false") (eq $serviceName "backend")) }} 32 | {{ $backendUsed = "true" }} 33 | location /api/ { 34 | proxy_pass http://{{ trim $container.Env.VIRTUAL_HOST_NAME }}/; 35 | } 36 | {{ end }} 37 | {{ if (and (eq $adminUsed "false") (eq $serviceName "admin")) }} 38 | {{ $adminUsed = "true" }} 39 | location /admin { 40 | proxy_pass http://{{ trim $container.Env.VIRTUAL_HOST_NAME }}; 41 | } 42 | {{ end }} 43 | {{ end }} 44 | {{ end }} 45 | {{ end }} 46 | {{ if eq $.HostName "bitcart-admin" }} 47 | {{ range $container := $.Containers }} 48 | {{ if (eq (or ($container.Env.ONE_DOMAIN_MODE) "") "true") }} 49 | {{ $serviceName := (index $container.Labels "com.docker.compose.service") }} 50 | {{ if (and (eq $backendUsed "false") (eq $serviceName "backend")) }} 51 | {{ $backendUsed = "true" }} 52 | location /api/ { 53 | proxy_pass http://{{ trim $container.Env.VIRTUAL_HOST_NAME }}/; 54 | } 55 | {{ end }} 56 | {{ end }} 57 | {{ end }} 58 | {{ end }} 59 | {{ end }} 60 | 61 | # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the 62 | # scheme used to connect to this server 63 | map $http_x_forwarded_proto $proxy_x_forwarded_proto { 64 | default $http_x_forwarded_proto; 65 | '' $scheme; 66 | } 67 | 68 | # If we receive X-Forwarded-Port, pass it through; otherwise, pass along the 69 | # server port the client connected to 70 | map $http_x_forwarded_port $proxy_x_forwarded_port { 71 | default $http_x_forwarded_port; 72 | '' $server_port; 73 | } 74 | 75 | # If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any 76 | # Connection header that may have been passed to this server 77 | map $http_upgrade $proxy_connection { 78 | default upgrade; 79 | '' close; 80 | } 81 | 82 | # Apply fix for very long server names 83 | server_names_hash_bucket_size 128; 84 | 85 | # Prevent Nginx Information Disclosure 86 | server_tokens off; 87 | 88 | # Default dhparam 89 | {{ if (exists "/etc/nginx/dhparam/dhparam.pem") }} 90 | ssl_dhparam /etc/nginx/dhparam/dhparam.pem; 91 | {{ end }} 92 | 93 | # Set appropriate X-Forwarded-Ssl header 94 | map $scheme $proxy_x_forwarded_ssl { 95 | default off; 96 | https on; 97 | } 98 | 99 | gzip on; 100 | gzip_min_length 1000; 101 | gzip_types image/svg+xml text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 102 | 103 | log_format vhost '$host $remote_addr - $remote_user [$time_local] ' 104 | '"$request" $status $body_bytes_sent ' 105 | '"$http_referer" "$http_user_agent"'; 106 | 107 | access_log off; 108 | 109 | {{ if $.Env.RESOLVERS }} 110 | resolver {{ $.Env.RESOLVERS }}; 111 | {{ end }} 112 | 113 | {{ if (exists "/etc/nginx/proxy.conf") }} 114 | include /etc/nginx/proxy.conf; 115 | {{ else }} 116 | # HTTP 1.1 support 117 | proxy_http_version 1.1; 118 | proxy_buffering off; 119 | proxy_set_header Host $http_host; 120 | proxy_set_header Upgrade $http_upgrade; 121 | proxy_set_header Connection $proxy_connection; 122 | proxy_set_header X-Real-IP $remote_addr; 123 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 124 | proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; 125 | proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; 126 | proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; 127 | 128 | # Mitigate httpoxy attack (see README for details) 129 | proxy_set_header Proxy ""; 130 | {{ end }} 131 | 132 | {{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }} 133 | 134 | {{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} 135 | server { 136 | server_name _; # This is just an invalid value which will never trigger on a real hostname. 137 | listen 443 ssl http2; 138 | {{ if $enable_ipv6 }} 139 | listen [::]:443 ssl http2; 140 | {{ end }} 141 | access_log /var/log/nginx/access.log vhost; 142 | return 503; 143 | 144 | ssl_session_tickets off; 145 | ssl_certificate /etc/nginx/certs/default.crt; 146 | ssl_certificate_key /etc/nginx/certs/default.key; 147 | } 148 | {{ end }} 149 | 150 | {{ range $host_name, $containers := groupByMulti $ "Env.VIRTUAL_HOST_NAME" "," }} 151 | 152 | {{ $host_name := trim $host_name }} 153 | {{ $upstream_name := $host_name }} 154 | 155 | upstream {{ $upstream_name }} { 156 | 157 | {{ range $container := $containers }} 158 | {{ $addrLen := len $container.Addresses }} 159 | 160 | {{ range $knownNetwork := $CurrentContainer.Networks }} 161 | {{ range $containerNetwork := $container.Networks }} 162 | {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} 163 | ## Can be connected with "{{ $containerNetwork.Name }}" network 164 | 165 | {{/* If only 1 port exposed, use that */}} 166 | {{ if eq $addrLen 1 }} 167 | {{ $address := index $container.Addresses 0 }} 168 | {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} 169 | {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} 170 | {{ else }} 171 | {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }} 172 | {{ $address := where $container.Addresses "Port" $port | first }} 173 | {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} 174 | {{ end }} 175 | {{ else }} 176 | # Cannot connect to network of this container 177 | server 127.0.0.1 down; 178 | {{ end }} 179 | {{ end }} 180 | {{ end }} 181 | {{ end }} 182 | } 183 | 184 | {{ $hiddenReverseProxy := trim (or (first (groupByKeys $containers "Env.HIDDENSERVICE_REVERSEPROXY")) "") }} 185 | {{ if ne $hiddenReverseProxy "" }} 186 | {{ $hiddenHostName := trim (or (first (groupByKeys $containers "Env.HIDDENSERVICE_NAME")) "") }} 187 | {{ $onionHost := read (printf "/var/lib/tor/hidden_services/%s/hostname" $hiddenHostName) }} 188 | {{ if ne $onionHost "" }} 189 | server { 190 | client_max_body_size 100M; 191 | server_name {{ trim $onionHost }}; 192 | listen 80 ; 193 | access_log /var/log/nginx/access.log vhost; 194 | {{ if (exists "/etc/nginx/vhost.d/default") }} 195 | include /etc/nginx/vhost.d/default; 196 | {{ end }} 197 | location / { 198 | proxy_pass http://{{ trim $upstream_name }}; 199 | } 200 | {{ template "redirects" (dict "HostName" $host_name "Containers" $) }} 201 | } 202 | {{ end }} 203 | {{ end }} 204 | 205 | {{ range $host, $containers := groupByMulti $containers "Env.VIRTUAL_HOST" "," }} 206 | {{ $host := trim $host }} 207 | {{ if ne $host "" }} 208 | {{ $default_host := or ($.Env.DEFAULT_HOST) "" }} 209 | {{ $default_server := index (dict $host "" $default_host "default_server") $host }} 210 | 211 | {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} 212 | {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} 213 | 214 | {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} 215 | {{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }} 216 | 217 | {{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} 218 | {{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) "redirect" }} 219 | 220 | {{/* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to "Mozilla-Intermediate" */}} 221 | {{ $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "Mozilla-Intermediate" }} 222 | 223 | {{/* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000" */}} 224 | {{ $hsts := or (first (groupByKeys $containers "Env.HSTS")) "max-age=31536000" }} 225 | 226 | {{/* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}} 227 | {{ $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} 228 | 229 | 230 | {{/* Get the first cert name defined by containers w/ the same vhost */}} 231 | {{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }} 232 | 233 | {{/* Get the best matching cert by name for the vhost. */}} 234 | {{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}} 235 | 236 | {{/* vhostCert is actually a filename so remove any suffixes since they are added later */}} 237 | {{ $vhostCert := trimSuffix ".crt" $vhostCert }} 238 | {{ $vhostCert := trimSuffix ".key" $vhostCert }} 239 | 240 | {{/* Use the cert specified on the container or fallback to the best vhost match */}} 241 | {{ $cert := (coalesce $certName $vhostCert) }} 242 | 243 | {{ $is_https := (and (ne $https_method "nohttps") (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }} 244 | 245 | server { 246 | client_max_body_size 100M; 247 | server_name _; 248 | listen 80; 249 | {{ if $enable_ipv6 }} 250 | listen [::]:80; 251 | {{ end }} 252 | access_log /var/log/nginx/access.log vhost; 253 | location / { 254 | proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; 255 | } 256 | {{ template "redirects" (dict "HostName" $host_name "Containers" $) }} 257 | } 258 | 259 | {{ if $is_https }} 260 | 261 | {{ if eq $https_method "redirect" }} 262 | server { 263 | server_name {{ $host }}; 264 | listen 80 {{ $default_server }}; 265 | {{ if $enable_ipv6 }} 266 | listen [::]:80 {{ $default_server }}; 267 | {{ end }} 268 | access_log /var/log/nginx/access.log vhost; 269 | return 301 https://$host$request_uri; 270 | } 271 | {{ end }} 272 | 273 | server { 274 | client_max_body_size 100M; 275 | server_name {{ $host }}; 276 | listen 443 ssl http2 {{ $default_server }}; 277 | {{ if $enable_ipv6 }} 278 | listen [::]:443 ssl http2 {{ $default_server }}; 279 | {{ end }} 280 | access_log /var/log/nginx/access.log vhost; 281 | 282 | {{ if eq $network_tag "internal" }} 283 | # Only allow traffic from internal clients 284 | include /etc/nginx/network_internal.conf; 285 | {{ end }} 286 | 287 | {{ if eq $ssl_policy "Mozilla-Modern" }} 288 | ssl_protocols TLSv1.2 TLSv1.3; 289 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; 290 | {{ else if eq $ssl_policy "Mozilla-Intermediate" }} 291 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 292 | ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:!DSS'; 293 | {{ else if eq $ssl_policy "Mozilla-Old" }} 294 | ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 295 | ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP'; 296 | {{ else if eq $ssl_policy "AWS-TLS-1-2-2017-01" }} 297 | ssl_protocols TLSv1.2 TLSv1.3; 298 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES128-SHA256:AES256-GCM-SHA384:AES256-SHA256'; 299 | {{ else if eq $ssl_policy "AWS-TLS-1-1-2017-01" }} 300 | ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; 301 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA'; 302 | {{ else if eq $ssl_policy "AWS-2016-08" }} 303 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 304 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA'; 305 | {{ else if eq $ssl_policy "AWS-2015-05" }} 306 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 307 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DES-CBC3-SHA'; 308 | {{ else if eq $ssl_policy "AWS-2015-03" }} 309 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 310 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA:DES-CBC3-SHA'; 311 | {{ else if eq $ssl_policy "AWS-2015-02" }} 312 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 313 | ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA'; 314 | {{ end }} 315 | 316 | ssl_prefer_server_ciphers on; 317 | ssl_session_timeout 5m; 318 | ssl_session_cache shared:SSL:50m; 319 | ssl_session_tickets off; 320 | 321 | ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }}; 322 | ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }}; 323 | 324 | {{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }} 325 | ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }}; 326 | {{ end }} 327 | 328 | {{ if (exists (printf "/etc/nginx/certs/%s.chain.pem" $cert)) }} 329 | ssl_stapling on; 330 | ssl_stapling_verify on; 331 | ssl_trusted_certificate {{ printf "/etc/nginx/certs/%s.chain.pem" $cert }}; 332 | {{ end }} 333 | 334 | {{ if (and (ne $https_method "noredirect") (ne $hsts "off")) }} 335 | add_header Strict-Transport-Security "{{ trim $hsts }}" always; 336 | {{ end }} 337 | 338 | {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} 339 | include {{ printf "/etc/nginx/vhost.d/%s" $host }}; 340 | {{ else if (exists "/etc/nginx/vhost.d/default") }} 341 | include /etc/nginx/vhost.d/default; 342 | {{ end }} 343 | 344 | location / { 345 | {{ if eq $proto "uwsgi" }} 346 | include uwsgi_params; 347 | uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; 348 | {{ else if eq $proto "fastcgi" }} 349 | root {{ trim $vhost_root }}; 350 | include fastcgi.conf; 351 | fastcgi_pass {{ trim $upstream_name }}; 352 | {{ else }} 353 | proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; 354 | {{ end }} 355 | 356 | {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} 357 | auth_basic "Restricted {{ $host }}"; 358 | auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; 359 | {{ end }} 360 | {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} 361 | include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; 362 | {{ else if (exists "/etc/nginx/vhost.d/default_location") }} 363 | include /etc/nginx/vhost.d/default_location; 364 | {{ end }} 365 | } 366 | {{ template "redirects" (dict "HostName" $host_name "Containers" $) }} 367 | } 368 | 369 | {{ end }} 370 | 371 | {{ if or (not $is_https) (eq $https_method "noredirect") }} 372 | 373 | server { 374 | client_max_body_size 100M; 375 | server_name {{ $host }}; 376 | listen 80 {{ $default_server }}; 377 | {{ if $enable_ipv6 }} 378 | listen [::]:80 {{ $default_server }}; 379 | {{ end }} 380 | access_log /var/log/nginx/access.log vhost; 381 | 382 | {{ if eq $network_tag "internal" }} 383 | # Only allow traffic from internal clients 384 | include /etc/nginx/network_internal.conf; 385 | {{ end }} 386 | 387 | {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} 388 | include {{ printf "/etc/nginx/vhost.d/%s" $host }}; 389 | {{ else if (exists "/etc/nginx/vhost.d/default") }} 390 | include /etc/nginx/vhost.d/default; 391 | {{ end }} 392 | 393 | location / { 394 | {{ if eq $proto "uwsgi" }} 395 | include uwsgi_params; 396 | uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; 397 | {{ else if eq $proto "fastcgi" }} 398 | root {{ trim $vhost_root }}; 399 | include fastcgi.conf; 400 | fastcgi_pass {{ trim $upstream_name }}; 401 | {{ else }} 402 | proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; 403 | {{ end }} 404 | {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} 405 | auth_basic "Restricted {{ $host }}"; 406 | auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; 407 | {{ end }} 408 | {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} 409 | include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; 410 | {{ else if (exists "/etc/nginx/vhost.d/default_location") }} 411 | include /etc/nginx/vhost.d/default_location; 412 | {{ end }} 413 | } 414 | {{ template "redirects" (dict "HostName" $host_name "Containers" $) }} 415 | } 416 | 417 | {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} 418 | server { 419 | server_name {{ $host }}; 420 | listen 443 ssl http2 {{ $default_server }}; 421 | {{ if $enable_ipv6 }} 422 | listen [::]:443 ssl http2 {{ $default_server }}; 423 | {{ end }} 424 | access_log /var/log/nginx/access.log vhost; 425 | return 500; 426 | 427 | ssl_certificate /etc/nginx/certs/default.crt; 428 | ssl_certificate_key /etc/nginx/certs/default.key; 429 | } 430 | {{ end }} 431 | {{ end }} 432 | {{ end }} 433 | {{ end }} 434 | {{ end }} 435 | {{ if $.Env.ADDITIONAL_NGINX_CONFIG }} 436 | {{ $.Env.ADDITIONAL_NGINX_CONFIG}} 437 | {{ end }} 438 | -------------------------------------------------------------------------------- /compose/pihole/resolv.conf: -------------------------------------------------------------------------------- 1 | nameserver 127.0.0.1 2 | nameserver 8.8.8.8 3 | -------------------------------------------------------------------------------- /compose/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcart/bitcart-docker/c4aa7c3b0426fd62d8cbfd45f873de1cc0249b52/compose/plugins/.gitkeep -------------------------------------------------------------------------------- /compose/sbch.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-sbch 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV SBCH_HOST=0.0.0.0 18 | LABEL org.bitcart.image=sbch-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group sbch 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/sbch.py"] 48 | -------------------------------------------------------------------------------- /compose/scripts/.jq-template.awk: -------------------------------------------------------------------------------- 1 | # this script assumes gawk! (busybox "awk" is not quite sufficient) 2 | 3 | # Thanks to https://github.com/docker-library 4 | 5 | # escape an arbitrary string for passing back to jq as program input 6 | function jq_escape(str) { 7 | gsub(/\\/, "\\\\", str) 8 | gsub(/\n/, "\\n", str) 9 | gsub(/\r/, "\\r", str) 10 | gsub(/\t/, "\\t", str) 11 | gsub(/"/, "\\\"", str) 12 | return "\"" str "\"" 13 | } 14 | 15 | # return the number of times needle appears in haystack 16 | function num(haystack, needle, # parameters 17 | ret, i ) # locals 18 | { 19 | ret = 0 20 | while (i = index(haystack, needle)) { 21 | ret++ 22 | haystack = substr(haystack, i + length(needle)) 23 | } 24 | return ret 25 | } 26 | 27 | BEGIN { 28 | jq_expr_defs = "" 29 | jq_expr = "" 30 | agg_jq = "" 31 | agg_text = "" 32 | 33 | OPEN = "{{" 34 | CLOSE = "}}" 35 | CLOSE_EAT_EOL = "-" CLOSE ORS 36 | } 37 | 38 | function trim(str) { 39 | sub(/^[[:space:]]+/, "", str) 40 | sub(/[[:space:]]+$/, "", str) 41 | return str 42 | } 43 | function append(str) { 44 | if (jq_expr && jq_expr !~ /\($/ && str !~ /^\)/) { 45 | jq_expr = jq_expr "\n+ " 46 | } else if (jq_expr) { 47 | jq_expr = jq_expr "\n" 48 | } 49 | jq_expr = jq_expr str 50 | } 51 | function append_string(str) { 52 | if (!str) return 53 | str = jq_escape(str) 54 | append(str) 55 | } 56 | function append_jq(expr) { 57 | if (!expr) return 58 | expr = trim(expr) 59 | if (!expr) return 60 | if (expr ~ /^#[^\n]*$/) return # ignore pure comment lines {{ # ... -}} 61 | if (expr ~ /^(def|include|import)[[:space:]]/) { # a few things need to go at the start of our "script" 62 | jq_expr_defs = jq_expr_defs expr ";\n" 63 | return 64 | } 65 | # if expr doesn't begin with ")" or end with "(", wrap it in parenthesis (so our addition chain works properly) 66 | if (expr !~ /^\)/) expr = "(" expr 67 | if (expr !~ /\($/) expr = expr ")" 68 | append(expr) 69 | } 70 | 71 | { 72 | line = $0 ORS 73 | 74 | i = 0 75 | if (agg_jq || (i = index(line, OPEN))) { 76 | if (i) { 77 | agg_text = agg_text substr(line, 1, i - 1) 78 | line = substr(line, i) 79 | } 80 | append_string(agg_text) 81 | agg_text = "" 82 | 83 | agg_jq = agg_jq line 84 | line = "" 85 | 86 | if (num(agg_jq, OPEN) > num(agg_jq, CLOSE)) { 87 | next 88 | } 89 | 90 | while (i = index(agg_jq, OPEN)) { 91 | line = substr(agg_jq, 1, i - 1) 92 | agg_jq = substr(agg_jq, i + length(OPEN)) 93 | if (i = index(agg_jq, CLOSE_EAT_EOL)) { 94 | expr = substr(agg_jq, 1, i - 1) 95 | agg_jq = substr(agg_jq, i + length(CLOSE_EAT_EOL)) 96 | } 97 | else { 98 | i = index(agg_jq, CLOSE) 99 | expr = substr(agg_jq, 1, i - 1) 100 | agg_jq = substr(agg_jq, i + length(CLOSE)) 101 | } 102 | append_string(line) 103 | append_jq(expr) 104 | } 105 | line = agg_jq 106 | agg_jq = "" 107 | } 108 | 109 | if (line) { 110 | agg_text = agg_text line 111 | } 112 | } 113 | 114 | END { 115 | append_string(agg_text) 116 | agg_text = "" 117 | 118 | append_jq(agg_jq) 119 | agg_jq = "" 120 | 121 | jq_expr = "(\n" jq_expr "\n)" 122 | jq_expr = jq_expr_defs jq_expr 123 | prog = "jq --join-output --null-input --from-file /dev/stdin" 124 | 125 | printf "%s", jq_expr | prog 126 | 127 | e = close(prog) 128 | if (e != 0) { 129 | exit(e) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /compose/scripts/cli-autocomplete.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | : ${PROG:=$(basename ${BASH_SOURCE})} 4 | 5 | # Macs have bash3 for which the bash-completion package doesn't include 6 | # _init_completion. This is a minimal version of that function. 7 | _cli_init_completion() { 8 | COMPREPLY=() 9 | _get_comp_words_by_ref "$@" cur prev words cword 10 | } 11 | 12 | _cli_bash_autocomplete() { 13 | if [[ "${COMP_WORDS[0]}" != "source" ]]; then 14 | local cur opts base words 15 | COMPREPLY=() 16 | cur="${COMP_WORDS[COMP_CWORD]}" 17 | if declare -F _init_completion >/dev/null 2>&1; then 18 | _init_completion -n "=:" || return 19 | else 20 | _cli_init_completion -n "=:" || return 21 | fi 22 | words=("${words[@]:0:$cword}") 23 | if [[ "$cur" == "-"* ]]; then 24 | requestComp="${words[*]} ${cur} --generate-shell-completion" 25 | else 26 | requestComp="${words[*]} --generate-shell-completion" 27 | fi 28 | opts=$(eval "${requestComp}" 2>/dev/null) 29 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 30 | return 0 31 | fi 32 | } 33 | 34 | complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG 35 | unset PROG 36 | -------------------------------------------------------------------------------- /compose/scripts/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -ex 3 | 4 | # Bitcart is configuring current instance or updating it via SSH access 5 | 6 | if [ ! -z "$SSH_KEY_FILE" ] && ! [ -f "$SSH_KEY_FILE" ]; then 7 | echo "Creating Bitcart SSH key File..." 8 | ssh-keygen -t ed25519 -f "$SSH_KEY_FILE" -q -P "" -m PEM -C bitcart >/dev/null 9 | if [ -f "$SSH_AUTHORIZED_KEYS" ]; then 10 | # Because the file is mounted, sed -i does not work 11 | sed '/bitcart$/d' "$SSH_AUTHORIZED_KEYS" >"$SSH_AUTHORIZED_KEYS.new" 12 | cat "$SSH_AUTHORIZED_KEYS.new" >"$SSH_AUTHORIZED_KEYS" 13 | rm -rf "$SSH_AUTHORIZED_KEYS.new" 14 | fi 15 | fi 16 | 17 | if [ ! -z "$SSH_KEY_FILE" ] && [ -f "$SSH_AUTHORIZED_KEYS" ] && ! grep -q "bitcart$" "$SSH_AUTHORIZED_KEYS"; then 18 | echo "Adding Bitcart SSH key to authorized keys" 19 | cat "$SSH_KEY_FILE.pub" >>"$SSH_AUTHORIZED_KEYS" 20 | fi 21 | 22 | # Fix all permissions 23 | 24 | getent group tor || groupadd --gid 19001 tor && usermod -a -G tor electrum 25 | 26 | for volume in $BITCART_VOLUMES; do 27 | if [ -d "$volume" ]; then 28 | # ignore authorized keys to not break ssh 29 | find "$volume" \! -user electrum \! -wholename '/datadir/host_authorized_keys' -exec chown electrum '{}' + 30 | fi 31 | done 32 | 33 | exec gosu electrum "$@" 34 | -------------------------------------------------------------------------------- /compose/scripts/generate-templates.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | # Thanks to https://github.com/docker-library 5 | 6 | images="compose/scripts/.images.json" 7 | 8 | if [ ! -f "$images" ] || [ ! "$#" -eq 0 ]; then 9 | wget -qO "$images" 'https://raw.githubusercontent.com/bitcart/bitcart/master/.circleci/images.json' 10 | fi 11 | 12 | generated_warning() { 13 | cat <<-EOH 14 | # 15 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 16 | # 17 | # PLEASE DO NOT EDIT IT DIRECTLY. 18 | # 19 | 20 | EOH 21 | } 22 | 23 | coins="$(jq -r '.[].dockerfile | sub(".Dockerfile"; "")' $images)" 24 | eval "coins=( $coins )" 25 | 26 | for coin in "${coins[@]}"; do 27 | if [ "$coin" == "backend" ]; then 28 | continue 29 | fi 30 | export bases=$(jq -r ".[\"bitcart-$coin\"].bases // \"btc\"" $images) 31 | export coin 32 | custom=false 33 | name=$(jq -r ".[\"bitcart-$coin\"].name // \"\"" $images) 34 | if [ -z "$name" ]; then 35 | if [ "$bases" == "btc" ]; then 36 | name="electrum" 37 | fi 38 | if [ "$bases" == "eth" ]; then 39 | name="bitcart" 40 | fi 41 | else 42 | custom=true 43 | fi 44 | export name 45 | export custom 46 | echo "processing compose/$coin..." 47 | { 48 | generated_warning 49 | gawk -f "compose/scripts/.jq-template.awk" "compose/Dockerfile-coin.template" 50 | } >"compose/$coin.Dockerfile" 51 | done 52 | -------------------------------------------------------------------------------- /compose/scripts/install-backend-plugins.sh: -------------------------------------------------------------------------------- 1 | for org in modules/*; do 2 | if [[ -d "$org" && ! -L "$org" ]]; then 3 | for plugin in "$org"/*; do 4 | if [[ -d "$plugin" && ! -L "$plugin" ]]; then 5 | echo "Installing $plugin" 6 | # check if file exists first 7 | if [ -f "$plugin/requirements.txt" ]; then 8 | uv pip install -r "$plugin/requirements.txt" 9 | fi 10 | # apply all patches from patches dir 11 | if [[ -d "$plugin/patches" ]]; then 12 | for patch in "$plugin/patches"/*; do 13 | if [ -f "$patch" ]; then 14 | git apply "$patch" 15 | fi 16 | done 17 | fi 18 | fi 19 | done 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /compose/scripts/install-daemon-plugins.sh: -------------------------------------------------------------------------------- 1 | for org in modules/*; do 2 | if [[ -d "$org" && ! -L "$org" ]]; then 3 | for plugin in "$org"/*; do 4 | if [[ -d "$plugin" && ! -L "$plugin" ]]; then 5 | echo "Installing $plugin" 6 | # Copy all dirs to daemons folder 7 | cp -r $plugin/* daemons/ 8 | # apply all patches from patches dir 9 | if [[ -d "$plugin/patches" ]]; then 10 | for patch in "$plugin/patches"/*; do 11 | if [ -f "$patch" ]; then 12 | git apply "$patch" 13 | fi 14 | done 15 | fi 16 | fi 17 | done 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /compose/scripts/install-ui-plugins.sh: -------------------------------------------------------------------------------- 1 | found=false 2 | for org in modules/*; do 3 | if [ -d "$org" ]; then 4 | for plugin in "$org"/*; do 5 | if [ -d "$plugin" ]; then 6 | if [ "$plugin" = "modules/@bitcart/core" ]; then 7 | continue 8 | fi 9 | found=true 10 | echo "Installing $plugin" 11 | if [ -f "$plugin/package.json" ]; then 12 | cd "$plugin" 13 | yarn 14 | cd $OLDPWD 15 | fi 16 | fi 17 | done 18 | fi 19 | done 20 | 21 | if [ "$found" = false ]; then 22 | echo "No plugins found" 23 | else 24 | echo "Plugins installed, re-building" 25 | yarn build 26 | yarn cache clean 27 | fi 28 | -------------------------------------------------------------------------------- /compose/store-plugins.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitcart/bitcart-store:original 2 | 3 | USER root 4 | COPY plugins/store modules 5 | COPY scripts/install-ui-plugins.sh /usr/local/bin/ 6 | RUN install-ui-plugins.sh 7 | USER node 8 | LABEL org.bitcart.plugins=true 9 | -------------------------------------------------------------------------------- /compose/torrc-relay.tmpl: -------------------------------------------------------------------------------- 1 | ORPort 9001 2 | DirPort 9030 3 | ExitPolicy reject *:* 4 | CookieAuthentication 1 5 | 6 | Nickname {{ $.Env.TOR_RELAY_NICKNAME}} 7 | ContactInfo {{ $.Env.TOR_RELAY_EMAIL}} 8 | 9 | 10 | {{ if $.Env.ADDITIONAL_TORRC_CONFIG }} 11 | {{ $.Env.ADDITIONAL_TORRC_CONFIG}} 12 | {{ end }} 13 | -------------------------------------------------------------------------------- /compose/torrc.tmpl: -------------------------------------------------------------------------------- 1 | {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} 2 | 3 | {{ range $name, $containers := groupByMulti $ "Env.HIDDENSERVICE_NAME" "," }} 4 | {{ $firstServicePort := true }} 5 | {{ range $container := $containers }} 6 | {{ range $knownNetwork := $CurrentContainer.Networks }} 7 | {{ range $containerNetwork := $container.Networks }} 8 | {{ if eq $knownNetwork.Name $containerNetwork.Name }} 9 | {{ $containerOrReverseProxyName := coalesce $container.Env.HIDDENSERVICE_REVERSEPROXY $container.Name }} 10 | 11 | {{ range $reverseProxyContainer := where $ "Name" $containerOrReverseProxyName }} 12 | {{ range $containerNetwork := where $reverseProxyContainer.Networks "Name" $knownNetwork.Name }} 13 | {{ $port := coalesceempty (index $container.Env (print $name "_HIDDENSERVICE_PORT")) $container.Env.HIDDENSERVICE_PORT "80" }} 14 | {{ $virtualPort := coalesceempty (index $container.Env (print $name "_HIDDENSERVICE_VIRTUAL_PORT")) $container.Env.HIDDENSERVICE_VIRTUAL_PORT $port }} 15 | {{ $ip := coalesceempty $container.Env.HIDDENSERVICE_IP $containerNetwork.IP }} 16 | {{ if ne $ip "" }} 17 | {{ if $firstServicePort }} 18 | # For the hidden service {{ $name }} 19 | HiddenServiceDir /var/lib/tor/hidden_services/{{ $name }} 20 | HiddenServiceDirGroupReadable 1 21 | {{ $firstServicePort = false }} 22 | {{ end }} 23 | # Redirecting to {{ $containerOrReverseProxyName }} 24 | HiddenServicePort {{ $port }} {{ $ip }}:{{ $virtualPort }} 25 | {{ end }} 26 | {{ end }} 27 | {{ end }} 28 | {{ end }} 29 | {{ end }} 30 | {{ end }} 31 | {{ end }} 32 | {{ end }} 33 | 34 | {{ if $.Env.ADDITIONAL_TORRC_CONFIG }} 35 | {{ $.Env.ADDITIONAL_TORRC_CONFIG}} 36 | {{ end }} 37 | -------------------------------------------------------------------------------- /compose/trx.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-trx 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV TRX_HOST=0.0.0.0 18 | LABEL org.bitcart.image=trx-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group trx 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/trx.py"] 48 | -------------------------------------------------------------------------------- /compose/xmr.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.bitcart-xmr 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV XMR_HOST=0.0.0.0 18 | LABEL org.bitcart.image=xmr-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group xmr 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/xmr.py"] 48 | -------------------------------------------------------------------------------- /compose/xrg.Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: THIS DOCKERFILE IS GENERATED VIA "generate-templates.sh" 3 | # 4 | # PLEASE DO NOT EDIT IT DIRECTLY. 5 | # 6 | 7 | FROM python:3.11-alpine AS base 8 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 9 | 10 | ENV ELECTRUM_USER=electrum 11 | ENV ELECTRUM_HOME=/home/$ELECTRUM_USER 12 | ENV ELECTRUM_DIRECTORY=${ELECTRUM_HOME}/.oregano 13 | ENV IN_DOCKER=1 14 | ENV UV_COMPILE_BYTECODE=1 15 | ENV UV_NO_CACHE=1 16 | ENV UV_NO_SYNC=1 17 | ENV XRG_HOST=0.0.0.0 18 | LABEL org.bitcart.image=xrg-daemon 19 | 20 | FROM base AS compile-image 21 | 22 | COPY bitcart $ELECTRUM_HOME/site 23 | 24 | RUN apk add git gcc python3-dev musl-dev automake autoconf libtool file git make libffi-dev openssl-dev rust cargo && \ 25 | cd $ELECTRUM_HOME/site && \ 26 | uv sync --frozen --no-dev --group xrg 27 | 28 | FROM base AS build-image 29 | 30 | RUN adduser -D $ELECTRUM_USER && \ 31 | mkdir -p /data/ && \ 32 | ln -sf /data/ $ELECTRUM_DIRECTORY && \ 33 | chown ${ELECTRUM_USER} $ELECTRUM_DIRECTORY && \ 34 | mkdir -p $ELECTRUM_HOME/site && \ 35 | chown ${ELECTRUM_USER} $ELECTRUM_HOME/site && \ 36 | apk add --no-cache libsecp256k1-dev git && \ 37 | apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main jemalloc 38 | 39 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site/.venv $ELECTRUM_HOME/.venv 40 | COPY --from=compile-image --chown=electrum $ELECTRUM_HOME/site $ELECTRUM_HOME/site 41 | 42 | ENV PYTHONUNBUFFERED=1 PYTHONMALLOC=malloc LD_PRELOAD=libjemalloc.so.2 MALLOC_CONF=background_thread:true,max_background_threads:1,metadata_thp:auto,dirty_decay_ms:80000,muzzy_decay_ms:80000 43 | ENV PATH="$ELECTRUM_HOME/.venv/bin:$PATH" 44 | USER $ELECTRUM_USER 45 | WORKDIR $ELECTRUM_HOME/site 46 | 47 | CMD ["python","daemons/xrg.py"] 48 | -------------------------------------------------------------------------------- /contrib/upgrades/README.md: -------------------------------------------------------------------------------- 1 | # Upgrade helpers 2 | 3 | This directory contains a list of files to help upgrade to various Bitcart versions. 4 | 5 | To apply any fix (note, scripts are one-time only in most cases), just run: 6 | 7 | `contrib/upgrades/upgrade-to-version.sh` 8 | 9 | Your docker deployment should be running. 10 | 11 | Current list: 12 | 13 | - `upgrade-to-5000.sh`, helps to upgrade to Bitcart 0.5.0.0, run this in case you get a migration error (invalid foreign key constraints names). It might be required for older Bitcart deployments, requires a running database container 14 | - `upgrade-to-0600.sh`, helps to upgrade to Bitcart 0.6.0.0, run this if you need to migrate your logs and images 15 | - `upgrade-to-0610.sh`, helps to change postgresql config to allow password-less login 16 | - `upgrade-to-0680.sh`, helps to fix permissions on tor hidden services volumes 17 | -------------------------------------------------------------------------------- /contrib/upgrades/upgrade-to-0500.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 3 | . "${SCRIPT_DIR}/../../helpers.sh" 4 | load_env 5 | 6 | docker exec -i $(container_name database-1) psql -U postgres bitcart </dev/null 2>&1 && docker exec -i $(container_name worker-1) python3 </dev/null && pwd)" 13 | . "${SCRIPT_DIR}/../../helpers.sh" 14 | load_env 15 | COMPOSE_DIR="$(realpath "${SCRIPT_DIR}/../../compose")" 16 | 17 | volumes_dir=/var/lib/docker/volumes 18 | datadir="$volumes_dir/$(volume_name bitcart_datadir)/_data" 19 | logdir="$volumes_dir/$(volume_name bitcart_logs)/_data" 20 | cp -rv --preserve $logdir/* $datadir/logs/ 21 | for fname in $datadir/logs/bitcart-log.log*; do 22 | fname_new=$(convert_name "$fname") 23 | mv -v "$fname" "$fname_new" 24 | done 25 | docker volume rm $(volume_name bitcart_logs) 26 | cp -rv --preserve $COMPOSE_DIR/images/* $datadir/images/ 27 | rm -rf "$COMPOSE_DIR/images" 28 | rm -rf "$COMPOSE_DIR/conf" 29 | -------------------------------------------------------------------------------- /contrib/upgrades/upgrade-to-0610.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 3 | . "${SCRIPT_DIR}/../../helpers.sh" 4 | load_env 5 | 6 | docker exec -i $(container_name database-1) sed -i 's/host all all all md5/host all all all trust/' /var/lib/postgresql/data/pg_hba.conf 7 | bitcart_restart 8 | -------------------------------------------------------------------------------- /contrib/upgrades/upgrade-to-0680.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 3 | . "${SCRIPT_DIR}/../../helpers.sh" 4 | load_env 5 | 6 | volumes_dir=/var/lib/docker/volumes 7 | datadir="$volumes_dir/$(volume_name tor_servicesdir)/_data" 8 | 9 | find "$datadir" -type d -exec chmod 750 '{}' + 10 | bitcart_restart 11 | -------------------------------------------------------------------------------- /contrib/upgrades/upgrade-to-0800.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" 3 | . "${SCRIPT_DIR}/../../helpers.sh" 4 | load_env 5 | 6 | try mv /etc/profile.d/bitcartcc-env$SCRIPTS_POSTFIX.sh /etc/profile.d/bitcart-env$SCRIPTS_POSTFIX.sh 7 | try mv $HOME/bitcartcc-env$SCRIPTS_POSTFIX.sh $HOME/bitcart-env$SCRIPTS_POSTFIX.sh 8 | try systemctl disable bitcartcc$SCRIPTS_POSTFIX.service 9 | try systemctl stop bitcartcc$SCRIPTS_POSTFIX.service 10 | try mv /etc/systemd/system/bitcartcc$SCRIPTS_POSTFIX.service /etc/systemd/system/bitcart$SCRIPTS_POSTFIX.service 11 | try systemctl daemon-reload 12 | try systemctl enable bitcart$SCRIPTS_POSTFIX.service 13 | try systemctl start bitcart$SCRIPTS_POSTFIX.service 14 | -------------------------------------------------------------------------------- /dev-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | if [[ "$OSTYPE" == "darwin"* ]]; then 4 | brew install git 5 | else 6 | sudo apt install -y git 7 | fi 8 | branch=${1:-master} 9 | rm -rf compose/bitcart && git clone --depth=1 https://github.com/bitcart/bitcart -b $branch compose/bitcart 10 | cd compose/bitcart 11 | rm -rf .git 12 | cat >conf/.env <:22?" 39 | SSH_KEY_FILE: ${BITCART_SSH_KEY_FILE} 40 | SSH_AUTHORIZED_KEYS: ${BITCART_SSH_AUTHORIZED_KEYS} 41 | BASH_PROFILE_SCRIPT: ${BASH_PROFILE_SCRIPT} 42 | BITCART_BACKEND_PLUGINS_DIR: /plugins/backend 43 | BITCART_ADMIN_PLUGINS_DIR: /plugins/admin 44 | BITCART_STORE_PLUGINS_DIR: /plugins/store 45 | BITCART_DAEMON_PLUGINS_DIR: /plugins/daemon 46 | BITCART_DOCKER_PLUGINS_DIR: /plugins/docker 47 | BITCART_HOST: ${BITCART_HOST} 48 | BITCART_ADMIN_HOST: ${BITCART_ADMIN_HOST} 49 | BITCART_ADMIN_ROOTPATH: ${BITCART_ADMIN_ROOTPATH:-/} 50 | BITCART_REVERSEPROXY: ${BITCART_REVERSEPROXY:-nginx-https} 51 | BITCART_HTTPS_ENABLED: ${BITCART_HTTPS_ENABLED:-false} 52 | SENTRY_DSN: ${BITCART_SENTRY_DSN:-} 53 | BITCART_API_WORKERS: ${BITCART_API_WORKERS} 54 | extra_hosts: 55 | - "host.docker.internal:host-gateway" 56 | expose: 57 | - "8000" 58 | volumes: 59 | - "bitcart_datadir:/datadir" 60 | - "backup_datadir:/backups" 61 | - "./plugins/backend:/plugins/backend" 62 | - "./plugins/admin:/plugins/admin" 63 | - "./plugins/store:/plugins/store" 64 | - "./plugins/daemon:/plugins/daemon" 65 | - "./plugins/docker:/plugins/docker" 66 | - "$?:${BITCART_SSH_AUTHORIZED_KEYS}" 67 | 68 | worker: 69 | depends_on: 70 | - backend 71 | links: 72 | - backend 73 | 74 | volumes: 75 | bitcart_datadir: 76 | backup_datadir: 77 | external: true 78 | -------------------------------------------------------------------------------- /generator/docker-components/binancecoin.yml: -------------------------------------------------------------------------------- 1 | services: 2 | binancecoin: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-bnb:stable 5 | environment: 6 | BNB_NETWORK: ${BNB_NETWORK:-mainnet} 7 | BNB_SERVER: ${BNB_SERVER:-https://seed-server.bitcart.ai} 8 | BNB_SEED_SERVER: ${BNB_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | BNB_SEED_SERVER_REFRESH_INTERVAL: ${BNB_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | BNB_ARCHIVE_SERVER: ${BNB_ARCHIVE_SERVER:-} 11 | BNB_DEBUG: ${BNB_DEBUG:-false} 12 | expose: 13 | - "5006" 14 | volumes: 15 | - "binancecoin_datadir:/data" 16 | 17 | backend: 18 | depends_on: 19 | - binancecoin 20 | links: 21 | - binancecoin 22 | 23 | volumes: 24 | binancecoin_datadir: 25 | -------------------------------------------------------------------------------- /generator/docker-components/bitcoin.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bitcoin: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-btc:stable 5 | environment: 6 | BTC_NETWORK: ${BTC_NETWORK:-mainnet} 7 | BTC_LIGHTNING: ${BTC_LIGHTNING:-false} 8 | BTC_LIGHTNING_GOSSIP: ${BTC_LIGHTNING_GOSSIP:-false} 9 | BTC_DEBUG: ${BTC_DEBUG:-false} 10 | expose: 11 | - "5000" 12 | volumes: 13 | - "bitcoin_datadir:/data" 14 | 15 | backend: 16 | depends_on: 17 | - bitcoin 18 | links: 19 | - bitcoin 20 | 21 | volumes: 22 | bitcoin_datadir: 23 | -------------------------------------------------------------------------------- /generator/docker-components/bitcoincash.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bitcoincash: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-bch:stable 5 | environment: 6 | BCH_NETWORK: ${BCH_NETWORK:-mainnet} 7 | BCH_DEBUG: ${BCH_DEBUG:-false} 8 | expose: 9 | - "5004" 10 | volumes: 11 | - "bitcoincash_datadir:/data" 12 | 13 | backend: 14 | depends_on: 15 | - bitcoincash 16 | links: 17 | - bitcoincash 18 | 19 | volumes: 20 | bitcoincash_datadir: 21 | -------------------------------------------------------------------------------- /generator/docker-components/cloudflare-tunnel.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cloudflared: 3 | image: bitcart/cloudflared:2022.7.1 4 | command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN} 5 | restart: on-failure 6 | depends_on: 7 | - nginx 8 | links: 9 | - nginx 10 | -------------------------------------------------------------------------------- /generator/docker-components/ergon.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ergon: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-xrg:stable 5 | environment: 6 | XRG_NETWORK: ${XRG_NETWORK:-mainnet} 7 | XRG_DEBUG: ${XRG_DEBUG:-false} 8 | expose: 9 | - "5005" 10 | volumes: 11 | - "ergon_datadir:/data" 12 | 13 | backend: 14 | depends_on: 15 | - ergon 16 | links: 17 | - ergon 18 | 19 | volumes: 20 | ergon_datadir: 21 | -------------------------------------------------------------------------------- /generator/docker-components/ethereum.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ethereum: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-eth:stable 5 | environment: 6 | ETH_NETWORK: ${ETH_NETWORK:-mainnet} 7 | ETH_SERVER: ${ETH_SERVER:-https://seed-server.bitcart.ai} 8 | ETH_SEED_SERVER: ${ETH_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | ETH_SEED_SERVER_REFRESH_INTERVAL: ${ETH_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | ETH_ARCHIVE_SERVER: ${ETH_ARCHIVE_SERVER:-} 11 | ETH_DEBUG: ${ETH_DEBUG:-false} 12 | expose: 13 | - "5002" 14 | volumes: 15 | - "ethereum_datadir:/data" 16 | 17 | backend: 18 | depends_on: 19 | - ethereum 20 | links: 21 | - ethereum 22 | 23 | volumes: 24 | ethereum_datadir: 25 | -------------------------------------------------------------------------------- /generator/docker-components/globalboost.yml: -------------------------------------------------------------------------------- 1 | services: 2 | globalboost: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-bsty:stable 5 | environment: 6 | BSTY_NETWORK: ${BSTY_NETWORK:-mainnet} 7 | BSTY_LIGHTNING: ${BSTY_LIGHTNING:-false} 8 | BSTY_LIGHTNING_GOSSIP: ${BSTY_LIGHTNING_GOSSIP:-false} 9 | BSTY_DEBUG: ${BSTY_DEBUG:-false} 10 | expose: 11 | - "5003" 12 | volumes: 13 | - "globalboost_datadir:/data" 14 | 15 | backend: 16 | depends_on: 17 | - globalboost 18 | links: 19 | - globalboost 20 | 21 | volumes: 22 | globalboost_datadir: 23 | -------------------------------------------------------------------------------- /generator/docker-components/groestlcoin.yml: -------------------------------------------------------------------------------- 1 | services: 2 | groestlcoin: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-grs:stable 5 | environment: 6 | GRS_NETWORK: ${GRS_NETWORK:-mainnet} 7 | GRS_LIGHTNING: ${GRS_LIGHTNING:-false} 8 | GRS_LIGHTNING_GOSSIP: ${GRS_LIGHTNING_GOSSIP:-false} 9 | GRS_DEBUG: ${GRS_DEBUG:-false} 10 | expose: 11 | - "5010" 12 | volumes: 13 | - "groestlcoin_datadir:/data" 14 | 15 | backend: 16 | depends_on: 17 | - groestlcoin 18 | links: 19 | - groestlcoin 20 | 21 | volumes: 22 | groestlcoin_datadir: 23 | -------------------------------------------------------------------------------- /generator/docker-components/litecoin.yml: -------------------------------------------------------------------------------- 1 | services: 2 | litecoin: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-ltc:stable 5 | environment: 6 | LTC_NETWORK: ${LTC_NETWORK:-mainnet} 7 | LTC_LIGHTNING: ${LTC_LIGHTNING:-false} 8 | LTC_LIGHTNING_GOSSIP: ${LTC_LIGHTNING_GOSSIP:-false} 9 | LTC_DEBUG: ${LTC_DEBUG:-false} 10 | expose: 11 | - "5001" 12 | volumes: 13 | - "litecoin_datadir:/data" 14 | 15 | backend: 16 | depends_on: 17 | - litecoin 18 | links: 19 | - litecoin 20 | 21 | volumes: 22 | litecoin_datadir: 23 | -------------------------------------------------------------------------------- /generator/docker-components/monero.yml: -------------------------------------------------------------------------------- 1 | services: 2 | monero: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-xmr:stable 5 | environment: 6 | XMR_NETWORK: ${XMR_NETWORK:-mainnet} 7 | XMR_SERVER: ${XMR_SERVER:-https://seed-server.bitcart.ai} 8 | XMR_SEED_SERVER: ${XMR_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | XMR_SEED_SERVER_REFRESH_INTERVAL: ${XMR_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | XMR_DEBUG: ${XMR_DEBUG:-false} 11 | expose: 12 | - "5011" 13 | volumes: 14 | - "monero_datadir:/data" 15 | 16 | backend: 17 | depends_on: 18 | - monero 19 | links: 20 | - monero 21 | 22 | volumes: 23 | monero_datadir: 24 | -------------------------------------------------------------------------------- /generator/docker-components/nginx-https.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx-https: 3 | restart: unless-stopped 4 | image: nginxproxy/acme-companion:2.1 5 | volumes: 6 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 7 | - "nginx_conf:/etc/nginx/conf.d" 8 | - "nginx_vhost:/etc/nginx/vhost.d" 9 | - "nginx_html:/usr/share/nginx/html" 10 | - "acme:/etc/acme.sh" 11 | - "nginx_certs:/etc/nginx/certs:rw" 12 | environment: 13 | NGINX_DOCKER_GEN_CONTAINER: "$?-nginx-gen-1" 14 | NGINX_PROXY_CONTAINER: "$?-nginx-1" 15 | ACME_CA_URI: ${ACME_CA_URI:-https://acme-v02.api.letsencrypt.org/directory} 16 | links: 17 | - nginx-gen 18 | 19 | volumes: 20 | acme: 21 | -------------------------------------------------------------------------------- /generator/docker-components/nginx.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | restart: unless-stopped 4 | image: nginx:stable 5 | ports: 6 | - "${REVERSEPROXY_HTTP_PORT:-80}:80" 7 | - "${REVERSEPROXY_HTTPS_PORT:-443}:443" 8 | volumes: 9 | - "nginx_conf:/etc/nginx/conf.d" 10 | - "nginx_vhost:/etc/nginx/vhost.d" 11 | - "nginx_html:/usr/share/nginx/html" 12 | - "nginx_certs:/etc/nginx/certs:ro" 13 | 14 | nginx-gen: 15 | restart: unless-stopped 16 | image: bitcart/docker-gen:0.9.2 17 | environment: 18 | DEFAULT_HOST: ${REVERSEPROXY_DEFAULT_HOST:-none} 19 | volumes: 20 | - "/var/run/docker.sock:/tmp/docker.sock:ro" 21 | - "./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro" 22 | - "nginx_conf:/etc/nginx/conf.d" 23 | - "nginx_vhost:/etc/nginx/vhost.d" 24 | - "nginx_html:/usr/share/nginx/html" 25 | - "nginx_certs:/etc/nginx/certs:ro" 26 | entrypoint: /usr/local/bin/docker-gen -filter-network $?_default -notify-sighup $?-nginx-1 -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf 27 | links: 28 | - nginx 29 | 30 | volumes: 31 | nginx_conf: 32 | nginx_vhost: 33 | nginx_html: 34 | nginx_certs: 35 | -------------------------------------------------------------------------------- /generator/docker-components/pihole.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pihole: 3 | restart: unless-stopped 4 | container_name: pihole 5 | image: pihole/pihole:latest 6 | environment: 7 | ServerIP: "${PIHOLE_SERVERIP:-127.0.0.1}" 8 | VIRTUAL_HOST: pi.hole 9 | VIRTUAL_HOST_NAME: "pihole" 10 | expose: 11 | - "53" 12 | - "67" 13 | - "80" 14 | - "443" 15 | ports: 16 | - "53:53/udp" 17 | volumes: 18 | - "./pihole/resolv.conf:/etc/resolv.conf:ro" 19 | - "pihole_datadir:/etc/pihole" 20 | volumes: 21 | pihole_datadir: 22 | -------------------------------------------------------------------------------- /generator/docker-components/polygon.yml: -------------------------------------------------------------------------------- 1 | services: 2 | polygon: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-matic:stable 5 | environment: 6 | MATIC_NETWORK: ${MATIC_NETWORK:-mainnet} 7 | MATIC_SERVER: ${MATIC_SERVER:-https://seed-server.bitcart.ai} 8 | MATIC_SEED_SERVER: ${MATIC_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | MATIC_SEED_SERVER_REFRESH_INTERVAL: ${MATIC_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | MATIC_ARCHIVE_SERVER: ${MATIC_ARCHIVE_SERVER:-} 11 | MATIC_DEBUG: ${MATIC_DEBUG:-false} 12 | expose: 13 | - "5008" 14 | volumes: 15 | - "polygon_datadir:/data" 16 | 17 | backend: 18 | depends_on: 19 | - polygon 20 | links: 21 | - polygon 22 | 23 | volumes: 24 | polygon_datadir: 25 | -------------------------------------------------------------------------------- /generator/docker-components/postgres.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | restart: unless-stopped 4 | image: pgautoupgrade/pgautoupgrade:17-alpine 5 | command: ["-c", "random_page_cost=1.0"] 6 | environment: 7 | POSTGRES_USER: postgres 8 | POSTGRES_DB: bitcart 9 | POSTGRES_HOST_AUTH_METHOD: trust 10 | volumes: 11 | - dbdata:/var/lib/postgresql/data 12 | expose: 13 | - "5432" 14 | backend: 15 | depends_on: 16 | - database 17 | links: 18 | - database 19 | 20 | volumes: 21 | dbdata: 22 | -------------------------------------------------------------------------------- /generator/docker-components/redis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | restart: unless-stopped 4 | image: redis:alpine 5 | expose: 6 | - "6379" 7 | backend: 8 | depends_on: 9 | - redis 10 | links: 11 | - redis 12 | -------------------------------------------------------------------------------- /generator/docker-components/smartbch.yml: -------------------------------------------------------------------------------- 1 | services: 2 | smartbch: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-sbch:stable 5 | environment: 6 | SBCH_NETWORK: ${SBCH_NETWORK:-mainnet} 7 | SBCH_SERVER: ${SBCH_SERVER:-https://seed-server.bitcart.ai} 8 | SBCH_SEED_SERVER: ${SBCH_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | SBCH_SEED_SERVER_REFRESH_INTERVAL: ${SBCH_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | SBCH_DEBUG: ${SBCH_DEBUG:-false} 11 | expose: 12 | - "5007" 13 | volumes: 14 | - "smartbch_datadir:/data" 15 | 16 | backend: 17 | depends_on: 18 | - smartbch 19 | links: 20 | - smartbch 21 | 22 | volumes: 23 | smartbch_datadir: 24 | -------------------------------------------------------------------------------- /generator/docker-components/store.yml: -------------------------------------------------------------------------------- 1 | services: 2 | store: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-store:stable 5 | expose: 6 | - "3000" 7 | command: yarn start 8 | environment: 9 | BITCART_STORE_API_URL: ${BITCART_STORE_API_URL} 10 | VIRTUAL_NETWORK: nginx-proxy 11 | VIRTUAL_PORT: 3000 12 | VIRTUAL_HOST: ${BITCART_STORE_HOST} 13 | VIRTUAL_HOST_NAME: "bitcart-store" 14 | LETSENCRYPT_HOST: ${BITCART_STORE_HOST} 15 | LETSENCRYPT_EMAIL: ${BITCART_LETSENCRYPT_EMAIL} 16 | BITCART_STORE_ROOTPATH: ${BITCART_STORE_ROOTPATH:-/} 17 | BITCART_ADMIN_HOST: ${BITCART_ADMIN_HOST} 18 | BITCART_ADMIN_ROOTPATH: ${BITCART_ADMIN_ROOTPATH:-/} 19 | -------------------------------------------------------------------------------- /generator/docker-components/tor-relay.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tor-relay-gen: 3 | restart: unless-stopped 4 | image: bitcart/docker-gen:0.9.2 5 | volumes: 6 | - "/var/run/docker.sock:/tmp/docker.sock:ro" 7 | - "./torrc-relay.tmpl:/etc/docker-gen/templates/torrc.tmpl:ro" 8 | - "tor_relay_torrcdir:/usr/local/etc/tor" 9 | entrypoint: /usr/local/bin/docker-gen -filter-network $?_default -notify-sighup $?-tor-relay-1 -watch -wait 5s:30s /etc/docker-gen/templates/torrc.tmpl /usr/local/etc/tor/torrc-2 10 | links: 11 | - tor-relay 12 | environment: 13 | TOR_RELAY_NICKNAME: ${TOR_RELAY_NICKNAME} 14 | TOR_RELAY_EMAIL: ${TOR_RELAY_EMAIL} 15 | 16 | tor-relay: 17 | restart: unless-stopped 18 | image: bitcart/tor:0.4.7.13 19 | environment: 20 | TOR_PASSWORD: bitcart 21 | TOR_ADDITIONAL_CONFIG: /usr/local/etc/tor/torrc-2 22 | volumes: 23 | - "tor_relay_datadir:/home/tor/.tor" 24 | - "tor_relay_torrcdir:/usr/local/etc/tor" 25 | ports: 26 | - "9001:9001" 27 | - "9030:9030" 28 | volumes: 29 | tor_relay_datadir: 30 | tor_relay_torrcdir: 31 | -------------------------------------------------------------------------------- /generator/docker-components/tor.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | environment: 4 | HIDDENSERVICE_NAME: Bitcart-Merchants-API 5 | TORRC_FILE: /usr/local/etc/tor/torrc-2 6 | volumes: 7 | - "tor_servicesdir:/var/lib/tor/hidden_services" 8 | - "tor_torrcdir:/usr/local/etc/tor/" 9 | 10 | worker: 11 | environment: 12 | TORRC_FILE: /usr/local/etc/tor/torrc-2 13 | volumes: 14 | - "tor_servicesdir:/var/lib/tor/hidden_services" 15 | - "tor_torrcdir:/usr/local/etc/tor/" 16 | 17 | admin: 18 | environment: 19 | HIDDENSERVICE_NAME: Bitcart-Admin-Panel 20 | BITCART_ADMIN_SOCKS_PROXY: socks5h://tor:9050 21 | volumes: 22 | - "tor_servicesdir:/var/lib/tor/hidden_services" 23 | 24 | store: 25 | environment: 26 | HIDDENSERVICE_NAME: Bitcart-Store 27 | BITCART_STORE_SOCKS_PROXY: socks5h://tor:9050 28 | volumes: 29 | - "tor_servicesdir:/var/lib/tor/hidden_services" 30 | 31 | nginx: 32 | volumes: 33 | - "tor_servicesdir:/var/lib/tor/hidden_services" 34 | nginx-gen: 35 | volumes: 36 | - "tor_servicesdir:/var/lib/tor/hidden_services" 37 | 38 | tor: 39 | restart: unless-stopped 40 | image: bitcart/tor:0.4.7.13 41 | environment: 42 | TOR_PASSWORD: bitcart 43 | TOR_ADDITIONAL_CONFIG: /usr/local/etc/tor/torrc-2 44 | TOR_EXTRA_ARGS: | 45 | CookieAuthentication 1 46 | DataDirectoryGroupReadable 1 47 | expose: 48 | - "9050" # SOCKS 49 | - "9051" # Tor Control 50 | volumes: 51 | - "tor_datadir:/home/tor/.tor" 52 | - "tor_torrcdir:/usr/local/etc/tor" 53 | - "tor_servicesdir:/var/lib/tor/hidden_services" 54 | 55 | tor-gen: 56 | restart: unless-stopped 57 | image: bitcart/docker-gen:0.9.2 58 | volumes: 59 | - "/var/run/docker.sock:/tmp/docker.sock:ro" 60 | - "./torrc.tmpl:/etc/docker-gen/templates/torrc.tmpl:ro" 61 | - "tor_torrcdir:/usr/local/etc/tor" 62 | entrypoint: /usr/local/bin/docker-gen -filter-network $?_default -notify-sighup $?-tor-1 -watch -wait 5s:30s /etc/docker-gen/templates/torrc.tmpl /usr/local/etc/tor/torrc-2 63 | links: 64 | - tor 65 | 66 | volumes: 67 | tor_datadir: 68 | tor_torrcdir: 69 | tor_servicesdir: 70 | -------------------------------------------------------------------------------- /generator/docker-components/tron.yml: -------------------------------------------------------------------------------- 1 | services: 2 | tron: 3 | restart: unless-stopped 4 | image: bitcart/bitcart-trx:stable 5 | environment: 6 | TRX_NETWORK: ${TRX_NETWORK:-mainnet} 7 | TRX_SERVER: ${TRX_SERVER:-https://seed-server.bitcart.ai} 8 | TRX_SEED_SERVER: ${TRX_SEED_SERVER:-https://seed-server.bitcart.ai} 9 | TRX_SEED_SERVER_REFRESH_INTERVAL: ${TRX_SEED_SERVER_REFRESH_INTERVAL:-3600} 10 | TRX_DEBUG: ${TRX_DEBUG:-false} 11 | expose: 12 | - "5009" 13 | volumes: 14 | - "tron_datadir:/data" 15 | 16 | backend: 17 | depends_on: 18 | - tron 19 | links: 20 | - tron 21 | 22 | volumes: 23 | tron_datadir: 24 | -------------------------------------------------------------------------------- /generator/docker-components/worker.yml: -------------------------------------------------------------------------------- 1 | services: 2 | worker: 3 | restart: unless-stopped 4 | image: bitcart/bitcart:stable 5 | command: python3 worker.py 6 | environment: 7 | LOG_FILE: bitcart.log 8 | BITCART_DATADIR: /datadir 9 | BITCART_BACKUPS_DIR: /backups 10 | BITCART_VOLUMES: /datadir /backups /plugins 11 | BITCART_CRYPTOS: ${BITCART_CRYPTOS:-btc} 12 | BTC_NETWORK: ${BTC_NETWORK:-mainnet} 13 | BTC_LIGHTNING: ${BTC_LIGHTNING:-false} 14 | BCH_NETWORK: ${BCH_NETWORK:-mainnet} 15 | ETH_NETWORK: ${ETH_NETWORK:-mainnet} 16 | BNB_NETWORK: ${BNB_NETWORK:-mainnet} 17 | SBCH_NETWORK: ${SBCH_NETWORK:-mainnet} 18 | MATIC_NETWORK: ${MATIC_NETWORK:-mainnet} 19 | TRX_NETWORK: ${TRX_NETWORK:-mainnet} 20 | XRG_NETWORK: ${XRG_NETWORK:-mainnet} 21 | LTC_NETWORK: ${LTC_NETWORK:-mainnet} 22 | LTC_LIGHTNING: ${LTC_LIGHTNING:-false} 23 | BSTY_NETWORK: ${BSTY_NETWORK:-mainnet} 24 | BSTY_LIGHTNING: ${BSTY_LIGHTNING:-false} 25 | GRS_NETWORK: ${GRS_NETWORK:-mainnet} 26 | GRS_LIGHTNING: ${GRS_LIGHTNING:-false} 27 | XMR_NETWORK: ${XMR_NETWORK:-mainnet} 28 | UPDATE_URL: ${BITCART_UPDATE_URL:-https://api.bitcart.ai/updates/latest} 29 | SSH_CONNECTION: "root@host.docker.internal:$:22?" 30 | SSH_KEY_FILE: ${BITCART_SSH_KEY_FILE} 31 | SSH_AUTHORIZED_KEYS: ${BITCART_SSH_AUTHORIZED_KEYS} 32 | BASH_PROFILE_SCRIPT: ${BASH_PROFILE_SCRIPT} 33 | BITCART_BACKEND_PLUGINS_DIR: /plugins/backend 34 | BITCART_ADMIN_PLUGINS_DIR: /plugins/admin 35 | BITCART_STORE_PLUGINS_DIR: /plugins/store 36 | BITCART_DAEMON_PLUGINS_DIR: /plugins/daemon 37 | BITCART_DOCKER_PLUGINS_DIR: /plugins/docker 38 | BITCART_HOST: ${BITCART_HOST} 39 | BITCART_ADMIN_HOST: ${BITCART_ADMIN_HOST} 40 | BITCART_ADMIN_ROOTPATH: ${BITCART_ADMIN_ROOTPATH:-/} 41 | BITCART_REVERSEPROXY: ${BITCART_REVERSEPROXY:-nginx-https} 42 | BITCART_HTTPS_ENABLED: ${BITCART_HTTPS_ENABLED:-false} 43 | SENTRY_DSN: ${BITCART_SENTRY_DSN:-} 44 | extra_hosts: 45 | - "host.docker.internal:host-gateway" 46 | expose: 47 | - "9020" 48 | volumes: 49 | - "bitcart_datadir:/datadir" 50 | - "backup_datadir:/backups" 51 | - "./plugins/backend:/plugins/backend" 52 | - "./plugins/admin:/plugins/admin" 53 | - "./plugins/store:/plugins/store" 54 | - "./plugins/daemon:/plugins/daemon" 55 | - "./plugins/docker:/plugins/docker" 56 | - "$?:${BITCART_SSH_AUTHORIZED_KEYS}" 57 | - /var/lib/dbus/machine-id:/var/lib/dbus/machine-id # for paid plugins 58 | 59 | volumes: 60 | bitcart_datadir: 61 | backup_datadir: 62 | external: true 63 | -------------------------------------------------------------------------------- /generator/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConfigError(Exception): 2 | """Invalid configuraton""" 3 | -------------------------------------------------------------------------------- /generator/generator.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import importlib 3 | import importlib.util 4 | import os 5 | import sys 6 | from collections import UserDict 7 | from os.path import basename, exists, isfile 8 | from os.path import join as path_join 9 | 10 | import oyaml as yaml 11 | 12 | from generator.constants import ( 13 | BACKEND_COMPONENTS, 14 | COMPONENTS_DIR, 15 | CRYPTO_COMPONENTS, 16 | CRYPTOS, 17 | FRONTEND_COMPONENTS, 18 | GENERATED_PATH, 19 | PLUGINS_DIR, 20 | RULES_DIR, 21 | RULES_PYTHON_DIR, 22 | RULES_PYTHON_PKG, 23 | ) 24 | from generator.settings import Settings 25 | from generator.utils import ConfigError 26 | 27 | AUTOGENERATED_COMMENT = """# NOTE: THIS FILE IS GENERATED VIA ./build.sh 28 | # DO NOT EDIT THIS FILE DIRECTLY UNLESS REQUESTED 29 | # Use BITCART_ADDITIONAL_COMPONENTS and set BITCARTGEN_DOCKER_IMAGE to bitcart/docker-compose-generator:local 30 | # to load custom .yml components you add in generator/docker-components directory 31 | """ 32 | 33 | 34 | class OrderedSet(UserDict): 35 | def add(self, v): 36 | self.data[v] = None 37 | 38 | def remove_key(self, v): 39 | self.data.pop(v, None) 40 | 41 | def update(self, *args, **kwargs): 42 | for s in args: 43 | for e in s: 44 | self.add(e) 45 | 46 | def remove(self, *args, **kwargs): 47 | for s in args: 48 | for e in s: 49 | self.remove_key(e) 50 | 51 | def __repr__(self): 52 | return f"{{{', '.join(map(repr, self.data.keys()))}}}" 53 | 54 | 55 | def get_plugin_components(): # pragma: no cover 56 | components = OrderedSet() 57 | if not os.path.exists(PLUGINS_DIR): 58 | return components 59 | for plugin in os.listdir(PLUGINS_DIR): 60 | components_dir = path_join(PLUGINS_DIR, plugin, "components") 61 | if not os.path.exists(components_dir): 62 | continue 63 | for component in os.listdir(components_dir): 64 | if component.endswith(".yml"): 65 | components.add(path_join(components_dir, component)) 66 | return components 67 | 68 | 69 | def add_components(settings: Settings) -> OrderedSet: 70 | components = OrderedSet() 71 | # add daemons 72 | for crypto in settings.CRYPTOS: 73 | if crypto: 74 | value = CRYPTOS.get(crypto) 75 | if value: 76 | components.add(value["component"]) 77 | # installation packs 78 | if settings.INSTALLATION_PACK == "all": 79 | components.update(BACKEND_COMPONENTS + FRONTEND_COMPONENTS) 80 | elif settings.INSTALLATION_PACK == "backend": 81 | components.update(BACKEND_COMPONENTS) 82 | elif settings.INSTALLATION_PACK == "frontend": 83 | components.update(FRONTEND_COMPONENTS) 84 | # reverse proxy 85 | if settings.REVERSE_PROXY == "nginx-https": 86 | components.update(["nginx", "nginx-https"]) 87 | elif settings.REVERSE_PROXY == "nginx": 88 | components.update(["nginx"]) 89 | # additional components 90 | components.update(settings.ADDITIONAL_COMPONENTS) 91 | components.remove(settings.EXCLUDE_COMPONENTS) 92 | # Add bitcoin if no valid cryptos specified 93 | HAS_CRYPTO = False 94 | for i in components: 95 | if i in CRYPTO_COMPONENTS: 96 | HAS_CRYPTO = True 97 | break 98 | if not HAS_CRYPTO: 99 | components.add(CRYPTOS["btc"]["component"]) 100 | components.update(get_plugin_components()) 101 | return components 102 | 103 | 104 | def load_component(component: str): 105 | path = component if os.path.isabs(component) else path_join(COMPONENTS_DIR, component + ".yml") 106 | if not exists(path): 107 | return {} 108 | with open(path) as f: 109 | data = yaml.load(f, Loader=yaml.SafeLoader) 110 | return data or {} 111 | 112 | 113 | def _merge(a, b, path=None): 114 | if path is None: 115 | path = [] 116 | for key in b: 117 | if key in a: 118 | if isinstance(a[key], dict) and isinstance(b[key], dict): 119 | _merge(a[key], b[key], path + [str(key)]) 120 | elif isinstance(a[key], list) and isinstance(b[key], list): 121 | a[key] += b[key] 122 | else: 123 | a[key] = b[key] 124 | return a 125 | 126 | 127 | def merge(services): 128 | new = [] 129 | for i in services: 130 | for j in i: 131 | new.append({j: i[j]}) 132 | d = {} 133 | new = sorted(new, key=lambda x: list(x)[0]) 134 | for i in new: 135 | key = list(i)[0] 136 | try: 137 | _merge(d[key], i[key]) 138 | except KeyError: 139 | d[key] = i[key] 140 | return d 141 | 142 | 143 | def load_module_by_path(plugin, rule, path): # pragma: no cover 144 | module_name = f"plugins.{plugin}.{rule}" 145 | spec = importlib.util.spec_from_file_location(module_name, path) 146 | module = importlib.util.module_from_spec(spec) 147 | sys.modules[module_name] = module 148 | spec.loader.exec_module(module) 149 | return module 150 | 151 | 152 | def get_plugin_rules(): # pragma: no cover 153 | rules = [] 154 | if not os.path.exists(PLUGINS_DIR): 155 | return rules 156 | for plugin in os.listdir(PLUGINS_DIR): 157 | rules_dir = path_join(PLUGINS_DIR, plugin, "rules") 158 | if not os.path.exists(rules_dir): 159 | continue 160 | for path in sorted(glob.glob(path_join(rules_dir, "*.py"))): 161 | if isfile(path): 162 | rules.append(load_module_by_path(plugin, path, path)) 163 | return rules 164 | 165 | 166 | def load_rules(): 167 | modules = sorted(glob.glob(path_join(RULES_DIR, "*.py"))) 168 | loaded = [ 169 | importlib.import_module(f"{RULES_PYTHON_DIR}." + basename(f)[:-3], RULES_PYTHON_PKG) for f in modules if isfile(f) 170 | ] 171 | loaded.extend(get_plugin_rules()) 172 | for i in loaded.copy(): 173 | if not getattr(i, "rule", None) or not callable(i.rule): 174 | loaded.remove(i) 175 | return loaded 176 | 177 | 178 | def execute_rules(rules, services, settings): 179 | for i in rules: 180 | i.rule(services, settings) 181 | 182 | 183 | def generate(components: OrderedSet, settings: Settings): 184 | # generated yaml 185 | services: dict | list = [] 186 | networks: dict | list = [] 187 | volumes: dict | list = [] 188 | for i in components: 189 | doc = load_component(i) 190 | if doc.get("services"): 191 | services.append(doc["services"]) 192 | if doc.get("networks"): # pragma: no cover 193 | networks.append(doc["networks"]) 194 | if doc.get("volumes"): 195 | volumes.append(doc["volumes"]) 196 | services = merge(services) 197 | rules = load_rules() 198 | execute_rules(rules, services, settings) 199 | networks = {j: i[j] for i in networks for j in i} 200 | volumes = {j: i[j] for i in volumes for j in i} 201 | return { 202 | "services": services, 203 | "networks": networks, 204 | "volumes": volumes, 205 | } 206 | 207 | 208 | def save(data, out_path=GENERATED_PATH): 209 | with open(out_path, "w") as f: 210 | f.write(AUTOGENERATED_COMMENT) 211 | yaml.dump(data, f, default_flow_style=False) 212 | 213 | 214 | def generate_config(): 215 | settings = Settings() 216 | return generate(add_components(settings), settings) 217 | 218 | 219 | def get_components_list(): 220 | settings = Settings() 221 | return list(add_components(settings)) 222 | 223 | 224 | def get_cryptos_list(): # pragma: no cover 225 | def get_crypto(crypto): 226 | for coin in CRYPTOS: 227 | if CRYPTOS[coin]["component"] == crypto: 228 | return coin.lower() 229 | return False 230 | 231 | return [coin for component in get_components_list() if (coin := get_crypto(component))] 232 | 233 | 234 | def main(): # pragma: no cover 235 | try: 236 | if len(sys.argv) == 2 and sys.argv[1] == "--components-only": 237 | print(" ".join(get_components_list())) 238 | return 239 | if len(sys.argv) == 2 and sys.argv[1] == "--cryptos-only": 240 | print(" ".join(get_cryptos_list())) 241 | return 242 | save(generate_config()) 243 | except ConfigError as e: 244 | sys.exit(str(e)) 245 | 246 | 247 | if __name__ == "__main__": # pragma: no cover 248 | main() 249 | -------------------------------------------------------------------------------- /generator/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "bitcart-compose-generator" 3 | version = "1.0.0" 4 | description = "Generate docker compose files based on env variables" 5 | requires-python = ">=3.11" 6 | dependencies = ["oyaml"] 7 | 8 | [dependency-groups] 9 | dev = ["pre-commit", "pytest", "pytest-cov", "ruff"] 10 | 11 | [tool.ruff] 12 | target-version = "py311" 13 | line-length = 127 14 | 15 | [tool.ruff.lint] 16 | select = [ 17 | "F", 18 | "E", 19 | "I", 20 | "UP", 21 | "YTT", 22 | "B", 23 | "T10", 24 | "C", 25 | "SIM", 26 | "RUF100", 27 | "RET", 28 | "A", 29 | "S", 30 | "ASYNC", 31 | ] 32 | ignore = ["RET502", "RET503", "S104", "S507", "ASYNC110"] 33 | mccabe = { max-complexity = 12 } 34 | 35 | [tool.ruff.lint.per-file-ignores] 36 | "tests/*" = ["S"] 37 | 38 | [tool.pytest.ini_options] 39 | addopts = ["--cov=.", "--cov-report", "term-missing"] 40 | filterwarnings = [ 41 | "error::DeprecationWarning", 42 | "error::PendingDeprecationWarning", 43 | ] 44 | -------------------------------------------------------------------------------- /generator/requirements.txt: -------------------------------------------------------------------------------- 1 | oyaml 2 | -------------------------------------------------------------------------------- /generator/rules/01_no_image.py: -------------------------------------------------------------------------------- 1 | def rule(services, settings): 2 | for i in services.copy(): 3 | item = services[i] 4 | if not item.get("image") and not item.get("build"): 5 | del services[i] 6 | -------------------------------------------------------------------------------- /generator/rules/02_one_domain.py: -------------------------------------------------------------------------------- 1 | from generator.constants import HOST_COMPONENTS 2 | from generator.utils import modify_key 3 | from generator.utils import preferred_service as get_pref 4 | 5 | 6 | def rule(services, settings): 7 | if not settings.ONE_DOMAIN_MODE: 8 | return 9 | for service in HOST_COMPONENTS: 10 | if services.get(service): 11 | with modify_key(services, service, "environment") as environment: 12 | del environment["VIRTUAL_HOST"] 13 | environment["ONE_DOMAIN_MODE"] = "true" # strings in yaml 14 | preferred_service = get_pref(services) 15 | if preferred_service: 16 | with modify_key(services, preferred_service, "environment") as environment: 17 | environment["VIRTUAL_HOST"] = settings.HOST 18 | -------------------------------------------------------------------------------- /generator/rules/03_root_path.py: -------------------------------------------------------------------------------- 1 | from os.path import join as urljoin 2 | 3 | from generator.utils import modify_key 4 | 5 | 6 | def rule(services, settings): 7 | if not settings.ONE_DOMAIN_MODE: 8 | return 9 | API_URL = urljoin(settings.API_URL, "api") 10 | STORE_AVAILABLE = services.get("store") 11 | ADMIN_AVAILABLE = services.get("admin") 12 | BACKEND_AVAILABLE = services.get("backend") 13 | # replace defaults 14 | if STORE_AVAILABLE: 15 | with modify_key(services, "store", "environment") as environment: 16 | environment["BITCART_STORE_API_URL"] = API_URL 17 | if ADMIN_AVAILABLE: 18 | with modify_key(services, "admin", "environment") as environment: 19 | environment["BITCART_ADMIN_ROOTPATH"] = environment["BITCART_ADMIN_ROOTPATH"].replace("/", "/admin") 20 | environment["BITCART_ADMIN_API_URL"] = API_URL 21 | environment["BITCART_STORE_HOST"] = settings.HOST or "" 22 | with modify_key(services, "store", "environment") as environment: 23 | environment["BITCART_ADMIN_HOST"] = urljoin(settings.HOST or "", "admin") 24 | environment["BITCART_ADMIN_ROOTPATH"] = environment["BITCART_ADMIN_ROOTPATH"].replace("/", "/admin") 25 | elif ADMIN_AVAILABLE: 26 | with modify_key(services, "admin", "environment") as environment: 27 | environment["BITCART_ADMIN_API_URL"] = API_URL 28 | if BACKEND_AVAILABLE and (ADMIN_AVAILABLE or STORE_AVAILABLE): 29 | with modify_key(services, "backend", "environment") as environment: 30 | environment["BITCART_BACKEND_ROOTPATH"] = environment["BITCART_BACKEND_ROOTPATH"].replace("-}", "-/api}") 31 | if BACKEND_AVAILABLE and ADMIN_AVAILABLE: 32 | with modify_key(services, "backend", "environment") as environment: 33 | if STORE_AVAILABLE: 34 | environment["BITCART_ADMIN_HOST"] = urljoin(settings.HOST or "", "admin") 35 | environment["BITCART_ADMIN_ROOTPATH"] = environment["BITCART_ADMIN_ROOTPATH"].replace("/", "/admin") 36 | else: 37 | environment["BITCART_ADMIN_HOST"] = settings.HOST or "" 38 | -------------------------------------------------------------------------------- /generator/rules/04_no_reverseproxy.py: -------------------------------------------------------------------------------- 1 | from generator.constants import CRYPTO_COMPONENTS, HOST_COMPONENTS 2 | from generator.utils import custom_port_allowed, env, modify_key 3 | 4 | 5 | def rule(services, settings): 6 | no_nginx = not services.get("nginx") 7 | custom_web_services = [ 8 | key for key, service in services.items() if service.get("environment", {}).get("BITCART_WEBSERVICE", False) 9 | ] 10 | items = HOST_COMPONENTS + CRYPTO_COMPONENTS + custom_web_services 11 | for i in items: 12 | if services.get(i) and custom_port_allowed(i, no_nginx): 13 | with modify_key(services, i, "expose", [], "ports") as expose: 14 | custom_port = env(f"{i.upper()}_PORT") 15 | for key, port in enumerate(expose): 16 | expose[key] = f"{custom_port or port}:{port}" 17 | -------------------------------------------------------------------------------- /generator/rules/05_frontend.py: -------------------------------------------------------------------------------- 1 | def rule(services, settings): 2 | store = services.get("store") 3 | admin = services.get("admin") 4 | backend = services.get("backend") 5 | if backend: 6 | if store: 7 | store["links"] = ["backend"] 8 | if admin: 9 | admin["links"] = ["backend"] 10 | -------------------------------------------------------------------------------- /generator/rules/06_tor.py: -------------------------------------------------------------------------------- 1 | from generator.constants import HOST_COMPONENTS, TOR_CRYPTOS 2 | from generator.utils import modify_key 3 | 4 | 5 | def rule(services, settings): 6 | items = HOST_COMPONENTS 7 | has_nginx = services.get("nginx") 8 | if services.get("tor"): 9 | for i in items: 10 | if services.get(i): 11 | with modify_key(services, i, "environment") as environment: 12 | if has_nginx: 13 | environment["HIDDENSERVICE_REVERSEPROXY"] = "$?-nginx-1" 14 | else: 15 | environment["HIDDENSERVICE_IP"] = "172.17.0.1" 16 | environment["HIDDENSERVICE_VIRTUAL_PORT"] = int(environment.get("VIRTUAL_PORT", "80")) 17 | for env_name, service in TOR_CRYPTOS.items(): 18 | service_name = service["component"] 19 | if services.get(service_name): 20 | with modify_key(services, service_name, "environment") as environment: 21 | environment[f"{env_name.upper()}_PROXY_URL"] = "socks5://tor:9050" 22 | -------------------------------------------------------------------------------- /generator/rules/07_build_time_env.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from generator.constants import ENV_PREFIX 4 | from generator.utils import apply_recursive, env 5 | 6 | BUILD_TIME_ENV_REGEX = re.compile(r"\$<(.*?)>:?(.*?)\?") 7 | 8 | 9 | def apply_build_time_env(line): 10 | if not isinstance(line, str): 11 | return False, line 12 | 13 | to_delete = False 14 | 15 | def load_env_var(match): 16 | nonlocal to_delete 17 | env_name = match.group(1) 18 | default = match.group(2) 19 | if env_name == "DEPLOYENT_NAME": 20 | return env("NAME", "compose", prefix="") 21 | if env_name.startswith(ENV_PREFIX): 22 | env_name = env_name[len(ENV_PREFIX) :] 23 | value = env(env_name, default or "") 24 | if not value: 25 | to_delete = True 26 | return value 27 | 28 | line = re.sub(BUILD_TIME_ENV_REGEX, load_env_var, line) 29 | return to_delete, line 30 | 31 | 32 | def rule(services, settings): 33 | services.update(apply_recursive(services, apply_build_time_env)[1]) 34 | -------------------------------------------------------------------------------- /generator/rules/08_scale.py: -------------------------------------------------------------------------------- 1 | from generator.utils import env, modify_key 2 | 3 | 4 | def rule(services, settings): 5 | for i in services: 6 | try: 7 | scale = int(env(f"{i.upper()}_SCALE", "1")) 8 | except ValueError: 9 | continue 10 | if scale != 1: 11 | with modify_key(services, i, "deploy") as deploy: 12 | deploy["replicas"] = scale 13 | -------------------------------------------------------------------------------- /generator/rules/09_custom_version.py: -------------------------------------------------------------------------------- 1 | from generator.utils import env 2 | 3 | 4 | def rule(services, settings): 5 | version = env("VERSION", "stable") 6 | for i in services: 7 | if "image" in services[i]: 8 | parts = services[i]["image"].split(":") 9 | if len(parts) == 2 and parts[1] == "stable" and parts[0].startswith("bitcart/"): 10 | services[i]["image"] = parts[0] + f":{version}" 11 | -------------------------------------------------------------------------------- /generator/rules/10_local_rule.py: -------------------------------------------------------------------------------- 1 | from generator.constants import HOST_COMPONENTS 2 | from generator.utils import modify_key 3 | 4 | 5 | def rule(services, settings): 6 | if not settings.HOST or not settings.HOST.endswith(".local"): 7 | return 8 | custom_web_services = [ 9 | key for key, service in services.items() if service.get("environment", {}).get("BITCART_WEBSERVICE", False) 10 | ] 11 | items = HOST_COMPONENTS + custom_web_services 12 | for i in items: 13 | if services.get(i): 14 | with modify_key(services, i, "extra_hosts", []) as extra_hosts: 15 | extra_hosts.append(f"{settings.HOST}:172.17.0.1") 16 | -------------------------------------------------------------------------------- /generator/rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcart/bitcart-docker/c4aa7c3b0426fd62d8cbfd45f873de1cc0249b52/generator/rules/__init__.py -------------------------------------------------------------------------------- /generator/settings.py: -------------------------------------------------------------------------------- 1 | from shlex import shlex 2 | 3 | from generator.constants import ALL_REVERSE_PROXIES, AVAILABLE_SETTINGS, HTTPS_REVERSE_PROXIES 4 | from generator.utils import config_error, env 5 | 6 | 7 | class Settings: 8 | def __init__(self): 9 | self.settings = {} 10 | self.load_settings() 11 | 12 | def load_settings(self): 13 | for setting in AVAILABLE_SETTINGS: 14 | default = None 15 | if len(setting) == 3: 16 | name, env_name, default = setting 17 | elif len(setting) == 2: # pragma: no cover 18 | name, env_name = setting 19 | else: 20 | name = env_name = setting[0] 21 | self.settings[name] = env(env_name, default) 22 | self.add_custom_settings() 23 | self.apply_checks() 24 | 25 | def add_custom_settings(self): 26 | self.CRYPTOS = self.load_comma_separated("CRYPTOS", "btc") 27 | self.ADDITIONAL_COMPONENTS = self.load_comma_separated("ADDITIONAL_COMPONENTS") 28 | self.EXCLUDE_COMPONENTS = self.load_comma_separated("EXCLUDE_COMPONENTS") 29 | self.ONE_DOMAIN_MODE = ( 30 | self.REVERSE_PROXY in ALL_REVERSE_PROXIES 31 | and not self.ADMIN_HOST 32 | and not self.STORE_HOST 33 | and not self.ADMIN_API_URL 34 | and not self.STORE_API_URL 35 | ) 36 | self.PROTOCOL = "https" if self.HTTPS_ENABLED or self.REVERSE_PROXY in HTTPS_REVERSE_PROXIES else "http" 37 | self.DEFAULT_API_PORT = "443" if self.PROTOCOL == "https" else "80" 38 | self.API_PORT = env(f"REVERSEPROXY_{self.PROTOCOL.upper()}_PORT", self.DEFAULT_API_PORT, prefix="") 39 | port_suffix = f":{self.API_PORT}" if not self.BEHIND_REVERSE_PROXY and self.API_PORT != self.DEFAULT_API_PORT else "" 40 | self.API_URL = f"{self.PROTOCOL}://{self.HOST}{port_suffix}" 41 | 42 | def apply_checks(self): 43 | if self.ONE_DOMAIN_MODE and self.INSTALLATION_PACK == "frontend": 44 | config_error( 45 | "Frontend installation pack is enabled and no API URL set. " 46 | "Please set BITCART_ADMIN_API_URL and BITCART_STORE_API_URL." 47 | ) 48 | 49 | @staticmethod 50 | def load_comma_separated(name, default=""): 51 | value = env(name, default) 52 | splitter = shlex(value, posix=True) 53 | splitter.whitespace = "," 54 | splitter.whitespace_split = True 55 | return [item.strip() for item in splitter] 56 | 57 | def __getattr__(self, name): 58 | return self.settings.__getitem__(name) 59 | 60 | def __setattr__(self, name, value): 61 | if name.isupper(): 62 | self.settings.__setitem__(name, value) 63 | else: 64 | super().__setattr__(name, value) 65 | -------------------------------------------------------------------------------- /generator/test-requirements.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | pytest 3 | pytest-cov 4 | ruff 5 | -------------------------------------------------------------------------------- /generator/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcart/bitcart-docker/c4aa7c3b0426fd62d8cbfd45f873de1cc0249b52/generator/tests/__init__.py -------------------------------------------------------------------------------- /generator/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | from os.path import basename 4 | from os.path import join as path_join 5 | 6 | import pytest 7 | 8 | from generator import generate_config 9 | from generator.constants import COMPONENTS_DIR 10 | from generator.settings import Settings 11 | 12 | 13 | def pytest_generate_tests(metafunc): 14 | # clean up settings before a test session 15 | for key in filter(lambda env: env.startswith("BITCART_"), os.environ): 16 | del os.environ[key] 17 | 18 | 19 | @pytest.fixture(autouse=True, scope="session") 20 | def settings(): 21 | return Settings() 22 | 23 | 24 | @pytest.fixture(autouse=True, scope="session") 25 | def config(): 26 | return generate_config() 27 | 28 | 29 | def convert_component(component): 30 | return basename(component).replace(".yml", "") 31 | 32 | 33 | @pytest.fixture(autouse=True, scope="session") 34 | def all_components(): 35 | return sorted(map(convert_component, glob.glob(path_join(COMPONENTS_DIR, "*.yml")))) 36 | -------------------------------------------------------------------------------- /generator/tests/test_core.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | import yaml 6 | 7 | from generator.generator import AUTOGENERATED_COMMENT, add_components, get_components_list, load_component, save 8 | 9 | THIRD_PARTY_IMAGES = ["database", "nginx-https", "nginx", "redis", "pihole"] 10 | DEFAULT_SERVICES = [ 11 | "admin", 12 | "backend", 13 | "bitcoin", 14 | "database", 15 | "nginx-https", 16 | "nginx", 17 | "nginx-gen", 18 | "redis", 19 | "store", 20 | "worker", 21 | ] 22 | 23 | 24 | def test_basic_structure(config): 25 | assert isinstance(config, dict) 26 | assert config.keys() == {"services", "networks", "volumes"} 27 | assert isinstance(config["services"], dict) 28 | assert isinstance(config["networks"], dict) 29 | assert isinstance(config["volumes"], dict) 30 | assert not config["networks"] 31 | assert not all(config["volumes"].values()) # all values are None 32 | assert len(config["services"]) > 0 33 | assert len(config["volumes"]) > 0 34 | 35 | 36 | def check_service(service, service_data): 37 | assert isinstance(service_data, dict) 38 | assert len(service_data.keys()) > 0 39 | if service_data.get("image") or service_data.get("build"): 40 | assert service_data.keys() >= {"restart", "image"} 41 | assert service in THIRD_PARTY_IMAGES or "bitcart" in service_data["image"] 42 | assert service_data["restart"] == "unless-stopped" if service != "cloudflared" else "on-failure" 43 | # Pin versions 44 | assert ":" in service_data["image"] 45 | check_additional_keys(service, service_data) 46 | 47 | 48 | def check_key(service, service_data, key, key_type=list, value_type=str): 49 | if key in service_data: 50 | assert isinstance(service_data[key], key_type) 51 | if key_type is list: 52 | assert all(isinstance(value, value_type) for value in service_data[key]) 53 | 54 | 55 | def check_additional_keys(service, service_data): 56 | check_key(service, service_data, "expose") 57 | check_key(service, service_data, "ports") 58 | check_key(service, service_data, "links") 59 | check_key(service, service_data, "depends_on") 60 | check_key(service, service_data, "volumes") 61 | check_key(service, service_data, "command", (str, list)) 62 | check_key(service, service_data, "entrypoint", str) 63 | if "environment" in service_data: 64 | assert isinstance(service_data["environment"], dict) 65 | for key, value in service_data["environment"].items(): 66 | assert isinstance(key, str) and value is None or isinstance(value, str | int) 67 | 68 | 69 | @pytest.mark.parametrize("service", DEFAULT_SERVICES) 70 | def test_default_services(config, service): 71 | services = config["services"] 72 | assert service in services 73 | check_service(service, services[service]) 74 | 75 | 76 | def test_default_components_list(): 77 | default_components = sorted(get_components_list()) 78 | for component in ("admin", "store", "backend", "bitcoin"): 79 | assert component in default_components 80 | 81 | 82 | def test_all_components(all_components): 83 | for component in all_components: 84 | component_data = load_component(component) 85 | assert isinstance(component_data, dict) 86 | assert component_data.keys() >= {"services"} 87 | services = component_data["services"] 88 | for service in services: 89 | check_service(service, services[service]) 90 | 91 | 92 | def test_load_component(): 93 | data = load_component("backend") 94 | assert isinstance(data, dict) 95 | assert len(data.keys()) > 0 96 | assert load_component("notexisting") == {} 97 | 98 | 99 | def test_ordered_set(settings): 100 | components = add_components(settings) 101 | viewable = repr(components) 102 | assert "backend" in viewable 103 | parsed_components = viewable.replace("{", "").replace("}", "").replace("'", "").split(", ") 104 | assert list(components.keys()) == parsed_components 105 | 106 | 107 | def get_saved_config(config, path): 108 | save(config, out_path=path) 109 | with open(path) as f: 110 | return f.read() 111 | 112 | 113 | def test_save_config(config): 114 | with tempfile.TemporaryDirectory(prefix="bitcart-docker-tests-") as tmp: 115 | path = os.path.join(tmp, "generated.yml") 116 | output = get_saved_config(config, path) 117 | # deterministic 118 | assert output == get_saved_config(config, path) 119 | # writing correct data 120 | assert output[len(AUTOGENERATED_COMMENT) :] == yaml.dump(config, default_flow_style=False) 121 | -------------------------------------------------------------------------------- /generator/tests/test_rules.py: -------------------------------------------------------------------------------- 1 | from generator.constants import CRYPTO_COMPONENTS, HOST_COMPONENTS, TOR_CRYPTOS 2 | from generator.generator import generate_config 3 | 4 | from .utils import delete_env, set_env 5 | 6 | 7 | # Rule 1 8 | def test_no_image_rule(config): 9 | for service in config["services"].values(): 10 | assert service.get("image") or service.get("build") 11 | 12 | 13 | # Rules 2 and 3: one_domain and root_path 14 | def check_one_domain_setting(name): 15 | set_env(name, "something") 16 | assert "ONE_DOMAIN_MODE" not in generate_config()["services"]["store"]["environment"] 17 | # Cleanup 18 | delete_env(name) 19 | 20 | 21 | def check_preferred(services, preferred_service): 22 | for service in HOST_COMPONENTS: 23 | if services.get(service): 24 | if service == preferred_service: 25 | assert "VIRTUAL_HOST" in services[service]["environment"] 26 | else: 27 | assert "VIRTUAL_HOST" not in services[service]["environment"] 28 | 29 | 30 | def check_root_path(services, service, value): 31 | if value is None: 32 | assert service not in services 33 | return 34 | root_path = services[service]["environment"][f"BITCART_{service.upper()}_ROOTPATH"] 35 | default = root_path.split(":-")[1].replace("}", "") 36 | if value != "/": 37 | assert default == value 38 | else: 39 | assert default in ["/", ""] 40 | 41 | 42 | def test_one_domain_rule(): 43 | services = generate_config()["services"] 44 | for service in HOST_COMPONENTS: 45 | if services.get(service): 46 | assert services[service]["environment"]["ONE_DOMAIN_MODE"] == "true" 47 | check_one_domain_setting("REVERSEPROXY") 48 | check_one_domain_setting("ADMIN_HOST") 49 | check_one_domain_setting("STORE_HOST") 50 | check_one_domain_setting("ADMIN_API_URL") 51 | check_one_domain_setting("STORE_API_URL") 52 | assert services["admin"]["environment"]["BITCART_ADMIN_API_URL"] == "https://None/api" 53 | set_env("REVERSEPROXY_HTTPS_PORT", "445", prefix="") 54 | services = generate_config()["services"] 55 | assert services["admin"]["environment"]["BITCART_ADMIN_API_URL"] == "https://None:445/api" 56 | delete_env("REVERSEPROXY_HTTPS_PORT", prefix="") 57 | set_env("REVERSEPROXY_HTTP_PORT", "445", prefix="") 58 | set_env("REVERSEPROXY", "nginx") 59 | services = generate_config()["services"] 60 | assert services["admin"]["environment"]["BITCART_ADMIN_API_URL"] == "http://None:445/api" 61 | delete_env("REVERSEPROXY_HTTP_PORT", prefix="") 62 | delete_env("REVERSEPROXY") 63 | # Check preferred service setting 64 | # Store preferred 65 | check_preferred(services, "store") 66 | check_root_path(services, "store", "/") 67 | check_root_path(services, "admin", "/admin") 68 | check_root_path(services, "backend", "/api") 69 | # Admin preferred 70 | set_env("INSTALL", "backend") 71 | set_env("ADDITIONAL_COMPONENTS", "admin") 72 | services = generate_config()["services"] 73 | check_preferred(services, "admin") 74 | check_root_path(services, "store", None) 75 | check_root_path(services, "admin", "/") 76 | check_root_path(services, "backend", "/api") 77 | # Backend preferred 78 | delete_env("ADDITIONAL_COMPONENTS") 79 | set_env("INSTALL", "backend") 80 | services = generate_config()["services"] 81 | check_preferred(services, "backend") 82 | check_root_path(services, "store", None) 83 | check_root_path(services, "admin", None) 84 | check_root_path(services, "backend", "/") 85 | # Cleanup 86 | delete_env("INSTALL") 87 | 88 | 89 | # Rule 4 90 | def check_no_ports(services, ports_components): 91 | for service in services: 92 | if service in ports_components: 93 | assert "ports" not in services[service] 94 | 95 | 96 | def test_no_reverseproxy_rule(): 97 | ports_components = HOST_COMPONENTS + CRYPTO_COMPONENTS 98 | services = generate_config()["services"] 99 | check_no_ports(services, ports_components) 100 | set_env("REVERSEPROXY", "nginx") 101 | services = generate_config()["services"] 102 | check_no_ports(services, ports_components) 103 | set_env("REVERSEPROXY", "none") 104 | services = generate_config()["services"] 105 | for service in services: 106 | if service in ports_components: 107 | if service in HOST_COMPONENTS: 108 | assert "ports" in services[service] 109 | if service in CRYPTO_COMPONENTS: 110 | assert "ports" not in services[service] 111 | # check that it works even with nginx on 112 | delete_env("REVERSEPROXY") 113 | set_env("BITCOIN_EXPOSE", "true") 114 | services = generate_config()["services"] 115 | assert "ports" in services["bitcoin"] 116 | # Cleanup 117 | delete_env("BITCOIN_EXPOSE") 118 | 119 | 120 | # Rule 5 121 | def test_frontend_rule(): 122 | # Backend, store, admin 123 | services = generate_config()["services"] 124 | assert services["admin"]["links"] == ["backend"] 125 | assert services["store"]["links"] == ["backend"] 126 | # Backend, admin 127 | set_env("INSTALL", "backend") 128 | set_env("ADDITIONAL_COMPONENTS", "admin") 129 | services = generate_config()["services"] 130 | assert services["admin"]["links"] == ["backend"] 131 | assert "store" not in services 132 | # Backend, store 133 | set_env("ADDITIONAL_COMPONENTS", "store") 134 | services = generate_config()["services"] 135 | assert services["store"]["links"] == ["backend"] 136 | assert "admin" not in services 137 | # Store, admin 138 | set_env("INSTALL", "frontend") 139 | delete_env("ADDITIONAL_COMPONENTS") 140 | set_env("ADMIN_API_URL", "test") 141 | services = generate_config()["services"] 142 | assert "links" not in services["admin"] 143 | assert "links" not in services["store"] 144 | # Nothing 145 | set_env("INSTALL", "none") 146 | delete_env("ADMIN_API_URL") 147 | services = generate_config()["services"] 148 | assert "store" not in services 149 | assert "admin" not in services 150 | # Cleanup 151 | delete_env("INSTALL") 152 | 153 | 154 | # Rule 6 155 | def test_tor_rule(): 156 | services = generate_config()["services"] 157 | assert "tor" not in services 158 | set_env("ADDITIONAL_COMPONENTS", "tor") 159 | services = generate_config()["services"] 160 | assert "tor" in services 161 | for env_name, service in TOR_CRYPTOS.items(): 162 | if services.get(service["component"]): 163 | assert f"{env_name.upper()}_PROXY_URL" in services[service["component"]]["environment"] 164 | for service in services: 165 | if service in HOST_COMPONENTS: 166 | assert services[service]["environment"]["HIDDENSERVICE_REVERSEPROXY"] == "compose-nginx-1" 167 | set_env("REVERSEPROXY", "none") 168 | services = generate_config()["services"] 169 | for service in services: 170 | if service in HOST_COMPONENTS: 171 | assert "HIDDENSERVICE_IP" in services[service]["environment"] 172 | assert ( 173 | services[service]["environment"]["HIDDENSERVICE_VIRTUAL_PORT"] 174 | == services[service]["environment"]["VIRTUAL_PORT"] 175 | ) 176 | # Cleanup 177 | delete_env("ADDITIONAL_COMPONENTS") 178 | delete_env("REVERSEPROXY") 179 | 180 | 181 | # Rule 8 182 | def test_scale(): 183 | services = generate_config()["services"] 184 | assert "deploy" not in services["backend"] 185 | set_env("BACKEND_SCALE", "2") 186 | services = generate_config()["services"] 187 | assert services["backend"]["deploy"]["replicas"] == 2 188 | set_env("BACKEND_SCALE", "test") 189 | services = generate_config()["services"] 190 | assert "deploy" not in services["backend"] 191 | # Cleanup 192 | delete_env("BACKEND_SCALE") 193 | 194 | 195 | # Rule 9: allow using older versions 196 | def test_bitcart_version(): 197 | services = generate_config()["services"] 198 | for service in ("backend", "admin", "store", "bitcoin", "worker"): 199 | assert services[service]["image"].endswith(":stable") 200 | set_env("VERSION", "test") 201 | services = generate_config()["services"] 202 | for service in ("backend", "admin", "store", "bitcoin", "worker"): 203 | assert services[service]["image"].endswith(":test") 204 | delete_env("VERSION") 205 | 206 | 207 | # Rule 10: local domains 208 | def test_local_deploy(): 209 | services = generate_config()["services"] 210 | assert "extra_hosts" not in services["store"] 211 | set_env("HOST", "bitcart.local") 212 | services = generate_config()["services"] 213 | assert services["store"]["extra_hosts"] == ["bitcart.local:172.17.0.1"] 214 | delete_env("HOST") 215 | -------------------------------------------------------------------------------- /generator/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from generator.exceptions import ConfigError 4 | from generator.generator import generate_config 5 | from generator.settings import Settings 6 | 7 | from .utils import check_service_list, delete_env, set_env 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "pack, expected", 12 | [ 13 | ("all", ["backend", "admin", "store"]), 14 | ("backend", ["backend"]), 15 | ("none", ["backend", "admin", "store"]), 16 | ], 17 | ids=["all", "backend", "none"], 18 | ) 19 | def test_installation_packs(pack, expected): 20 | set_env("INSTALL", pack) 21 | config = generate_config() 22 | check_service_list(config, expected, is_none=pack == "none") 23 | # Cleanup 24 | delete_env("INSTALL") 25 | 26 | 27 | # Frontend pack requires one domain mode to be off 28 | def test_frontend_pack(): 29 | set_env("INSTALL", "frontend") 30 | with pytest.raises(ConfigError): 31 | generate_config() 32 | set_env("ADMIN_API_URL", "http://localhost:8000") 33 | set_env("STORE_API_URL", "http://localhost:8000") 34 | config = generate_config() 35 | check_service_list(config, ["admin", "store"]) 36 | # Cleanup 37 | delete_env("INSTALL") 38 | delete_env("ADMIN_API_URL") 39 | delete_env("STORE_API_URL") 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "proxy, expected", 44 | [ 45 | ("nginx-https", ["nginx", "nginx-https"]), 46 | ("nginx", ["nginx"]), 47 | ("none", ["nginx", "nginx-https"]), 48 | ], 49 | ids=["nginx-https", "nginx", "none"], 50 | ) 51 | def test_reverse_proxy(proxy, expected): 52 | set_env("REVERSEPROXY", proxy) 53 | config = generate_config() 54 | check_service_list(config, expected, is_none=proxy == "none") 55 | # Cleanup 56 | delete_env("REVERSEPROXY") 57 | 58 | 59 | def test_no_cryptos(): 60 | set_env("CRYPTOS", "invalid") 61 | assert "bitcoin" in generate_config()["services"] 62 | # Cleanup 63 | delete_env("CRYPTOS") 64 | 65 | 66 | def test_additional_components_basic(): 67 | set_env("ADDITIONAL_COMPONENTS", "invalid") 68 | assert "invalid" not in generate_config()["services"] 69 | set_env("ADDITIONAL_COMPONENTS", "test1;test2") 70 | assert Settings().ADDITIONAL_COMPONENTS == ["test1;test2"] 71 | set_env("ADDITIONAL_COMPONENTS", "test1,test2") 72 | assert Settings().ADDITIONAL_COMPONENTS == ["test1", "test2"] 73 | # Cleanup 74 | delete_env("ADDITIONAL_COMPONENTS") 75 | 76 | 77 | @pytest.mark.parametrize( 78 | "component, expected", 79 | [ 80 | ("tor", ["tor", "tor-gen"]), 81 | ("", ["tor", "tor-gen"]), 82 | ], 83 | ids=["tor", "none"], 84 | ) 85 | def test_additional_components(component, expected): 86 | set_env("ADDITIONAL_COMPONENTS", component) 87 | config = generate_config() 88 | check_service_list(config, expected, is_none=not component) 89 | # Cleanup 90 | delete_env("ADDITIONAL_COMPONENTS") 91 | 92 | 93 | def test_exclude_components_basic(): 94 | set_env("EXCLUDE_COMPONENTS", "invalid") 95 | assert "invalid" not in generate_config()["services"] 96 | set_env("EXCLUDE_COMPONENTS", "test1;test2") 97 | assert Settings().EXCLUDE_COMPONENTS == ["test1;test2"] 98 | set_env("EXCLUDE_COMPONENTS", "test1,test2") 99 | assert Settings().EXCLUDE_COMPONENTS == ["test1", "test2"] 100 | # Cleanup 101 | delete_env("EXCLUDE_COMPONENTS") 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "component, expected", 106 | [ 107 | ("admin", ["admin"]), 108 | ("", ["admin"]), 109 | ("admin,store", ["admin", "store"]), 110 | ("", ["admin", "store"]), 111 | ], 112 | ids=["admin-empty", "admin", "admin-store-empty", "admin-store"], 113 | ) 114 | def test_exclude_components(component, expected): 115 | set_env("EXCLUDE_COMPONENTS", component) 116 | config = generate_config() 117 | check_service_list(config, expected, is_none=bool(component)) 118 | # Cleanup 119 | delete_env("EXCLUDE_COMPONENTS") 120 | 121 | 122 | def test_https_hint(): 123 | set_env("HTTPS_ENABLED", "true") 124 | set_env("REVERSEPROXY", "nginx") 125 | config = generate_config() 126 | assert "letsencrypt-nginx-proxy-companion" not in config["services"] 127 | assert config["services"]["admin"]["environment"]["BITCART_ADMIN_API_URL"].startswith("https://") 128 | # Cleanup 129 | delete_env("HTTPS_ENABLED") 130 | delete_env("REVERSEPROXY") 131 | -------------------------------------------------------------------------------- /generator/tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def set_env(name, value, prefix="BITCART_"): 5 | os.environ[f"{prefix}{name}"] = value 6 | 7 | 8 | def delete_env(name, prefix="BITCART_"): 9 | del os.environ[f"{prefix}{name}"] 10 | 11 | 12 | def check_service_list(config, expected, is_none=False): 13 | for service in expected: 14 | if is_none: 15 | assert service not in config["services"] 16 | else: 17 | assert service in config["services"] 18 | -------------------------------------------------------------------------------- /generator/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from generator.constants import CRYPTO_COMPONENTS, ENV_PREFIX, HOST_COMPONENTS 4 | from generator.exceptions import ConfigError 5 | 6 | 7 | def env(name, default=None, prefix=None): 8 | env_prefix = prefix if prefix is not None else ENV_PREFIX 9 | value = os.getenv(f"{env_prefix}{name}", default) 10 | if not value: # additional checks for empty values 11 | value = default 12 | return value 13 | 14 | 15 | def custom_port_allowed(service, no_nginx): 16 | return (service not in CRYPTO_COMPONENTS and no_nginx) or env(f"{service.upper()}_EXPOSE", False) 17 | 18 | 19 | def preferred_service(components): 20 | for variant in HOST_COMPONENTS: 21 | if components.get(variant): 22 | return variant 23 | 24 | 25 | def config_error(message): 26 | raise ConfigError(f"ERROR: {message}") 27 | 28 | 29 | class ModifyKey: 30 | def __init__(self, services, service, key, default=None, save_key=None): 31 | if default is None: 32 | default = {} 33 | self.services = services 34 | self.service = service 35 | self.key = key 36 | self.default = default 37 | self.save_key = save_key or key 38 | self.key_exists = self.services.get(self.service) 39 | self.copied = self.default 40 | 41 | def __enter__(self): 42 | if self.key_exists: 43 | self.copied = self.services[self.service].get(self.key, self.default).copy() 44 | return self.copied 45 | 46 | def __exit__(self, *args, **kwargs): 47 | if self.key_exists: 48 | self.services[self.service][self.save_key] = self.copied 49 | 50 | 51 | modify_key = ModifyKey 52 | 53 | 54 | def apply_recursive(data, func): 55 | if isinstance(data, dict): 56 | new_data = {} 57 | for key, value in data.items(): 58 | to_delete, new = apply_recursive(value, func) 59 | if not to_delete: 60 | new_data[key] = new 61 | return False, new_data 62 | if isinstance(data, list): 63 | new_data = [] 64 | for value in data: 65 | to_delete, new = apply_recursive(value, func) 66 | if not to_delete: 67 | new_data.append(new) 68 | return False, new_data 69 | return func(data) 70 | -------------------------------------------------------------------------------- /helpers.sh: -------------------------------------------------------------------------------- 1 | read_from_env_file() { 2 | if cat "$1" &>/dev/null; then 3 | while IFS= read -r line; do 4 | ! [[ "$line" == "#"* ]] && [[ "$line" == *"="* ]] && export "$line" || true 5 | done <"$1" 6 | fi 7 | } 8 | 9 | read_from_env_file .deploy 10 | 11 | bitcart_update_docker_env() { 12 | touch $BITCART_ENV_FILE 13 | cat >$BITCART_ENV_FILE <>"$HOME/.bash_profile" 110 | fi 111 | 112 | else 113 | BASH_PROFILE_SCRIPT="/etc/profile.d/bitcart-env$1.sh" 114 | 115 | if $CHECK_ROOT && [[ $EUID -ne 0 ]]; then 116 | echo "This script must be run as root after running \"sudo su -\"" 117 | exit 1 118 | fi 119 | fi 120 | export BASH_PROFILE_SCRIPT 121 | } 122 | 123 | load_env() { 124 | get_profile_file "$SCRIPTS_POSTFIX" ${1:-false} 125 | . ${BASH_PROFILE_SCRIPT} 126 | } 127 | 128 | try() { 129 | "$@" || true 130 | } 131 | 132 | remove_host() { 133 | if [ -n "$(grep -w "$1$" /etc/hosts)" ]; then 134 | try sudo sed -ie "/[[:space:]]$1/d" /etc/hosts 135 | fi 136 | } 137 | 138 | add_host() { 139 | if [ -z "$(grep -E "[[:space:]]$2" /etc/hosts)" ]; then 140 | try sudo printf "%s\t%s\n" "$1" "$2" | sudo tee -a /etc/hosts >/dev/null 141 | fi 142 | } 143 | 144 | modify_host() { 145 | if [ -z "$2" ]; then 146 | return 147 | fi 148 | remove_host $2 149 | add_host $1 $2 150 | } 151 | 152 | apply_local_modifications() { 153 | if [[ "$BITCART_HOST" == *.local ]]; then 154 | echo "Local setup detected." 155 | if [[ "$BITCART_NOHOSTSEDIT" = true ]]; then 156 | echo "Not modifying hosts." 157 | else 158 | echo "WARNING! Modifying /etc/hosts to make local setup work. It may require superuser privileges." 159 | modify_host 172.17.0.1 $BITCART_STORE_HOST 160 | modify_host 172.17.0.1 $BITCART_HOST 161 | modify_host 172.17.0.1 $BITCART_ADMIN_HOST 162 | fi 163 | fi 164 | } 165 | 166 | container_name() { 167 | deployment_name=${NAME:-compose} 168 | echo "${deployment_name}-$1" 169 | } 170 | 171 | volume_name() { 172 | deployment_name=${NAME:-compose} 173 | echo "${deployment_name}_$1" 174 | } 175 | 176 | create_backup_volume() { 177 | backup_dir="/var/lib/docker/volumes/backup_datadir/_data" 178 | if [ ! -d "$backup_dir" ]; then 179 | docker volume create backup_datadir >/dev/null 2>&1 180 | fi 181 | } 182 | 183 | bitcart_dump_db() { 184 | create_backup_volume 185 | docker exec $(container_name "database-1") pg_dumpall -c -U postgres >"$backup_dir/$1" 186 | } 187 | 188 | bitcart_restore_db() { 189 | bitcart_start database 190 | # wait for db to be up 191 | until docker exec -i $(container_name "database-1") psql -U postgres -c '\l'; do 192 | echo >&2 "Postgres is unavailable - sleeping" 193 | sleep 1 194 | done 195 | cat $1 | docker exec -i $(container_name "database-1") psql -U postgres 196 | } 197 | 198 | version() { 199 | echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' 200 | } 201 | 202 | check_docker_compose() { 203 | if [ ! -z "$(docker-compose --version 2>/dev/null | grep docker-compose)" ] || ! [[ $(docker compose version 2>/dev/null) ]] || [ $(version $(docker compose version --short)) -lt $(version "2.9.0") ]; then 204 | install_docker_compose 205 | fi 206 | } 207 | 208 | install_docker_compose() { 209 | OS=$(uname -s) 210 | ARCH=$(uname -m) 211 | INSTALL_PATH=/usr/local/lib/docker/cli-plugins 212 | if [[ "$OS" == "Darwin" ]]; then 213 | INSTALL_PATH=~/.docker/cli-plugins 214 | if [[ "$ARCH" == "arm64" ]]; then 215 | ARCH="aarch64" 216 | fi 217 | fi 218 | DOCKER_COMPOSE_DOWNLOAD="https://github.com/docker/compose/releases/latest/download/docker-compose-$OS-$ARCH" 219 | echo "Trying to install docker-compose by downloading on $DOCKER_COMPOSE_DOWNLOAD ($(uname -m))" 220 | sudo mkdir -p $INSTALL_PATH 221 | sudo curl -L "$DOCKER_COMPOSE_DOWNLOAD" -o $INSTALL_PATH/docker-compose 222 | sudo chmod +x $INSTALL_PATH/docker-compose 223 | # remove old docker-compose 224 | try sudo rm "$(command -v docker-compose)" &>/dev/null 225 | } 226 | 227 | install_tooling() { 228 | try sudo cp compose/scripts/cli-autocomplete.sh /etc/bash_completion.d/bitcart-cli.sh 229 | try sudo chmod +x /etc/bash_completion.d/bitcart-cli.sh 230 | } 231 | 232 | save_deploy_config() { 233 | BITCART_DEPLOYMENT_CONFIG="$BITCART_BASE_DIRECTORY/.deploy" 234 | cat >${BITCART_DEPLOYMENT_CONFIG} </dev/null | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}' 250 | } 251 | 252 | make_backup_image() { 253 | if [ "$(docker inspect --format '{{ index .Config.Labels "org.bitcart.plugins"}}' $1:stable)" = true ]; then 254 | : 255 | else 256 | docker tag $1:stable $1:original 257 | fi 258 | } 259 | 260 | install_plugins() { 261 | COMPONENTS=$(./build.sh --components-only | tail -1) 262 | COIN_COMPONENTS=$(./build.sh --cryptos-only | tail -1) 263 | failed_file="/var/lib/docker/volumes/$(volume_name "bitcart_datadir")/_data/.plugins-failed" 264 | error=false 265 | rm -f $failed_file 266 | if [[ " ${COMPONENTS[*]} " =~ " backend " ]]; then 267 | make_backup_image bitcart/bitcart 268 | fi 269 | if [[ " ${COMPONENTS[*]} " =~ " admin " ]]; then 270 | make_backup_image bitcart/bitcart-admin 271 | fi 272 | if [[ " ${COMPONENTS[*]} " =~ " store " ]]; then 273 | make_backup_image bitcart/bitcart-store 274 | fi 275 | for coin in $COIN_COMPONENTS; do 276 | make_backup_image bitcart/bitcart-$coin 277 | done 278 | if [[ "$DOCKER_PLUGINS_HASH" != "$(get_plugins_hash docker)" ]]; then 279 | ./build.sh || touch $failed_file 280 | docker compose -f compose/generated.yml config || touch $failed_file 281 | fi 282 | if [[ " ${COMPONENTS[*]} " =~ " backend " ]] && [[ "$BACKEND_PLUGINS_HASH" != "$(get_plugins_hash backend)" ]]; then 283 | docker build -t bitcart/bitcart:stable -f compose/backend-plugins.Dockerfile compose || error=true 284 | fi 285 | if [[ "$error" = false ]] && [[ " ${COMPONENTS[*]} " =~ " admin " ]] && [[ "$ADMIN_PLUGINS_HASH" != "$(get_plugins_hash admin)" ]]; then 286 | docker build -t bitcart/bitcart-admin:stable -f compose/admin-plugins.Dockerfile compose || error=true 287 | fi 288 | if [[ "$error" = false ]] && [[ " ${COMPONENTS[*]} " =~ " store " ]] && [[ "$STORE_PLUGINS_HASH" != "$(get_plugins_hash store)" ]]; then 289 | docker build -t bitcart/bitcart-store:stable -f compose/store-plugins.Dockerfile compose || error=true 290 | fi 291 | if [[ "$error" = false ]] && [[ "$DAEMON_PLUGINS_HASH" != "$(get_plugins_hash daemon)" ]]; then 292 | for coin in $COIN_COMPONENTS; do 293 | docker build -t bitcart/bitcart-$coin:stable -f compose/coin-plugins.Dockerfile compose --build-arg COIN=$coin || error=true 294 | done 295 | fi 296 | if [[ "$error" = true ]]; then 297 | echo "Plugins installation failed, restoring original images" 298 | if [[ " ${COMPONENTS[*]} " =~ " backend " ]]; then 299 | docker tag bitcart/bitcart:original bitcart/bitcart:stable 300 | fi 301 | if [[ " ${COMPONENTS[*]} " =~ " admin " ]]; then 302 | docker tag bitcart/bitcart-admin:original bitcart/bitcart-admin:stable 303 | fi 304 | if [[ " ${COMPONENTS[*]} " =~ " store " ]]; then 305 | docker tag bitcart/bitcart-store:original bitcart/bitcart-store:stable 306 | fi 307 | for coin in $COIN_COMPONENTS; do 308 | docker tag bitcart/bitcart-$coin:original bitcart/bitcart-$coin:stable 309 | done 310 | touch $failed_file 311 | fi 312 | save_deploy_config 313 | } 314 | -------------------------------------------------------------------------------- /install-master.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | . helpers.sh 6 | load_env 7 | 8 | cd "$BITCART_BASE_DIRECTORY" 9 | 10 | # Create a backup 11 | ./backup.sh 12 | # First, update to latest stable release, then we can apply staging changes 13 | ./update.sh 14 | 15 | COMPONENTS=$(./build.sh --components-only | tail -1) 16 | IFS=', ' read -r -a CRYPTOS <<<"$BITCART_CRYPTOS" 17 | 18 | ./dev-setup.sh 19 | 20 | cd compose 21 | 22 | if [[ " ${COMPONENTS[*]} " =~ " backend " ]]; then 23 | docker build -t bitcart/bitcart:stable -f backend.Dockerfile . || true 24 | fi 25 | 26 | for coin in "${CRYPTOS[@]}"; do 27 | docker build -t bitcart/bitcart-$coin:stable -f $coin.Dockerfile . || true 28 | done 29 | 30 | cd .. 31 | rm -rf compose/bitcart 32 | 33 | build_additional_image() { 34 | if [[ " ${COMPONENTS[*]} " =~ " $1 " ]]; then 35 | OLDDIR="$PWD" 36 | TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 37 | cd $TEMP_DIR 38 | git clone https://github.com/bitcart/bitcart-$1 39 | cd bitcart-$1 40 | docker build -t bitcart/bitcart-$1:stable . || true 41 | cd "$OLDDIR" 42 | rm -rf $TEMP_DIR 43 | fi 44 | } 45 | 46 | build_additional_image admin 47 | build_additional_image store 48 | bitcart_reset_plugins 49 | bitcart_start 50 | -------------------------------------------------------------------------------- /load_env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . helpers.sh 4 | load_env 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.uv.workspace] 2 | members = ["generator"] 3 | 4 | [tool.coverage.run] 5 | source = ["."] 6 | omit = [ 7 | "*__main__.py", 8 | "generator/tests/**", 9 | "venv/**", 10 | "env/**", 11 | "*__init__.py", 12 | ] 13 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . helpers.sh 4 | load_env 5 | 6 | cd "$BITCART_BASE_DIRECTORY" 7 | bitcart_restart 8 | -------------------------------------------------------------------------------- /restore.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function display_help() { 4 | cat <<-END 5 | Usage: 6 | ------ 7 | Restore Bitcart files 8 | This script must be run as root 9 | -h, --help: Show help 10 | --delete-backup: Delete backup file after restoring. Default: false 11 | This script will restore the database from SQL script and copy essential volumes to /var/lib/docker/volumes 12 | extracted from tar.gz backup archive 13 | END 14 | } 15 | 16 | DELETE_BACKUP=false 17 | BACKUP_FILE= 18 | 19 | while (("$#")); do 20 | case "$1" in 21 | -h) 22 | display_help 23 | exit 0 24 | ;; 25 | --help) 26 | display_help 27 | exit 0 28 | ;; 29 | --delete-backup) 30 | DELETE_BACKUP=true 31 | shift 1 32 | ;; 33 | --) # end argument parsing 34 | shift 35 | break 36 | ;; 37 | -* | --*=) # unsupported flags 38 | echo "Error: Unsupported flag $1" >&2 39 | display_help 40 | exit 1 41 | ;; 42 | *) 43 | if [ -z "$BACKUP_FILE" ]; then 44 | BACKUP_FILE="$1" 45 | fi 46 | shift 47 | ;; 48 | esac 49 | done 50 | 51 | if [ -z "$BACKUP_FILE" ]; then 52 | display_help 53 | exit 0 54 | fi 55 | 56 | . helpers.sh 57 | load_env true 58 | cd "$BITCART_BASE_DIRECTORY" 59 | 60 | TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 61 | 62 | tar -C $TEMP_DIR -xvf "$BACKUP_FILE" 63 | 64 | echo "Stopping Bitcart…" 65 | bitcart_stop 66 | 67 | echo "Restoring database …" 68 | bitcart_restore_db $TEMP_DIR/database.sql 69 | echo "Restoring docker volumes…" 70 | cp -r $TEMP_DIR/volumes/ /var/lib/docker 71 | cp -r $TEMP_DIR/plugins compose 72 | 73 | echo "Restarting Bitcart…" 74 | bitcart_start 75 | 76 | rm -rf $TEMP_DIR 77 | 78 | if $DELETE_BACKUP; then 79 | rm -rf "$BACKUP_FILE" 80 | fi 81 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +x 4 | 5 | . helpers.sh 6 | 7 | function display_help() { 8 | cat <<-END 9 | Usage: 10 | ------ 11 | Install Bitcart on this server 12 | This script must be run as root, except on Mac OS 13 | -h, --help: Show help 14 | -p: print settings and exit 15 | --name name: Configure new deployment name. Affects naming of profile env files and 16 | startup config files. Allows multiple deployments on one server. 17 | Empty by default 18 | --install-only: Run install only 19 | --no-startup-register: Do not register Bitcart to start via systemctl or upstart 20 | --no-systemd-reload: Do not reload systemd configuration 21 | This script will: 22 | * Install Docker 23 | * Install Docker-Compose 24 | * Setup Bitcart settings 25 | * Make sure it starts at reboot via upstart or systemd 26 | * Add Bitcart utilities in /usr/bin 27 | * Start Bitcart 28 | You can run again this script if you desire to change your configuration. 29 | Make sure you own a domain with DNS record pointing to your website. 30 | Passing domain ending with .local will automatically edit /etc/hosts to make it work. 31 | Environment variables: 32 | BITCART_INSTALL: installation template to use (eg. all, backend, frontend) 33 | BITCART_CRYPTOS: comma-separated list of cryptocurrencies to enable (eg. btc) 34 | BITCART_REVERSEPROXY: which reverse proxy to use (eg. nginx, nginx-https, none) 35 | REVERSEPROXY_HTTP_PORT: The port the reverse proxy binds to for public HTTP requests. Default: 80 36 | REVERSEPROXY_HTTPS_PORT: The port the reverse proxy binds to for public HTTPS requests. Default: 443 37 | REVERSEPROXY_DEFAULT_HOST: Optional, if using a reverse proxy nginx, specify which website should be presented if the server is accessed by its IP. 38 | BITCART_ENABLE_SSH: Gives Bitcart SSH access to the host by allowing it to edit authorized_keys of the host, it can be used for updating or reconfiguring your instance directly through the website. (Default: true) 39 | BITCART_SSH_PORT: Port where ssh server runs on host machine. Default: 22 40 | BITCART_HOST: The hostname of your website API (eg. api.example.com) 41 | BITCART_LETSENCRYPT_EMAIL: A mail will be sent to this address if certificate expires and fail to renew automatically (eg. me@example.com) 42 | BITCART_STORE_HOST: The hostname of your website store (eg. example.com) 43 | BITCART_STORE_API_URL: The URL to your website API (hosted locally or remotely, eg. https://api.example.com) 44 | BITCART_ADMIN_HOST: The hostname of your website admin panel (eg. admin.example.com) 45 | BITCART_ADMIN_API_URL: The URL to your website API (hosted locally or remotely, eg. https://api.example.com) 46 | BTC_NETWORK: The network to run bitcoin daemon on (eg. mainnet, testnet) 47 | BTC_LIGHTNING: Whether to enable bitcoin lightning network or not (eg. true, false) 48 | BCH_NETWORK: The network to run bitcoin cash daemon on (eg. mainnet, testnet) 49 | ETH_NETWORK: The network to run ethereum daemon on (eg. mainnet, kovan) 50 | BNB_NETWORK: The network to run binancecoin daemon on (eg. mainnet, testnet) 51 | SBCH_NETWORK: The network to run smartbch daemon on (eg. mainnet, testnet) 52 | MATIC_NETWORK: The network to run polygon daemon on (eg. mainnet, testnet) 53 | TRX_NETWORK: The network to run tron daemon on (eg. mainnet, testnet) 54 | XRG_NETWORK: The network to run ergon daemon on (eg. mainnet) 55 | LTC_NETWORK: The network to run litecoin daemon on (eg. mainnet, testnet) 56 | LTC_LIGHTNING: Whether to enable litecoin lightning network or not (eg. true, false) 57 | BSTY_NETWORK: The network to run globalboost daemon on (eg. mainnet, testnet) 58 | BSTY_LIGHTNING: Whether to enable globalboost lightning network or not (eg. true, false) 59 | GRS_NETWORK: The network to run groestlcoin daemon on (eg. mainnet, testnet) 60 | GRS_LIGHTNING: Whether to enable groestlcoin lightning network or not (eg. true, false) 61 | XMR_NETWORK: The network to run monero daemon on (eg. mainnet, testnet) 62 | BITCART_ADDITIONAL_COMPONENTS: A list of additional components to add 63 | BITCART_EXCLUDE_COMPONENTS: A list of components to exclude from the result set 64 | BITCART_HTTPS_ENABLED: a special flag to rewrite bitcart API URLs to https. It does not enable https setup automatically and is required for custom cases such as cloudflare tunnel. 65 | BITCART_BEHIND_REVERSEPROXY: set this when running in one domain mode with custom nginx port, but when bitcart's nginx is behind your own reverse proxy 66 | BITCART_SENTRY_DSN: Optional Sentry DSN for error tracking 67 | BITCART_API_WORKERS: Number of API workers to run (default: 2*cores+1=$((2 * $(nproc) + 1))) 68 | Add-on specific variables: 69 | TOR_RELAY_NICKNAME: If tor relay is activated, the relay nickname 70 | TOR_RELAY_EMAIL: If tor relay is activated, the email for Tor to contact you regarding your relay 71 | CLOUDFLARE_TUNNEL_TOKEN: Used to expose your instance to clearnet with a Cloudflare Tunnel 72 | END 73 | } 74 | 75 | START=true 76 | STARTUP_REGISTER=true 77 | SYSTEMD_RELOAD=true 78 | NAME_INPUT=false 79 | PREVIEW_SETTINGS=false 80 | NAME= 81 | SCRIPTS_POSTFIX= 82 | while (("$#")); do 83 | case "$1" in 84 | -h) 85 | display_help 86 | exit 0 87 | ;; 88 | --help) 89 | display_help 90 | exit 0 91 | ;; 92 | -p) 93 | PREVIEW_SETTINGS=true 94 | shift 1 95 | ;; 96 | --install-only) 97 | START=false 98 | shift 1 99 | ;; 100 | --no-startup-register) 101 | STARTUP_REGISTER=false 102 | shift 1 103 | ;; 104 | --no-systemd-reload) 105 | SYSTEMD_RELOAD=false 106 | shift 1 107 | ;; 108 | --name) 109 | NAME_INPUT=true 110 | shift 1 111 | ;; 112 | --) # end argument parsing 113 | shift 114 | break 115 | ;; 116 | -* | --*=) # unsupported flags 117 | echo "Error: Unsupported flag $1" >&2 118 | display_help 119 | exit 1 120 | ;; 121 | *) # preserve positional arguments 122 | if $NAME_INPUT; then 123 | NAME="$1" 124 | SCRIPTS_POSTFIX="-$NAME" 125 | NAME_INPUT=false 126 | fi 127 | PARAMS="$PARAMS $1" 128 | shift 129 | ;; 130 | esac 131 | done 132 | 133 | # Check root, and set correct profile file for the platform 134 | get_profile_file "$SCRIPTS_POSTFIX" 135 | 136 | # Set settings default values 137 | [[ $BITCART_LETSENCRYPT_EMAIL == *@example.com ]] && echo "BITCART_LETSENCRYPT_EMAIL ends with @example.com, setting to empty email instead" && BITCART_LETSENCRYPT_EMAIL="" 138 | 139 | : "${BITCART_LETSENCRYPT_EMAIL:=}" 140 | : "${BITCART_INSTALL:=all}" 141 | : "${BITCART_CRYPTOS:=btc}" 142 | : "${BITCART_REVERSEPROXY:=nginx-https}" 143 | : "${REVERSEPROXY_DEFAULT_HOST:=none}" 144 | : "${REVERSEPROXY_HTTP_PORT:=80}" 145 | : "${REVERSEPROXY_HTTPS_PORT:=443}" 146 | : "${BITCART_ENABLE_SSH:=true}" 147 | : "${BITCART_SSH_PORT:=22}" 148 | : "${CLOUDFLARE_TUNNEL_TOKEN:=}" 149 | : "${PIHOLE_SERVERIP:=}" 150 | 151 | # Crypto default settings (adjust to add a new coin) 152 | : "${BTC_NETWORK:=mainnet}" 153 | : "${BTC_LIGHTNING:=false}" 154 | : "${BCH_NETWORK:=mainnet}" 155 | : "${ETH_NETWORK:=mainnet}" 156 | : "${BNB_NETWORK:=mainnet}" 157 | : "${SBCH_NETWORK:=mainnet}" 158 | : "${MATIC_NETWORK:=mainnet}" 159 | : "${TRX_NETWORK:=mainnet}" 160 | : "${XRG_NETWORK:=mainnet}" 161 | : "${LTC_NETWORK:=mainnet}" 162 | : "${LTC_LIGHTNING:=false}" 163 | : "${BSTY_NETWORK:=mainnet}" 164 | : "${BSTY_LIGHTNING:=false}" 165 | : "${GRS_NETWORK:=mainnet}" 166 | : "${GRS_LIGHTNING:=false}" 167 | : "${XMR_NETWORK:=mainnet}" 168 | 169 | BITCART_BASE_DIRECTORY="$(pwd)" 170 | BITCART_ENV_FILE="$BITCART_BASE_DIRECTORY/.env" 171 | BITCART_DEPLOYMENT_CONFIG="$BITCART_BASE_DIRECTORY/.deploy" 172 | 173 | first_setup=false 174 | if ! [ -f "$BITCART_DEPLOYMENT_CONFIG" ]; then 175 | first_setup=true 176 | fi 177 | 178 | # SSH settings 179 | BITCART_SSH_KEY_FILE="" 180 | 181 | if $BITCART_ENABLE_SSH && ! [[ "$BITCART_HOST_SSH_AUTHORIZED_KEYS" ]]; then 182 | BITCART_HOST_SSH_AUTHORIZED_KEYS=~/.ssh/authorized_keys 183 | fi 184 | 185 | if $BITCART_ENABLE_SSH && [[ "$BITCART_HOST_SSH_AUTHORIZED_KEYS" ]]; then 186 | if ! [[ -f "$BITCART_HOST_SSH_AUTHORIZED_KEYS" ]]; then 187 | mkdir -p "$(dirname $BITCART_HOST_SSH_AUTHORIZED_KEYS)" 188 | touch $BITCART_HOST_SSH_AUTHORIZED_KEYS 189 | fi 190 | BITCART_SSH_AUTHORIZED_KEYS="/datadir/host_authorized_keys" 191 | BITCART_SSH_KEY_FILE="/datadir/host_id_rsa" 192 | fi 193 | 194 | if [ "$BITCARTGEN_DOCKER_IMAGE" == "bitcartcc/docker-compose-generator:local" ]; then 195 | export BITCARTGEN_DOCKER_IMAGE="bitcart/docker-compose-generator:local" 196 | fi 197 | 198 | echo "-------SETUP----------- 199 | Parameters passed: 200 | BITCART_HOST=$BITCART_HOST 201 | REVERSEPROXY_HTTP_PORT=$REVERSEPROXY_HTTP_PORT 202 | REVERSEPROXY_HTTPS_PORT=$REVERSEPROXY_HTTPS_PORT 203 | REVERSEPROXY_DEFAULT_HOST=$REVERSEPROXY_DEFAULT_HOST 204 | BITCART_ENABLE_SSH=$BITCART_ENABLE_SSH 205 | BITCART_SSH_PORT=$BITCART_SSH_PORT 206 | BITCART_LETSENCRYPT_EMAIL=$BITCART_LETSENCRYPT_EMAIL 207 | BITCART_STORE_HOST=$BITCART_STORE_HOST 208 | BITCART_STORE_API_URL=$BITCART_STORE_API_URL 209 | BITCART_ADMIN_HOST=$BITCART_ADMIN_HOST 210 | BITCART_ADMIN_API_URL=$BITCART_ADMIN_API_URL 211 | BITCART_INSTALL=$BITCART_INSTALL 212 | BITCART_REVERSEPROXY=$BITCART_REVERSEPROXY 213 | BITCART_CRYPTOS=$BITCART_CRYPTOS 214 | BITCART_ADDITIONAL_COMPONENTS=$BITCART_ADDITIONAL_COMPONENTS 215 | BITCART_EXCLUDE_COMPONENTS=$BITCART_EXCLUDE_COMPONENTS 216 | BTC_NETWORK=$BTC_NETWORK 217 | BTC_LIGHTNING=$BTC_LIGHTNING 218 | BCH_NETWORK=$BCH_NETWORK 219 | ETH_NETWORK=$ETH_NETWORK 220 | BNB_NETWORK=$BNB_NETWORK 221 | SBCH_NETWORK=$SBCH_NETWORK 222 | MATIC_NETWORK=$MATIC_NETWORK 223 | TRX_NETWORK=$TRX_NETWORK 224 | XRG_NETWORK=$XRG_NETWORK 225 | LTC_NETWORK=$LTC_NETWORK 226 | LTC_LIGHTNING=$LTC_LIGHTNING 227 | BSTY_NETWORK=$BSTY_NETWORK 228 | BSTY_LIGHTNING=$BSTY_LIGHTNING 229 | GRS_NETWORK=$GRS_NETWORK 230 | GRS_LIGHTNING=$GRS_LIGHTNING 231 | XMR_NETWORK=$XMR_NETWORK 232 | ---------------------- 233 | Additional exported variables: 234 | BITCART_BASE_DIRECTORY=$BITCART_BASE_DIRECTORY 235 | BITCART_ENV_FILE=$BITCART_ENV_FILE 236 | BITCART_DEPLOYMENT_CONFIG=$BITCART_DEPLOYMENT_CONFIG 237 | BITCART_SSH_KEY_FILE=$BITCART_SSH_KEY_FILE 238 | BITCART_SSH_AUTHORIZED_KEYS=$BITCART_SSH_AUTHORIZED_KEYS 239 | BITCART_HOST_SSH_AUTHORIZED_KEYS=$BITCART_HOST_SSH_AUTHORIZED_KEYS 240 | ----------------------" 241 | 242 | if $PREVIEW_SETTINGS; then 243 | exit 0 244 | fi 245 | 246 | # Local setup modifications 247 | apply_local_modifications 248 | 249 | # Configure deployment config to determine which deployment name to use 250 | save_deploy_config 251 | 252 | # Init the variables when a user log interactively 253 | touch "$BASH_PROFILE_SCRIPT" 254 | cat >${BASH_PROFILE_SCRIPT} < /dev/null; then 269 | while IFS= read -r line; do 270 | ! [[ "\$line" == "#"* ]] && [[ "\$line" == *"="* ]] && export "\$line" || true 271 | done < "\$BITCART_ENV_FILE" 272 | fi 273 | EOF 274 | 275 | chmod +x ${BASH_PROFILE_SCRIPT} 276 | 277 | echo -e "Bitcart environment variables successfully saved in $BASH_PROFILE_SCRIPT\n" 278 | echo -e "Bitcart deployment config saved in $BITCART_DEPLOYMENT_CONFIG\n" 279 | 280 | bitcart_update_docker_env 281 | 282 | echo -e "Bitcart docker-compose parameters saved in $BITCART_ENV_FILE\n" 283 | 284 | . "$BASH_PROFILE_SCRIPT" 285 | 286 | # Try to install docker 287 | if ! [[ -x "$(command -v docker)" ]]; then 288 | if ! [[ -x "$(command -v curl)" ]]; then 289 | apt-get update 2>error 290 | apt-get install -y \ 291 | curl \ 292 | apt-transport-https \ 293 | ca-certificates \ 294 | software-properties-common \ 295 | 2>error 296 | fi 297 | if [[ "$(uname -m)" == "x86_64" ]] || [[ "$(uname -m)" == "armv7l" ]] || [[ "$(uname -m)" == "aarch64" ]] || [[ "$(uname -m)" == "arm64" ]]; then 298 | if [[ "$OSTYPE" == "darwin"* ]]; then 299 | # Mac OS 300 | if ! [[ -x "$(command -v brew)" ]]; then 301 | # Brew is not installed, install it now 302 | echo "Homebrew, the package manager for Mac OS, is not installed. Installing it now..." 303 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 304 | fi 305 | if [[ -x "$(command -v brew)" ]]; then 306 | echo "Homebrew is installed, but Docker isn't. Installing it now using brew..." 307 | # Brew is installed, install docker now 308 | brew install --cask docker 309 | # Launch UI and wait for user to finish installation 310 | nohup open /Applications/Docker.app >/dev/null 2>&1 & 311 | echo "Please finish Docker installation from it's UI" 312 | while ! docker ps >/dev/null 2>&1; do 313 | sleep 5 314 | echo "Waiting for docker to come up" 315 | done 316 | fi 317 | else 318 | # Not Mac OS 319 | echo "Trying to install docker..." 320 | curl -fsSL https://get.docker.com -o get-docker.sh 321 | chmod +x get-docker.sh 322 | sh get-docker.sh 323 | rm get-docker.sh 324 | fi 325 | else 326 | echo "Unsupported architecture $(uname -m)" 327 | exit 1 328 | fi 329 | 330 | if [[ "$(uname -m)" == "armv7l" ]] && cat "/etc/os-release" 2>/dev/null | grep -q "VERSION_CODENAME=buster" 2>/dev/null; then 331 | if [[ "$(apt list libseccomp2 2>/dev/null)" == *" 2.3"* ]]; then 332 | echo "Outdated version of libseccomp2, updating... (see: https://blog.samcater.com/fix-workaround-rpi4-docker-libseccomp2-docker-20/)" 333 | # https://blog.samcater.com/fix-workaround-rpi4-docker-libseccomp2-docker-20/ 334 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 04EE7237B7D453EC 648ACFD622F3D138 335 | echo 'deb http://httpredir.debian.org/debian buster-backports main contrib non-free' | sudo tee -a /etc/apt/sources.list.d/debian-backports.list 336 | apt update 337 | apt install libseccomp2 -t buster-backports 338 | fi 339 | fi 340 | fi 341 | 342 | check_docker_compose 343 | 344 | if ! [[ -x "$(command -v docker)" ]]; then 345 | echo "Failed to install 'docker'. Please install docker manually, then retry." 346 | exit 1 347 | fi 348 | 349 | if ! [[ $(docker compose version 2>/dev/null) ]]; then 350 | echo "Failed to install 'docker compose'. Please install docker compose manually, then retry." 351 | exit 1 352 | fi 353 | 354 | # Generate the docker compose 355 | if ! ./build.sh; then 356 | echo "Failed to generate the docker-compose" 357 | exit 1 358 | fi 359 | 360 | # Schedule for reboot 361 | if $STARTUP_REGISTER && [[ -x "$(command -v systemctl)" ]]; then 362 | # Use systemd 363 | if [[ -e "/etc/init/start_containers.conf" ]]; then 364 | echo -e "Uninstalling upstart script /etc/init/start_containers.conf" 365 | rm "/etc/init/start_containers.conf" 366 | initctl reload-configuration 367 | fi 368 | echo "Adding bitcart$SCRIPTS_POSTFIX.service to systemd" 369 | echo " 370 | [Unit] 371 | Description=Bitcart service 372 | After=docker.service network-online.target 373 | Requires=docker.service network-online.target 374 | [Service] 375 | Type=oneshot 376 | RemainAfterExit=yes 377 | ExecStart=/bin/bash -c '. \"$BASH_PROFILE_SCRIPT\" && cd \"$BITCART_BASE_DIRECTORY\" && ./start.sh' 378 | ExecStop=/bin/bash -c '. \"$BASH_PROFILE_SCRIPT\" && cd \"$BITCART_BASE_DIRECTORY\" && ./stop.sh' 379 | ExecReload=/bin/bash -c '. \"$BASH_PROFILE_SCRIPT\" && cd \"$BITCART_BASE_DIRECTORY\" && ./stop.sh && ./start.sh' 380 | [Install] 381 | WantedBy=multi-user.target" >"/etc/systemd/system/bitcart$SCRIPTS_POSTFIX.service" 382 | 383 | if ! [[ -f "/etc/docker/daemon.json" ]] && [ -w "/etc/docker" ]; then 384 | echo "{ 385 | \"log-driver\": \"json-file\", 386 | \"log-opts\": {\"max-size\": \"5m\", \"max-file\": \"3\"} 387 | }" >/etc/docker/daemon.json 388 | echo "Setting limited log files in /etc/docker/daemon.json" 389 | $SYSTEMD_RELOAD && $START && systemctl restart docker 390 | fi 391 | 392 | echo -e "Bitcart systemd configured in /etc/systemd/system/bitcart$SCRIPTS_POSTFIX.service\n" 393 | if $SYSTEMD_RELOAD; then 394 | systemctl daemon-reload 395 | systemctl enable "bitcart$SCRIPTS_POSTFIX" 396 | if $START; then 397 | echo "Bitcart starting... this can take 5 to 10 minutes..." 398 | systemctl start "bitcart$SCRIPTS_POSTFIX" 399 | echo "Bitcart started" 400 | fi 401 | else 402 | systemctl --no-reload enable "bitcart$SCRIPTS_POSTFIX" 403 | fi 404 | elif $STARTUP_REGISTER && [[ -x "$(command -v initctl)" ]]; then 405 | # Use upstart 406 | echo "Using upstart" 407 | echo " 408 | # File is saved under /etc/init/start_containers.conf 409 | # After file is modified, update config with : $ initctl reload-configuration 410 | description \"Start containers (see http://askubuntu.com/a/22105 and http://askubuntu.com/questions/612928/how-to-run-docker-compose-at-bootup)\" 411 | start on filesystem and started docker 412 | stop on runlevel [!2345] 413 | # if you want it to automatically restart if it crashes, leave the next line in 414 | # respawn # might cause over charge 415 | script 416 | . \"$BASH_PROFILE_SCRIPT\" 417 | cd \"$BITCART_BASE_DIRECTORY\" 418 | ./start.sh 419 | end script" >/etc/init/start_containers.conf 420 | echo -e "Bitcart upstart configured in /etc/init/start_containers.conf\n" 421 | 422 | if $START; then 423 | initctl reload-configuration 424 | fi 425 | fi 426 | 427 | install_tooling 428 | if [[ "$first_setup" = true ]]; then 429 | bitcart_pull 430 | fi 431 | 432 | if $START; then 433 | ./start.sh 434 | fi 435 | 436 | echo "Setup done." 437 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . helpers.sh 4 | load_env 5 | 6 | cd "$BITCART_BASE_DIRECTORY" 7 | bitcart_start 8 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . helpers.sh 4 | load_env 5 | 6 | cd "$BITCART_BASE_DIRECTORY" 7 | bitcart_stop 8 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | . helpers.sh 6 | load_env 7 | 8 | cd "$BITCART_BASE_DIRECTORY" 9 | 10 | if [[ "$1" != "--skip-git-pull" ]]; then 11 | git pull --force 12 | exec "./update.sh" --skip-git-pull 13 | return 14 | fi 15 | 16 | if ! [ -f "/etc/docker/daemon.json" ] && [ -w "/etc/docker" ]; then 17 | echo "{ 18 | \"log-driver\": \"json-file\", 19 | \"log-opts\": {\"max-size\": \"5m\", \"max-file\": \"3\"} 20 | }" >/etc/docker/daemon.json 21 | echo "Setting limited log files in /etc/docker/daemon.json" 22 | fi 23 | 24 | if ! ./build.sh; then 25 | echo "Failed to generate the docker-compose" 26 | exit 1 27 | fi 28 | 29 | . helpers.sh 30 | check_docker_compose 31 | install_tooling 32 | bitcart_update_docker_env 33 | bitcart_pull 34 | bitcart_start 35 | 36 | set +e 37 | docker image prune -f --filter "label=org.bitcart.image" --filter "label!=org.bitcart.image=docker-compose-generator" 38 | --------------------------------------------------------------------------------