├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── deno.json ├── deno.lock └── usr_local_bin ├── create-docker-ct └── create-docker-ct-files ├── cli.ts ├── ct-template.ts ├── deps.ts ├── fn.ts ├── network.ts ├── os.ts ├── parse-columns.ts ├── prompt.ts ├── read-from-url.ts ├── run.ts ├── storage.ts └── template ├── alpine-318-install ├── debian-12-install ├── docker-compose.yml ├── summary.md └── ubuntu-2204-install /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.vscode/ 3 | /.idea/ 4 | /*.log 5 | .*.swp 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hugo Josefson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-docker-ct for Proxmox VE 2 | 3 | Scripts and notes for running one `docker-compose.yml` application using 4 | [Docker Compose](https://github.com/docker/compose) inside an unprivileged LXC 5 | container, on 6 | [Proxmox Virtual Environment (PVE)](https://www.proxmox.com/en/proxmox-ve). 7 | 8 | Use this if you want: 9 | 10 | - one unprivileged LXC container per one application (one `docker-compose.yml`) 11 | - a bind-mounted directory into each container/application, where 12 | - its `docker-compose.yml` lives there 13 | - its configuration lives there 14 | - its data lives there 15 | 16 | This means, that to deploy a new application, you... 17 | 18 | 1. run the script `create-docker-ct` with the name of your new application as 19 | argument, 20 | 2. create or download its `docker-compose.yml` file into the directory the 21 | script tells you, 22 | 3. start the newly created LXC container. 23 | 24 | ## Install 25 | 26 | ### Prerequisites 27 | 28 | - Install PVE (for example via 29 | https://github.com/hugojosefson/proxmox-root-on-encrypted-zfs). 30 | 31 | ### Install create-docker-ct 32 | 33 | ```sh 34 | curl -sSfL https://github.com/hugojosefson/proxmox-create-docker-ct/tarball/main \ 35 | | tar -xzvC /usr/local/bin --wildcards "*/usr_local_bin/" --strip-components=2 36 | ``` 37 | 38 | ## Create an app CT 39 | 40 | ``` 41 | USAGE: 42 | 43 | create-docker-ct --help This help message 44 | create-docker-ct Create a CT from alpine-3.18-default_20230607_amd64.tar.xz 45 | create-docker-ct [] Create a CT from specified base template 46 | 47 | EXAMPLE: 48 | 49 | create-docker-ct my-service 50 | create-docker-ct my-service alpine-3.18-default_20230607_amd64.tar.xz 51 | create-docker-ct my-service ubuntu-22.04-standard_22.04-1_amd64.tar.zst 52 | create-docker-ct my-service debian-12-standard_12.0-1_amd64.tar.zst 53 | ``` 54 | 55 | Run this script: 56 | 57 | ```sh 58 | create-docker-ct 59 | ``` 60 | 61 | ...where `` is the name of the app. 62 | 63 | The application's name will be its directory name, and its hostname. 64 | 65 | The script will output: 66 | 67 | - the app CT's VMID number, and 68 | - the directory mounted as `/appdata` inside the CT, where its 69 | `docker-compose.yml` etc. lives. 70 | 71 | ## Deploy/redeploy application inside 72 | 73 | Edit the CT's `docker-compose.yml` file, or overwrite it with what you want to 74 | deploy. 75 | 76 | Start or restart the app CT: 77 | 78 | ```sh 79 | pct start 80 | ``` 81 | 82 | ```sh 83 | pct restart 84 | ``` 85 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "all": "deno fmt && deno lint && deno task check", 4 | "all-reload": "deno fmt && deno lint && deno task check --reload && deno task test --reload", 5 | "check": "sh -c '$(command -v fd || command -v fdfind) '\"'\"'\\.(mj|j|t)sx?$'\"'\"' --hidden --threads=1 --exec deno check {}'", 6 | "udd": "sh -c 'deno run --allow-read=. --allow-write=. --allow-net --allow-run=deno https://deno.land/x/udd@0.8.2/main.ts --test \"deno task all\" $($(command -v fd || command -v fdfind) '\"'\"'(\\.(mj|j|t)sx?|^deno.jsonc?|^create-docker-ct)$'\"'\"')'" 7 | }, 8 | "fmt": { 9 | "exclude": [ 10 | ".isolate-in-docker" 11 | ] 12 | }, 13 | "lint": { 14 | "exclude": [ 15 | ".isolate-in-docker" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "npm:parse-columns@3.0.0": "npm:parse-columns@3.0.0" 6 | }, 7 | "npm": { 8 | "array-uniq@3.0.0": { 9 | "integrity": "sha512-T/3qyw9JTDHjj+aIo4uQyHCAoG1DkFqFViq0e6uPzkXwT74MEPsmQ30rxx8x9+yjBQ3KJ2bXOb2bBKC1FwEjdw==", 10 | "dependencies": {} 11 | }, 12 | "arrify@3.0.0": { 13 | "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", 14 | "dependencies": {} 15 | }, 16 | "clone-regexp@3.0.0": { 17 | "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", 18 | "dependencies": { 19 | "is-regexp": "is-regexp@3.1.0" 20 | } 21 | }, 22 | "escape-string-regexp@5.0.0": { 23 | "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", 24 | "dependencies": {} 25 | }, 26 | "execall@3.0.0": { 27 | "integrity": "sha512-FaJeg2uWc8ADWnAnoDbxhAAr4U/j86ujlojnEpnBHQ4FM2viZKNjjyO2O6AKG6+9usZAnqsI4+1+w6vzx0x4uw==", 28 | "dependencies": { 29 | "clone-regexp": "clone-regexp@3.0.0" 30 | } 31 | }, 32 | "is-regexp@3.1.0": { 33 | "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", 34 | "dependencies": {} 35 | }, 36 | "num-sort@3.0.0": { 37 | "integrity": "sha512-N5dLIfqCzlJm7M14KqmX/sl+6Zg5WH0E04HKfuVHbPj9jIaY1T2zuCS+xe0qeT/YN3UpYQ6lIIXcE/3Xbwg3Xw==", 38 | "dependencies": {} 39 | }, 40 | "parse-columns@3.0.0": { 41 | "integrity": "sha512-GonZBwJI7aEd+gEpZK4oZa0wL8vIMgEfalr2qKtOh1NUx+xJuqtyF2ssu97REtUTZLip8LpHbUn4RPDftzlwrw==", 42 | "dependencies": { 43 | "escape-string-regexp": "escape-string-regexp@5.0.0", 44 | "execall": "execall@3.0.0", 45 | "split-at": "split-at@3.0.0" 46 | } 47 | }, 48 | "split-at@3.0.0": { 49 | "integrity": "sha512-CXacHHs293c+c6+V5LYOqlZq5oohV44vXP1vYex+8HoDpGdhEMpqh7gj6bOYxMVJW0YzDi/OuOz4Ghux+v19yw==", 50 | "dependencies": { 51 | "array-uniq": "array-uniq@3.0.0", 52 | "arrify": "arrify@3.0.0", 53 | "num-sort": "num-sort@3.0.0" 54 | } 55 | } 56 | } 57 | }, 58 | "remote": { 59 | "https://deno.land/std@0.196.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", 60 | "https://deno.land/std@0.196.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", 61 | "https://deno.land/std@0.196.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", 62 | "https://deno.land/std@0.196.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", 63 | "https://deno.land/std@0.196.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", 64 | "https://deno.land/std@0.196.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 65 | "https://deno.land/std@0.196.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", 66 | "https://deno.land/std@0.196.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", 67 | "https://deno.land/std@0.196.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", 68 | "https://deno.land/std@0.196.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", 69 | "https://deno.land/std@0.196.0/path/mod.ts": "f065032a7189404fdac3ad1a1551a9ac84751d2f25c431e101787846c86c79ef", 70 | "https://deno.land/std@0.196.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", 71 | "https://deno.land/std@0.196.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", 72 | "https://deno.land/std@0.196.0/path/win32.ts": "4fca292f8d116fd6d62f243b8a61bd3d6835a9f0ede762ba5c01afe7c3c0aa12", 73 | "https://deno.land/std@0.202.0/collections/map_keys.ts": "3dd2cf3a940f1432628cb0252b919d268e8bb38fe8bcd78153a440909173ae98", 74 | "https://deno.land/std@0.202.0/collections/map_values.ts": "c88f306b2b3ec84043e16ab4e1b062055ab32cf4d68bb1d7447afaafa0a1b3bf", 75 | "https://deno.land/std@0.202.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 76 | "https://deno.land/std@0.202.0/path/_dirname.ts": "355e297236b2218600aee7a5301b937204c62e12da9db4b0b044993d9e658395", 77 | "https://deno.land/std@0.202.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", 78 | "https://deno.land/std@0.202.0/path/_util.ts": "4e191b1bac6b3bf0c31aab42e5ca2e01a86ab5a0d2e08b75acf8585047a86221", 79 | "https://deno.land/std@0.202.0/path/dirname.ts": "b6533f4ee4174a526dec50c279534df5345836dfdc15318400b08c62a62a39dd", 80 | "https://deno.land/std@0.208.0/collections/map_keys.ts": "3dd2cf3a940f1432628cb0252b919d268e8bb38fe8bcd78153a440909173ae98", 81 | "https://deno.land/std@0.208.0/collections/map_values.ts": "c88f306b2b3ec84043e16ab4e1b062055ab32cf4d68bb1d7447afaafa0a1b3bf", 82 | "https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946", 83 | "https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", 84 | "https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", 85 | "https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65", 86 | "https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", 87 | "https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361", 88 | "https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5", 89 | "https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472", 90 | "https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54", 91 | "https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94", 92 | "https://deno.land/std@0.96.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", 93 | "https://deno.land/std@0.96.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7", 94 | "https://deno.land/std@0.96.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", 95 | "https://deno.land/std@0.96.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", 96 | "https://deno.land/std@0.96.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", 97 | "https://deno.land/std@0.96.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a", 98 | "https://deno.land/std@0.96.0/path/glob.ts": "314ad9ff263b895795208cdd4d5e35a44618ca3c6dd155e226fb15d065008652", 99 | "https://deno.land/std@0.96.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", 100 | "https://deno.land/std@0.96.0/path/posix.ts": "f56c3c99feb47f30a40ce9d252ef6f00296fa7c0fcb6dd81211bdb3b8b99ca3b", 101 | "https://deno.land/std@0.96.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", 102 | "https://deno.land/std@0.96.0/path/win32.ts": "77f7b3604e0de40f3a7c698e8a79e7f601dc187035a1c21cb1e596666ce112f8", 103 | "https://deno.land/std@0.97.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", 104 | "https://deno.land/std@0.97.0/bytes/mod.ts": "1ae1ccfe98c4b979f12b015982c7444f81fcb921bea7aa215bf37d84f46e1e13", 105 | "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4", 106 | "https://deno.land/std@0.97.0/io/buffer.ts": "ed3528e299fd1e0dc056c4b5005a07b28c3dabad2595f077a562ff7b06fe89a5", 107 | "https://deno.land/std@0.97.0/io/util.ts": "318be78b7954da25f0faffe123fef0d9423ea61af98467e860c06b60265eff6d", 108 | "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3", 109 | "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f", 110 | "https://deno.land/x/case@2.1.1/camelCase.ts": "0808961e69e1883c6e94faf85e333196a464ceba46ddc76984d7b8ce9e26a43a", 111 | "https://deno.land/x/case@2.1.1/constantCase.ts": "e50eaa8a45cf68417902fda218f03a99e45e2a30f7fce2db325306c84e8c3f0a", 112 | "https://deno.land/x/case@2.1.1/dotCase.ts": "ef3977567057e6c2a4e1b5ca09ec34b9e0788c1a50246187c11f435a468be17e", 113 | "https://deno.land/x/case@2.1.1/headerCase.ts": "4440a251a196fb6d7090213c39e4c6c50ddecab90c14ec91495bee3563082d3c", 114 | "https://deno.land/x/case@2.1.1/lowerCase.ts": "86d5533f9587ed60003181591e40e648838c23f371edfa79d00288153d113b16", 115 | "https://deno.land/x/case@2.1.1/lowerFirstCase.ts": "74e8ebe10f3c54a9d8e81d21127a20fcfb34c446e9c49b2a335162babd652de9", 116 | "https://deno.land/x/case@2.1.1/mod.ts": "28b0b1329c7b18730799ac05627a433d9547c04b9bfb429116247c60edecd97b", 117 | "https://deno.land/x/case@2.1.1/normalCase.ts": "6a8b924da9ab0790d99233ae54bfcfc996d229cb91b2533639fe20972cc33dac", 118 | "https://deno.land/x/case@2.1.1/paramCase.ts": "cf3101c59fd4f16ee14fd09985adb7fa3dbfb194f102499986f7407995202394", 119 | "https://deno.land/x/case@2.1.1/pascalCase.ts": "f68936d584182c8f4b341238bd9c424b1a740bfba3ab2847234f57a4c205f1df", 120 | "https://deno.land/x/case@2.1.1/pathCase.ts": "76e5f437369f8981e17ecdb07870e1c9c8d2d0357f1bf3377e2b0eb132159ed8", 121 | "https://deno.land/x/case@2.1.1/sentenceCase.ts": "f3355985a6a41e088c8c9be80219e5e055a68ad9a987df084a57ee437a0871c5", 122 | "https://deno.land/x/case@2.1.1/snakeCase.ts": "ee2ab4e2c931d30bb79190d090c21eb5c00d1de1b7a9a3e7f33e035ae431333b", 123 | "https://deno.land/x/case@2.1.1/swapCase.ts": "d9b5ee5b8e1ee3d202cbce32a504dde976b9002ed94b4527054a004179905dbb", 124 | "https://deno.land/x/case@2.1.1/titleCase.ts": "36d3fc73df58712240a74b04d84191ac22dd2866bef3838b02157f8e46cb0ecb", 125 | "https://deno.land/x/case@2.1.1/types.ts": "8e2bd6edaa27c0d1972c0d5b76698564740f37b4d3787d58d1fb5f48de611e61", 126 | "https://deno.land/x/case@2.1.1/upperCase.ts": "e6a1a3dea30e17013aa212ec237b35e2dd5c5c0b1501778c07db66ff0bbe4372", 127 | "https://deno.land/x/case@2.1.1/upperFirstCase.ts": "2b083db95744105a4f4accefe66dcd09390634514abf757fc580a7ebad58bf4f", 128 | "https://deno.land/x/case@2.1.1/vendor/camelCaseRegexp.ts": "7d9ff02aad4ab6429eeab7c7353f7bcdd6cc5909a8bd3dda97918c8bbb7621ae", 129 | "https://deno.land/x/case@2.1.1/vendor/camelCaseUpperRegexp.ts": "292de54a698370f90adcdf95727993d09888b7f33d17f72f8e54ba75f7791787", 130 | "https://deno.land/x/case@2.1.1/vendor/nonWordRegexp.ts": "c1a052629a694144b48c66b0175a22a83f4d61cb40f4e45293fc5d6b123f927e", 131 | "https://deno.land/x/case@2.2.0/camelCase.ts": "b9a4cf361a7c9740ecb75e00b5e2c006bd4e5d40e442d26c5f2760286fa66796", 132 | "https://deno.land/x/case@2.2.0/constantCase.ts": "c698fc32f00cd267c1684b1d413d784260d7e7798f2bf506803e418497d839b5", 133 | "https://deno.land/x/case@2.2.0/dotCase.ts": "03ae55d5635e6a4ca894a003d9297cd9cd283af2e7d761dd3de13663849a9423", 134 | "https://deno.land/x/case@2.2.0/headerCase.ts": "3f6c8ab2ab30a88147326bce28a00d1189ec98ab61c83ab72ce79e852afddc4a", 135 | "https://deno.land/x/case@2.2.0/lowerCase.ts": "d75eb55cadfa589f9f2a973924a8a209054477d9574da669410f4d817ab25b41", 136 | "https://deno.land/x/case@2.2.0/lowerFirstCase.ts": "b001efbf2d715b53d066b22cdbf8eda7f99aa7108e3d12fb02f80d499bae93d9", 137 | "https://deno.land/x/case@2.2.0/mod.ts": "28b0b1329c7b18730799ac05627a433d9547c04b9bfb429116247c60edecd97b", 138 | "https://deno.land/x/case@2.2.0/normalCase.ts": "085c8b6f9d69283c8b86f2e504d43278c2be8b7e56a3ed8d4a5f395e398bdc29", 139 | "https://deno.land/x/case@2.2.0/paramCase.ts": "a234c9c17dfbaddee647b6571c2c90e8f6530123fed26c4546f4063d67c1609f", 140 | "https://deno.land/x/case@2.2.0/pascalCase.ts": "4b3ef0a68173871a821d306d4067e8f72d42aeeef1eea6aeab30af6bfa3d7427", 141 | "https://deno.land/x/case@2.2.0/pathCase.ts": "330a34b4df365b0291d8e36158235340131730aae6f6add66962ed2d0fbead4a", 142 | "https://deno.land/x/case@2.2.0/sentenceCase.ts": "b312cef147a13b58ffdf3c36bf55b33aa8322c91f4aa9b32318f3911bb92327f", 143 | "https://deno.land/x/case@2.2.0/snakeCase.ts": "e5ac1e08532ca397aa3150a0a3255d59f63a186d934e5094a8ffd24cbca7f955", 144 | "https://deno.land/x/case@2.2.0/swapCase.ts": "bb03742fcf613f733890680ceca1b39b65ed290f36a317fcd47edd517c4e0e1e", 145 | "https://deno.land/x/case@2.2.0/titleCase.ts": "c287131ea2c955e67cdd5cf604de96d31a8e2813305759922b9ed27e3be354e7", 146 | "https://deno.land/x/case@2.2.0/types.ts": "8e2bd6edaa27c0d1972c0d5b76698564740f37b4d3787d58d1fb5f48de611e61", 147 | "https://deno.land/x/case@2.2.0/upperCase.ts": "6cca267bb04d098bf4abf21e42e60c3e68ede89b12e525643c6b6eff3e10de34", 148 | "https://deno.land/x/case@2.2.0/upperFirstCase.ts": "b964c2d8d3a85c78cd35f609135cbde99d84b9522a21470336b5af80a37facbd", 149 | "https://deno.land/x/case@2.2.0/vendor/camelCaseRegexp.ts": "7d9ff02aad4ab6429eeab7c7353f7bcdd6cc5909a8bd3dda97918c8bbb7621ae", 150 | "https://deno.land/x/case@2.2.0/vendor/camelCaseUpperRegexp.ts": "292de54a698370f90adcdf95727993d09888b7f33d17f72f8e54ba75f7791787", 151 | "https://deno.land/x/case@2.2.0/vendor/nonWordRegexp.ts": "c1a052629a694144b48c66b0175a22a83f4d61cb40f4e45293fc5d6b123f927e", 152 | "https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", 153 | "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/ansi_escapes.ts": "193b3c3a4e520274bd8322ca4cab1c3ce38070bed1898cb2ade12a585dddd7c9", 154 | "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/chain.ts": "eca61b1b64cad7b9799490c12c7aa5538d0f63ac65a73ddb6acac8b35f0a5323", 155 | "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/cursor_position.ts": "caa008d29f7a904908bda514f9839bfbb7a93f2d5f5580501675b646d26a87ff", 156 | "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/deps.ts": "f48ae5d066684793f4a203524db2a9fd61f514527934b458006f3e57363c0215", 157 | "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/tty.ts": "155aacdcb7dc00f3f95352616a2415c622ffb88db51c5934e5d2e8341eab010b", 158 | "https://deno.land/x/cliffy@v1.0.0-rc.3/keycode/_key_codes.ts": "917f0a2da0dbace08cf29bcfdaaa2257da9fe7e705fff8867d86ed69dfb08cfe", 159 | "https://deno.land/x/cliffy@v1.0.0-rc.3/keycode/key_code.ts": "730fa675ca12fc2a99ba718aa8dbebb1f2c89afd47484e30ef3cb705ddfca367", 160 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_figures.ts": "e22413ddd51bb271b6b861a058742e83aaa3f62c14e8162cb73ae6f047062f51", 161 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_input.ts": "870dad97077582439cee26cb19aec123b4850376331338abdc64a91224733cdc", 162 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_list.ts": "8b0bea4521b1e2f62c564e0d3764a63264043694f4228bb0bc0b63ce129ef33b", 163 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_prompt.ts": "4c9d9cdeda749620a3f5332524df13d083e2d59b1ed90a003f43cd0991a75a10", 164 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_utils.ts": "498ae639d7666599d612b615ee85de9103b3c3a913d5196f6b265072674258c7", 165 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/deps.ts": "2560142f070bb2668e2e8a74683c799461648b9aad01bbf36b3cad3851d712e6", 166 | "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/select.ts": "c10902aeaca02a55d9b846934958dd166ee39c741faebdaa9800689e402186cf", 167 | "https://deno.land/x/file_fetch@0.2.0/mod.ts": "ad1cfcddc24905cd66101842f31e09ee260054d7b5a9c349f86e1cd6823032db", 168 | "https://deno.land/x/media_types@v2.8.4/db.ts": "22db9bd6eb24934105098e9fce27d2746ef3861cd2668edef749c83c60acd60b", 169 | "https://deno.land/x/media_types@v2.8.4/deps.ts": "2dd71d14adbe5f3c837bd79e010d9706895a2cebdcd50d13ab1debe42b32ff7f", 170 | "https://deno.land/x/media_types@v2.8.4/mod.ts": "d63583b978d32eff8b76e1ae5d83cba2fb27baa90cc1bcb0ad15a06122ea8c19", 171 | "https://deno.land/x/run_simple@2.1.0/mod.ts": "107de176c82574d128164e31ad7811a03ef85d67919061eed3b8f3bcecec1885", 172 | "https://deno.land/x/run_simple@2.1.0/src/fn.ts": "1faf24c00a2676c717f99c5e2d2ff9ccee6191fb09b94496522f9700af7acd15", 173 | "https://deno.land/x/run_simple@2.1.0/src/os.ts": "47820a9a43f99006ef8e6a0c4c353406cda1155e18b6ced50d0863c2a11863e9", 174 | "https://deno.land/x/run_simple@2.1.0/src/run.ts": "c7ce54de495dd4cc171adddb3e9412ad90dac8b6f44f46439119b7aa309ee6e0", 175 | "https://deno.land/x/run_simple@2.2.0/mod.ts": "3bd3206b7664491b73799856196a1081e8d4dbb4877de9d5883c149798906ae9", 176 | "https://deno.land/x/run_simple@2.2.0/src/fn.ts": "1faf24c00a2676c717f99c5e2d2ff9ccee6191fb09b94496522f9700af7acd15", 177 | "https://deno.land/x/run_simple@2.2.0/src/os.ts": "47820a9a43f99006ef8e6a0c4c353406cda1155e18b6ced50d0863c2a11863e9", 178 | "https://deno.land/x/run_simple@2.2.0/src/run.ts": "dfcb9fb07a7884770f3277c8144cbc0c4202185607b37218207e884f13ff4159", 179 | "https://deno.land/x/semver@v1.4.1/mod.ts": "0b79c87562eb8a1f008ab0d98f8bb60076dd65bc06f1f8fdfac2d2dab162c27b" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | // 2>/dev/null;DENO_VERSION_RANGE="^1.38.3";DENO_RUN_ARGS="--allow-all";set -e;V="$DENO_VERSION_RANGE";A="$DENO_RUN_ARGS";h(){ [ -x "$(command -v $1 2>&1)" ];};g(){ u="$([ $(id -u) != 0 ]&&echo sudo||:)";if h brew;then echo "brew install $1";elif h apt;then echo "($u apt update && $u DEBIAN_FRONTEND=noninteractive apt install -y $1)";elif h yum;then echo "$u yum install -y $1";elif h pacman;then echo "$u pacman -yS --noconfirm $1";elif h opkg-install;then echo "$u opkg-install $1";fi;};p(){ q="$(g $1)";if [ -z "$q" ];then echo "Please install '$1' manually, then try again.">&2;exit 1;fi;eval "o=\"\$(set +o)\";set -x;$q;set +x;eval \"\$o\"">&2;};f(){ h "$1"||p "$1";};U="$(echo "$V"|tr -d '\n'|od -An -tx1|tr ' ' %|tr -d '\n')";D="$(command -v deno||true)";t(){ d="$(mktemp)";rm "${d}";dirname "${d}";};a(){ [ -n $D ];};s(){ a&&[ -x "$R/deno" ]&&[ "$R/deno" = "$D" ]&&return;deno eval "import{satisfies as e}from'https://deno.land/x/semver@v1.4.1/mod.ts';Deno.exit(e(Deno.version.deno,'$V')?0:1);">/dev/null 2>&1;};e(){ R="$(t)/deno-range-$V/bin";mkdir -p "$R";export PATH="$R:$PATH";[ -x "$R/deno" ]&&return;a&&s&&([ -L "$R/deno" ]||ln -s "$D" "$R/deno")&&return;f curl;v="$(curl -sSfL "https://semver-version.deno.dev/api/github/denoland/deno/$U")";i="$(t)/deno-$v";[ -L "$R/deno" ]||ln -s "$i/bin/deno" "$R/deno";s && return;f unzip;([ "${A#*-q}" != "$A" ]&&exec 2>/dev/null;curl -fsSL https://deno.land/install.sh|DENO_INSTALL="$i" sh -s $DENO_INSTALL_ARGS "$v">&2);};e;exec "$R/deno" run $A "$0" "$@" 3 | 4 | import("./create-docker-ct-files/cli.ts"); 5 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * via https://github.com/hugojosefson/proxmox-create-docker-ct 3 | * License: MIT 4 | * Copyright (c) 2022 Hugo Josefson 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | */ 9 | 10 | import { 11 | createCt, 12 | ensureExistsCtTemplate, 13 | getShortName, 14 | } from "./ct-template.ts"; 15 | import { 16 | CONTENT_CT_ROOTDIR, 17 | CONTENT_CT_TEMPLATE, 18 | getStorage, 19 | } from "./storage.ts"; 20 | import { VMID } from "./os.ts"; 21 | 22 | /** template filenames we support. the first one is default if user does not specify. */ 23 | const compatibleTemplates = [ 24 | "alpine-3.18-default_20230607_amd64.tar.xz", 25 | "ubuntu-22.04-standard_22.04-1_amd64.tar.zst", 26 | "debian-12-standard_12.0-1_amd64.tar.zst", 27 | ]; 28 | 29 | const defaultTemplate = compatibleTemplates[0]; 30 | const [name, CT_BASE_TEMPLATE_FILENAME = defaultTemplate] = Deno.args; 31 | 32 | function usageAndExit(code = 2): never { 33 | console.error(`USAGE: 34 | 35 | create-docker-ct --help This help message 36 | create-docker-ct Create a CT from ${defaultTemplate} 37 | create-docker-ct [] Create a CT from specified base template 38 | 39 | EXAMPLE: 40 | 41 | create-docker-ct my-service 42 | ${ 43 | compatibleTemplates.map((filename) => 44 | `create-docker-ct my-service ${filename}` 45 | ).join("\n ") 46 | } 47 | `); 48 | Deno.exit(code); 49 | } 50 | 51 | if (!name || name === "--help") { 52 | usageAndExit(); 53 | } 54 | 55 | const DOCKER_CT_TEMPLATE_NAME = "docker-ct-" + 56 | getShortName(CT_BASE_TEMPLATE_FILENAME); 57 | const DOCKER_CT_TEMPLATE_FILENAME = `docker-ct-${CT_BASE_TEMPLATE_FILENAME}`; 58 | 59 | const templateVmid: VMID = await ensureExistsCtTemplate({ 60 | baseTemplateStorage: () => 61 | getStorage( 62 | CONTENT_CT_TEMPLATE, 63 | CT_BASE_TEMPLATE_FILENAME, 64 | ), 65 | baseFilename: CT_BASE_TEMPLATE_FILENAME, 66 | storage: () => 67 | getStorage( 68 | CONTENT_CT_ROOTDIR, 69 | DOCKER_CT_TEMPLATE_FILENAME, 70 | ), 71 | name: DOCKER_CT_TEMPLATE_NAME, 72 | filename: DOCKER_CT_TEMPLATE_FILENAME, 73 | }); 74 | 75 | const { vmid, appdataDir } = await createCt({ 76 | templateVmid, 77 | name, 78 | storage: () => 79 | getStorage( 80 | undefined, 81 | `appdata for CT ${name}`, 82 | ), 83 | }); 84 | 85 | console.log(vmid); 86 | console.log(appdataDir); 87 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/ct-template.ts: -------------------------------------------------------------------------------- 1 | import { getCtTemplate, PctEntry, VMID } from "./os.ts"; 2 | import { dirname, run } from "./deps.ts"; 3 | import { CONTENT_CT_TEMPLATE, StorageRow } from "./storage.ts"; 4 | import { readFromUrl } from "./read-from-url.ts"; 5 | import { getNetworkInterface } from "./network.ts"; 6 | import { extractShebangCommand, extractValueFromPveShGet } from "./fn.ts"; 7 | 8 | /** 9 | * Extracts a short alphanumeric (plus dash) name from a ct base template filename, for example "ubuntu-2204", or "alpine-316". 10 | * @param filename 11 | */ 12 | export function getShortName(filename: string): string { 13 | return filename.split("-").slice(0, 2).join("-") 14 | .replaceAll(/[^a-z0-9-]/g, ""); 15 | } 16 | 17 | async function getAppdataDir( 18 | storage: Pick, 19 | options: Pick, 20 | ) { 21 | const path = extractValueFromPveShGet( 22 | await run( 23 | ["pvesh", "get", `/storage/${storage.name}`], 24 | ), 25 | [ 26 | "path", 27 | "mountpoint", 28 | ], 29 | ); 30 | if (!path) { 31 | throw new Error( 32 | `Could not find path or mountpoint for storage ${storage.name}.`, 33 | ); 34 | } 35 | 36 | return `${path}/appdata/${options.name}`; 37 | } 38 | 39 | type CtTemplateOptions = { 40 | baseTemplateStorage: () => Promise; 41 | baseFilename: string; 42 | storage: () => Promise; 43 | name: string; 44 | filename: string; 45 | vmid?: VMID; 46 | cores?: number; 47 | memoryMegabytes?: number; 48 | }; 49 | 50 | async function readInstallScript(baseFilename: string): Promise { 51 | const url = new URL( 52 | `template/${getShortName(baseFilename)}-install`, 53 | import.meta.url, 54 | ); 55 | try { 56 | return await readFromUrl(url); 57 | } catch (e) { 58 | if (e instanceof Deno.errors.NotFound) { 59 | throw new Error( 60 | `Could not find install script for ${baseFilename}. 61 | It should be at ${url}. 62 | Only install scripts in ${dirname(url.toString())}/ are supported.`, 63 | ); 64 | } 65 | throw e; 66 | } 67 | } 68 | 69 | export async function createCtTemplate( 70 | options: CtTemplateOptions, 71 | ): Promise { 72 | const templateVolumeId = await ensureExistsTemplate( 73 | options.baseTemplateStorage, 74 | options.baseFilename, 75 | ); 76 | const vmid: VMID = options.vmid ?? 77 | parseInt(await run("pvesh get /cluster/nextid"), 10); 78 | const bridgeName = (await getNetworkInterface("bridge")).name; 79 | const storageName = (await options.storage()).name; 80 | const installScript = await readInstallScript(options.baseFilename); 81 | 82 | await run([ 83 | "pct", 84 | "create", 85 | vmid, 86 | templateVolumeId, 87 | "--cores", 88 | options.cores ?? 2, 89 | "--description", 90 | (await readFromUrl(new URL("template/summary.md", import.meta.url))) 91 | .replaceAll("${APP_NAME}", options.name) 92 | .replaceAll("${DOCKER_CT_TEMPLATE_FILENAME}", options.baseFilename), 93 | "--features", 94 | "nesting=1", 95 | "--hostname", 96 | options.name, 97 | "--memory", 98 | options.memoryMegabytes ?? 2048, 99 | "--swap", 100 | options.memoryMegabytes ?? 2048, 101 | "--net0", 102 | `name=eth0,bridge=${bridgeName},firewall=1,ip=dhcp`, 103 | "--ostype", 104 | options.baseFilename.split("-")[0], 105 | "--ssh-public-keys", 106 | "/root/.ssh/authorized_keys", 107 | "--timezone", 108 | "host", 109 | "--template", 110 | 0, 111 | "--storage", 112 | storageName, 113 | "--unprivileged", 114 | 1, 115 | ]); 116 | await run(["pct", "start", vmid]); 117 | await run( 118 | ["pct", "exec", vmid, "--", ...extractShebangCommand(installScript)], 119 | { 120 | stdin: installScript, 121 | }, 122 | ); 123 | await run(["pct", "shutdown", vmid]); 124 | await run(["pct", "template", vmid]); 125 | return vmid; 126 | } 127 | 128 | type CtOptions = { 129 | templateVmid: VMID; 130 | vmid?: VMID; 131 | name: string; 132 | storage: () => Promise; 133 | }; 134 | 135 | export async function createCt( 136 | options: CtOptions, 137 | ): Promise<{ vmid: VMID; appdataDir: string }> { 138 | const vmid: VMID = options.vmid ?? 139 | parseInt(await run("pvesh get /cluster/nextid"), 10); 140 | 141 | const cloneCmd = [ 142 | "pct", 143 | "clone", 144 | options.templateVmid, 145 | vmid, 146 | "--hostname", 147 | options.name, 148 | "--description", 149 | (await readFromUrl(new URL("template/summary.md", import.meta.url))) 150 | .replaceAll("${APP_NAME}", options.name) 151 | .replaceAll("${DOCKER_CT_TEMPLATE_FILENAME}", `${options.templateVmid}`), 152 | ]; 153 | await run(cloneCmd).catch(() => 154 | run([ 155 | ...cloneCmd, 156 | "--full", 157 | 1, 158 | ]) 159 | ); 160 | const storageName = (await options.storage()).name; 161 | const appdataDir = await getAppdataDir({ name: storageName }, options); 162 | await Deno.mkdir(appdataDir, { recursive: true }); 163 | await Deno.writeTextFile( 164 | appdataDir + "/docker-compose.yml", 165 | await readFromUrl(new URL("template/docker-compose.yml", import.meta.url)), 166 | ); 167 | await run(["chown", "-R", `${100_000}:${100_000}`, appdataDir]); 168 | await run([ 169 | "pct", 170 | "set", 171 | vmid, 172 | "--mp0", 173 | `${appdataDir},mp=/appdata,backup=1`, 174 | ]); 175 | return { vmid, appdataDir }; 176 | } 177 | 178 | export async function ensureExistsCtTemplate( 179 | options: CtTemplateOptions, 180 | ): Promise { 181 | const template: PctEntry | undefined = await getCtTemplate(options.name); 182 | if (!template) { 183 | return await createCtTemplate(options); 184 | } 185 | return template.vmid; 186 | } 187 | 188 | export async function ensureExistsTemplate( 189 | templateStorage: () => Promise, 190 | filename: string, 191 | ): Promise { 192 | await run("pveam update"); 193 | const storageName = (await templateStorage()).name; 194 | await run([ 195 | "pveam", 196 | "download", 197 | storageName, 198 | filename, 199 | ]); 200 | return `${storageName}:${CONTENT_CT_TEMPLATE}/${filename}`; 201 | } 202 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/deps.ts: -------------------------------------------------------------------------------- 1 | export { dirname } from "https://deno.land/std@0.208.0/path/dirname.ts"; 2 | export { mapKeys } from "https://deno.land/std@0.208.0/collections/map_keys.ts"; 3 | export { mapValues } from "https://deno.land/std@0.208.0/collections/map_values.ts"; 4 | 5 | export { camelCase } from "https://deno.land/x/case@2.2.0/mod.ts"; 6 | export { Select } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/select.ts"; 7 | export type { SelectOptions } from "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/select.ts"; 8 | export { fetch as fetchFile } from "https://deno.land/x/file_fetch@0.2.0/mod.ts"; 9 | export { default as _parseColumns } from "npm:parse-columns@3.0.0"; 10 | 11 | export { jsonRun, run } from "https://deno.land/x/run_simple@2.2.0/mod.ts"; 12 | export { defaultRunOptions } from "https://deno.land/x/run_simple@2.2.0/src/run.ts"; 13 | export { parseJsonSafe } from "https://deno.land/x/run_simple@2.2.0/src/fn.ts"; 14 | export type { 15 | RunOptions, 16 | SimpleValue, 17 | } from "https://deno.land/x/run_simple@2.2.0/mod.ts"; 18 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/fn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the command to run a script, from its shebang line. 3 | * @param scriptContents the script source code 4 | * @returns array of command and parameters 5 | */ 6 | export function extractShebangCommand(scriptContents: string): string[] { 7 | return scriptContents.split("\n")[0].replace(/^#!/, "").split(" "); 8 | } 9 | 10 | /** 11 | * Extracts a value from a table, output by `pvesh get`. 12 | * @param table the table output by `pvesh get` 13 | * @param key the key to search for 14 | * @returns the value, or undefined if not found 15 | */ 16 | export function extractValueFromPveShGet( 17 | table: string, 18 | key: string, 19 | ): string | undefined; 20 | /** 21 | * Extracts a value from a table, output by `pvesh get`. 22 | * @param table the table output by `pvesh get` 23 | * @param keys the keys to search for 24 | * @returns the value, or undefined if not found 25 | */ 26 | export function extractValueFromPveShGet( 27 | table: string, 28 | keys: string[], 29 | ): string | undefined; 30 | /** 31 | * Extracts a value from a table, output by `pvesh get`. 32 | * @param table the table output by `pvesh get` 33 | * @param keyOrKeys the key or keys to search for 34 | * @returns the value, or undefined if not found 35 | */ 36 | export function extractValueFromPveShGet( 37 | table: string, 38 | keyOrKeys: string | string[], 39 | ): string | undefined { 40 | const keys = Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]; 41 | const line: string = table 42 | .split("\n") 43 | .map((line) => line.replaceAll(/[│ ]+/g, " ") as string) 44 | .find((line) => keys.some((key) => line.startsWith(` ${key} `))) ?? 45 | ""; 46 | const [_, _key, value, __] = line.split(" "); 47 | return value; 48 | } 49 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/network.ts: -------------------------------------------------------------------------------- 1 | import { jsonRun } from "./deps.ts"; 2 | import { chooseOne } from "./prompt.ts"; 3 | 4 | export type NetworkInterfaceType = 5 | | "bareudp" 6 | | "bond" 7 | | "bond_slave" 8 | | "bridge" 9 | | "bridge_slave" 10 | | "dummy" 11 | | "erspan" 12 | | "geneve" 13 | | "gre" 14 | | "gretap" 15 | | "ifb" 16 | | "ip6erspan" 17 | | "ip6gre" 18 | | "ip6gretap" 19 | | "ip6tnl" 20 | | "ipip" 21 | | "ipoib" 22 | | "ipvlan" 23 | | "ipvtap" 24 | | "macsec" 25 | | "macvlan" 26 | | "macvtap" 27 | | "netdevsim" 28 | | "nlmon" 29 | | "rmnet" 30 | | "sit" 31 | | "team_slave" 32 | | "vcan" 33 | | "veth" 34 | | "vlan" 35 | | "vrf" 36 | | "vti" 37 | | "vxcan" 38 | | "vxlan" 39 | | "xfrm"; 40 | 41 | export interface NetworkInterface { 42 | name: string; 43 | type: NetworkInterfaceType; 44 | up: boolean; 45 | macAddress: string; 46 | } 47 | 48 | interface IpResponseRow { 49 | ifname: string; 50 | operstate: "UP" | unknown; 51 | address: string; 52 | linkinfo: { 53 | info_kind: NetworkInterfaceType; 54 | }; 55 | } 56 | 57 | export type UpStatus = true | undefined; 58 | 59 | export async function getNetworkInterface( 60 | type?: NetworkInterfaceType, 61 | up: UpStatus = true, 62 | descriptive = type ? `${type} network interface` : "network interface", 63 | message = `Pick ${descriptive}.`, 64 | ): Promise { 65 | const choices: NetworkInterface[] = await getNetworkInterfaces(type, up); 66 | if (choices.length === 0) { 67 | throw new Error( 68 | `Could not find any ${up ? "active " : ""}${descriptive}.`, 69 | ); 70 | } 71 | const valueProperty = "name"; 72 | const search = true; 73 | 74 | return await chooseOne({ 75 | choices, 76 | message, 77 | valueProperty, 78 | search, 79 | }); 80 | } 81 | 82 | function parseIpResponseRow(response: IpResponseRow): NetworkInterface { 83 | return ({ 84 | name: response.ifname, 85 | type: response.linkinfo.info_kind, 86 | up: response.operstate === "UP", 87 | macAddress: response.address, 88 | }); 89 | } 90 | 91 | export async function getNetworkInterfaces( 92 | type?: NetworkInterfaceType, 93 | up?: UpStatus, 94 | ): Promise { 95 | const ipResponseRows: IpResponseRow[] = await jsonRun([ 96 | "ip", 97 | "-json", 98 | "-pretty", 99 | "-details", 100 | "link", 101 | "show", 102 | ...(up ? ["up"] : []), 103 | ...(type ? ["type", type] : []), 104 | ]); 105 | return ipResponseRows.map(parseIpResponseRow); 106 | } 107 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/os.ts: -------------------------------------------------------------------------------- 1 | import { columnRun } from "./run.ts"; 2 | 3 | export type VMID = number; 4 | 5 | export interface PctEntry { 6 | vmid: VMID; 7 | status: string; 8 | lock?: unknown; 9 | name: string; 10 | } 11 | 12 | export async function getCtTemplate( 13 | name: string, 14 | ): Promise { 15 | const templates = await getCtTemplates(); 16 | return templates.find((template) => template.name === name); 17 | } 18 | 19 | export async function getCtTemplates(): Promise { 20 | return await columnRun("pct list") as unknown as PctEntry[]; 21 | } 22 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/parse-columns.ts: -------------------------------------------------------------------------------- 1 | import { 2 | _parseColumns, 3 | camelCase, 4 | mapKeys, 5 | mapValues, 6 | parseJsonSafe, 7 | } from "./deps.ts"; 8 | 9 | export function parseColumns(input: string): T[] { 10 | const rows: T[] = _parseColumns(input) as T[]; 11 | return rows 12 | .map((row) => mapKeys(row as Readonly>, camelCase)) 13 | .map((row) => mapValues(row, parseJsonSafe) as T); 14 | } 15 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/prompt.ts: -------------------------------------------------------------------------------- 1 | import { run, Select, SelectOptions } from "./deps.ts"; 2 | 3 | export interface ChooseOneOptions { 4 | choices: T[]; 5 | valueProperty: string; 6 | message?: string; 7 | search?: boolean; 8 | } 9 | 10 | export async function chooseOne( 11 | { 12 | choices, 13 | valueProperty, 14 | message = "Choose one.", 15 | search = true, 16 | }: ChooseOneOptions, 17 | ): Promise { 18 | if (choices.length === 1 && choices[0]) { 19 | return choices[0]; 20 | } 21 | const choiceValue: string = await Select.prompt({ 22 | message, 23 | search, 24 | options: await toSelectOptions( 25 | choices as unknown as Record[], 26 | valueProperty, 27 | ), 28 | }); 29 | const choice: T | undefined = choices 30 | .find( 31 | (possibleChoice) => 32 | (possibleChoice as unknown as Record)[valueProperty] === 33 | choiceValue, 34 | ); 35 | if (!choice) { 36 | throw new Error( 37 | `Unexpectedly could not find your choice "${choiceValue}", in the list.`, 38 | ); 39 | } 40 | return choice; 41 | } 42 | 43 | async function toSelectOptions>( 44 | items: T[], 45 | valueProperty: string, 46 | ): Promise["options"]> { 47 | if (items.length === 0) { 48 | return []; 49 | } 50 | const headerLineInput: string = Object.keys(items[0]).join(" "); 51 | const table: string = await run( 52 | "column --table", 53 | { 54 | stdin: [ 55 | headerLineInput, 56 | ...items.map((item) => Object.values(item).join(" ")), 57 | ].join("\n"), 58 | }, 59 | ); 60 | const tableLines = table.split("\n"); 61 | const tableWidth = Math.max(...tableLines.map((line) => line.length)); 62 | 63 | const [headerLine, ...lines] = tableLines; 64 | return [ 65 | { name: headerLine, value: "", disabled: true }, 66 | Select.separator("-".repeat(tableWidth)), 67 | ...lines.map((line, index) => ({ 68 | name: line, 69 | value: (items[index] && (items[index][valueProperty] as string)) ?? "", 70 | })), 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/read-from-url.ts: -------------------------------------------------------------------------------- 1 | import { fetchFile } from "./deps.ts"; 2 | 3 | export async function readFromUrl(url: string | URL): Promise { 4 | const response: Response = await fetchFile(url); 5 | if (!response.ok) { 6 | if (response.status === 404) { 7 | throw new Deno.errors.NotFound( 8 | `Could not find "${url}".`, 9 | ); 10 | } 11 | throw new Error( 12 | `Could not read "${url}", got status ${response.status}.`, 13 | ); 14 | } 15 | return await response.text(); 16 | } 17 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/run.ts: -------------------------------------------------------------------------------- 1 | import { defaultRunOptions, run, RunOptions, SimpleValue } from "./deps.ts"; 2 | import { parseColumns } from "./parse-columns.ts"; 3 | 4 | export async function columnRun( 5 | command: string | SimpleValue[], 6 | options: RunOptions = defaultRunOptions, 7 | ): Promise { 8 | return parseColumns(await run(command, options)); 9 | } 10 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/storage.ts: -------------------------------------------------------------------------------- 1 | import { columnRun } from "./run.ts"; 2 | import { chooseOne } from "./prompt.ts"; 3 | 4 | export type StorageType = 5 | | "zfspool" 6 | | "dir" 7 | | "btrfs" 8 | | "nfs" 9 | | "cifs" 10 | | "pbs" 11 | | "glusterfs" 12 | | "cephfs" 13 | | "lvm" 14 | | "lvmthin" 15 | | "iscsi" 16 | | "iscsidirect" 17 | | "rbd" 18 | | "zfs"; 19 | export type StorageStatus = "active" | unknown; 20 | export interface StorageRow { 21 | name: string; 22 | type: StorageType; 23 | status: StorageStatus; 24 | total: number; 25 | used: number; 26 | available: number; 27 | "%": string; 28 | } 29 | 30 | export type ContentType = 31 | | typeof CONTENT_VM_IMAGE 32 | | typeof CONTENT_CT_ROOTDIR 33 | | typeof CONTENT_CT_TEMPLATE 34 | | typeof CONTENT_CT_BACKUP 35 | | typeof CONTENT_ISO 36 | | typeof CONTENT_SNIPPET; 37 | 38 | /** KVM-Qemu VM images. */ 39 | export const CONTENT_VM_IMAGE = "images" as const; 40 | 41 | /** Allow to store container data. */ 42 | export const CONTENT_CT_ROOTDIR = "rootdir" as const; 43 | 44 | /** Container templates. */ 45 | export const CONTENT_CT_TEMPLATE = "vztmpl" as const; 46 | 47 | /** Backup files (vzdump). */ 48 | export const CONTENT_CT_BACKUP = "backup" as const; 49 | 50 | /** ISO images */ 51 | export const CONTENT_ISO = "iso" as const; 52 | 53 | /** Snippet files, for example guest hook scripts */ 54 | export const CONTENT_SNIPPET = "snippets" as const; 55 | 56 | export async function getStorage( 57 | content: ContentType | undefined, 58 | descriptive = `"${content}" content`, 59 | message = `Pick storage for ${descriptive}.`, 60 | ): Promise { 61 | const choices: StorageRow[] = await getStorages(content); 62 | if (choices.length === 0) { 63 | throw new Error( 64 | `Could not find any active storage that supports ${descriptive}.`, 65 | ); 66 | } 67 | const valueProperty = "name"; 68 | const search = true; 69 | 70 | return await chooseOne({ 71 | choices, 72 | message, 73 | valueProperty, 74 | search, 75 | }); 76 | } 77 | 78 | export async function getStorages( 79 | content?: ContentType, 80 | ): Promise { 81 | return await columnRun([ 82 | "pvesm", 83 | "status", 84 | "-enabled", 85 | ...(content ? ["-content", content] : []), 86 | ]); 87 | } 88 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/template/alpine-318-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -xe 3 | 4 | # Type any extra commands inside this function. They will run last. 5 | extra_commands() { 6 | apk --no-cache add byobu musl-locales neovim 7 | byobu-enable 8 | ln -sf "$(command -v nvim)" /usr/local/bin/vim 9 | } 10 | 11 | create_executable() { 12 | filename="$1" 13 | mkdir -p "$(dirname "${filename}")" 14 | cat > "${filename}" 15 | chmod +x "${filename}" 16 | } 17 | 18 | enable_service() { 19 | name="$1" 20 | rc-update add "${name}" boot 21 | } 22 | 23 | create_service() { 24 | name="$1" 25 | create_executable "/etc/init.d/${name}" 26 | enable_service "${name}" 27 | } 28 | 29 | create_executable "/etc/local.d/update-issue.start" <<'EOF' 30 | #!/bin/sh 31 | set -e 32 | hr(){ 33 | echo "----------------------------------------------------------------------" 34 | } 35 | sed "/-----/,100d" -i /etc/issue || true 36 | sed "/Link encap/,100d" -i /etc/issue || true 37 | hr >> /etc/issue 38 | /sbin/ifconfig | grep -vE '^lo ' | grep -EA2 '^[^ ]' >> /etc/issue 39 | hr >> /etc/issue 40 | EOF 41 | 42 | apk upgrade --no-cache 43 | apk add --no-cache curl docker openssh-server 44 | 45 | DOCKER_COMPOSE_VERSION="$(curl -sSfL https://semver-version.deno.dev/api/github/docker/compose)" 46 | curl -sSfL -o /usr/local/bin/docker-compose "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" 47 | chmod +x /usr/local/bin/docker-compose 48 | 49 | mkdir -p /appdata 50 | 51 | enable_service local 52 | enable_service sshd 53 | 54 | create_service cgroups-patch <<'EOF' 55 | #!/sbin/openrc-run 56 | 57 | description="Mount the control groups for Docker" 58 | # via https://wildwolf.name/how-to-run-docker-in-alpine-container-in-lxc-lxd/ 59 | 60 | depend() 61 | { 62 | keyword -docker 63 | need sysfs cgroups 64 | } 65 | 66 | start() 67 | { 68 | if [ -d /sys/fs/cgroup ]; then 69 | mkdir -p /sys/fs/cgroup/cpu,cpuacct 70 | mkdir -p /sys/fs/cgroup/net_cls,net_prio 71 | 72 | mount -n -t cgroup cgroup /sys/fs/cgroup/cpu,cpuacct -o rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 73 | mount -n -t cgroup cgroup /sys/fs/cgroup/net_cls,net_prio -o rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 74 | 75 | if ! mountinfo -q /sys/fs/cgroup/openrc; then 76 | local agent="${RC_LIBEXECDIR}/sh/cgroup-release-agent.sh" 77 | mkdir -p /sys/fs/cgroup/openrc 78 | mount -n -t cgroup -o none,nodev,noexec,nosuid,name=systemd,release_agent="$agent" openrc /sys/fs/cgroup/openrc 79 | fi 80 | fi 81 | 82 | return 0 83 | } 84 | EOF 85 | 86 | enable_service docker 87 | 88 | create_service docker-compose <<'EOF' 89 | #!/sbin/openrc-run 90 | 91 | name=$RC_SVCNAME 92 | description="Docker Compose Application Service" 93 | 94 | depend() { 95 | after cgroups-patch 96 | need cgroups-patch 97 | } 98 | 99 | start() { 100 | ebegin "Starting $RC_SVCNAME" 101 | cd /appdata 102 | /usr/local/bin/docker-compose up -d --remove-orphans --wait 103 | eend $? 104 | } 105 | 106 | stop() { 107 | ebegin "Stopping $RC_SVCNAME" 108 | cd /appdata 109 | /usr/local/bin/docker-compose down 110 | eend $? 111 | } 112 | EOF 113 | 114 | extra_commands 115 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/template/debian-12-install: -------------------------------------------------------------------------------- 1 | ubuntu-2204-install -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/template/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | image: docker.io/traefik/whoami 6 | environment: 7 | - PORT="80" 8 | ports: [ "80:80" ] 9 | # volumes: 10 | # - /appdata/data:/data:ro # left side is inside lxc, right is inside docker 11 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/template/summary.md: -------------------------------------------------------------------------------- 1 | # ${APP_NAME} 2 | 3 | **CT template**\ 4 | `${DOCKER_CT_TEMPLATE_FILENAME}` 5 | 6 | --- 7 | 8 | _Created using 9 | [create-docker-ct](https://github.com/hugojosefson/proxmox-create-docker-ct)._ 10 | -------------------------------------------------------------------------------- /usr_local_bin/create-docker-ct-files/template/ubuntu-2204-install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail; IFS=$'\t\n' 3 | export DEBIAN_FRONTEND=noninteractive 4 | 5 | # Type any extra commands inside this function. They will run last. 6 | extra_commands() { 7 | apt-get install -y byobu neovim 8 | byobu-enable 9 | } 10 | 11 | cat >> /etc/issue <<<' 12 | \4 13 | \6 14 | ' 15 | 16 | apt-get update 17 | apt-get full-upgrade --purge -y 18 | apt-get auto-remove -y 19 | apt-get install -y curl 20 | 21 | curl https://get.docker.com/ | sh 22 | docker run --rm hello-world 23 | 24 | DOCKER_COMPOSE_VERSION="$(curl -sSfL https://semver-version.deno.dev/api/github/docker/compose)" 25 | curl -sSfL -o /usr/local/bin/docker-compose "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" 26 | chmod +x /usr/local/bin/docker-compose 27 | 28 | cat > /etc/systemd/system/docker-compose.service <<'EOF' 29 | [Unit] 30 | Description=Docker Compose Application Service 31 | Requires=docker.service 32 | After=docker.service 33 | 34 | [Service] 35 | Type=oneshot 36 | RemainAfterExit=yes 37 | WorkingDirectory=/appdata 38 | ExecStart=/usr/local/bin/docker-compose up -d --remove-orphans --wait 39 | ExecStop=/usr/local/bin/docker-compose down 40 | TimeoutStartSec=0 41 | 42 | [Install] 43 | WantedBy=multi-user.target 44 | EOF 45 | systemctl enable docker-compose 46 | echo " 47 | 48 | " 49 | docker --version 50 | /usr/local/bin/docker-compose --version 51 | 52 | apt-get install -y unattended-upgrades 53 | 54 | extra_commands 55 | apt-get clean 56 | --------------------------------------------------------------------------------