├── .devcontainer └── devcontainer.json ├── .github ├── CODE_OF_CONDUCT.md ├── README.md ├── dependabot.yml └── workflows │ ├── build-template.yml │ ├── publish-docker.yml │ └── sync-mirror.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── Schema.json ├── index.html ├── lib ├── combine.py ├── download.py ├── list.py ├── requirements.txt └── validate.py ├── sources.csv ├── sources ├── .gitignore ├── .gitkeep ├── example_templates.json ├── lissy93_templates.json └── stacks │ ├── cryptpad.yml │ ├── databag.yml │ ├── outline.yml │ ├── terraria-server.yml │ ├── traefik.yml │ ├── valheim-server-docker.yml │ └── ztnet │ └── docker-compose.yml ├── templates.json └── templates_v3.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", 7 | 8 | "customizations": { 9 | "vscode": { 10 | "extensions": [ 11 | "ms-python.vscode-pylance", 12 | "ms-azuretools.vscode-docker", 13 | "ms-python.autopep8" 14 | ] 15 | } 16 | }, 17 | 18 | // Features to add to the dev container. More info: https://containers.dev/features. 19 | "features": { 20 | "ghcr.io/devcontainers/features/node:1": { 21 | "version": "latest" 22 | } 23 | } 24 | 25 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 26 | // "forwardPorts": [], 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 30 | 31 | // Configure tool-specific properties. 32 | // "customizations": {}, 33 | 34 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 35 | // "remoteUser": "root" 36 | } 37 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | alicia@omg.lol. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/build-template.yml: -------------------------------------------------------------------------------- 1 | name: 🏗️ Build + Publish templates.json file 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | schedule: 7 | - cron: '0 2 * * 0' # At 02:00 on Sunday 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout repo 13 | - name: Checkout repository 🛎️ 14 | uses: actions/checkout@v2 15 | 16 | # Get current date-time (used for commit message) 17 | - name: Get Date 📅 18 | id: date 19 | run: echo "::set-output name=date::$(date +'%d-%b-%Y')" 20 | 21 | # Downloads + installs Python (used for running gen scripts) 22 | - name: Set up Python 🐍 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | 27 | # Install contents of requirements.txt 28 | - name: Install dependencies 📥 29 | run: | 30 | python -m pip install --upgrade pip 31 | cd lib && pip install -r requirements.txt 32 | 33 | # The make command triggers all the Python scripts, generates output 34 | - name: Run make command 🔨 35 | run: make 36 | 37 | # Commit and push the outputed JSON files 38 | - name: Commit and push generated files ⤴️ 39 | run: | 40 | git config --global user.name "Liss-Bot" 41 | git config --global user.email "alicia-gh-bot@mail.as93.net" 42 | git add templates.json 43 | if git diff --staged --quiet; then 44 | echo "Nothin new added, so nothing to commit, exiting..." 45 | else 46 | git commit -m "Updates templates (auto-generated, on ${{ steps.date.outputs.date }})" 47 | git push 48 | fi 49 | git add .github/README.md 50 | if git diff --staged --quiet; then 51 | echo "No need to update README, skipping..." 52 | else 53 | git commit -m "Updates template + source list in docs (auto-generated, on ${{ steps.date.outputs.date }})" 54 | git push 55 | fi 56 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | # Scans, builds and releases a multi-architecture docker image 2 | name: 🐳 Build + Publish Multi-Platform Image 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: ['main'] 8 | tags: ['v[0-9].[0-9]+.[0-9]+'] 9 | paths: 10 | - 'templates.json' 11 | - 'Dockerfile' 12 | 13 | env: 14 | DH_IMAGE: ${{ secrets.DOCKER_REPO || github.event.repository.name }} 15 | GH_IMAGE: ${{ github.repository_owner }}/${{ github.event.repository.name }} 16 | 17 | jobs: 18 | docker: 19 | runs-on: ubuntu-latest 20 | permissions: { contents: read, packages: write } 21 | if: "!contains(github.event.head_commit.message, '[ci-skip]')" 22 | 23 | steps: 24 | - name: 🛎️ Checkout Repo 25 | uses: actions/checkout@v2 26 | 27 | # - name: ✨ Validate Dockerfile 28 | # uses: ghe-actions/dockerfile-validator@v1 29 | # with: 30 | # dockerfile: 'Dockerfile' 31 | # lint: 'hadolint' 32 | 33 | - name: 🗂️ Make Docker Meta 34 | id: meta 35 | uses: docker/metadata-action@v3 36 | with: 37 | images: | 38 | ${{ env.DH_IMAGE }} 39 | ghcr.io/${{ env.GH_IMAGE }} 40 | # tags: | 41 | # type=ref,event=tag,suffix={{tag}} 42 | # type=ref,event=branch,branch=main,name=latest 43 | labels: | 44 | maintainer=Lissy93 45 | org.opencontainers.image.title=Portainer-Templates 46 | org.opencontainers.image.description=An offline collection of 500 Portainer app and stack templates 47 | org.opencontainers.image.documentation=https://github.com/lissy93/portainer-templates 48 | org.opencontainers.image.authors=Alicia Sykes 49 | org.opencontainers.image.licenses=MIT 50 | 51 | - name: 🔧 Set up QEMU 52 | uses: docker/setup-qemu-action@v1 53 | 54 | - name: 🔧 Set up Docker Buildx 55 | uses: docker/setup-buildx-action@v1 56 | 57 | - name: 🔑 Login to DockerHub 58 | uses: docker/login-action@v1 59 | with: 60 | username: ${{ secrets.DOCKER_USER }} 61 | password: ${{ secrets.DOCKER_TOKEN }} 62 | 63 | - name: 🔑 Login to GitHub Container Registry 64 | uses: docker/login-action@v1 65 | with: 66 | registry: ghcr.io 67 | username: ${{ github.repository_owner }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: 🚦 Check Registry Status 71 | uses: crazy-max/ghaction-docker-status@v1 72 | 73 | - name: ⚒️ Build and push 74 | uses: docker/build-push-action@v4 75 | with: 76 | context: . 77 | file: ./Dockerfile 78 | platforms: linux/amd64,linux/arm64,linux/arm/v7 79 | tags: ${{ steps.meta.outputs.tags }} 80 | labels: ${{ steps.meta.outputs.labels }} 81 | push: true 82 | # - name: 💬 Set Docker Hub Description 83 | # uses: peter-evans/dockerhub-description@v2 84 | # with: 85 | # repository: lissy93/devolio 86 | # readme-filepath: ./README.md 87 | # short-description: Devolio - A developer portfolio site for the rest of us 88 | # username: ${{ secrets.DOCKER_USERNAME }} 89 | # password: ${{ secrets.DOCKER_USER_PASS }} 90 | -------------------------------------------------------------------------------- /.github/workflows/sync-mirror.yml: -------------------------------------------------------------------------------- 1 | # Pushes the contents of the repo to the Codeberg mirror 2 | name: 🪞 Mirror to Codeberg 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '30 2 * * 0' # At 02:30 on Sunday 7 | jobs: 8 | codeberg: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: { fetch-depth: 0 } 13 | - uses: pixta-dev/repository-mirroring-action@v1 14 | with: 15 | target_repo_url: git@codeberg.org:alicia/portainer-templates.git 16 | ssh_private_key: ${{ secrets.CODEBERG_SSH }} 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | 3 | COPY templates.json /usr/share/nginx/html/templates.json 4 | COPY index.html /usr/share/nginx/html/index.html 5 | 6 | EXPOSE 80 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alicia Sykes 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 | .PHONY: all install_requirements download combine 2 | 3 | PYTHON := $(shell which python3 2>/dev/null || which python) 4 | 5 | all: install_requirements download combine list 6 | 7 | install_requirements: 8 | $(PYTHON) -m pip install -r lib/requirements.txt 9 | 10 | download: 11 | $(PYTHON) lib/download.py 12 | 13 | combine: 14 | $(PYTHON) lib/combine.py 15 | 16 | validate: 17 | $(PYTHON) lib/validate.py 18 | 19 | list: 20 | $(PYTHON) lib/list.py 21 | -------------------------------------------------------------------------------- /Schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "title": "PortainerAppTemplate", 5 | "properties": { 6 | "version": { 7 | "type": "string", 8 | "minLength": 1, 9 | "description": "The version of the Portainer App Template." 10 | }, 11 | "templates": { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "type": { 17 | "type": "integer", 18 | "minimum": 1, 19 | "maximum": 2, 20 | "description": "The type of the application (1 for container, 2 for swarm stack)." 21 | }, 22 | "title": { 23 | "type": "string", 24 | "minLength": 1, 25 | "description": "The title of the application." 26 | }, 27 | "description": { 28 | "type": "string", 29 | "minLength": 1, 30 | "description": "A brief description of the application." 31 | }, 32 | "categories": { 33 | "type": "array", 34 | "items": { 35 | "type": "string", 36 | "minLength": 1 37 | }, 38 | "description": "An array of categories the application belongs to." 39 | }, 40 | "platform": { 41 | "type": "string", 42 | "minLength": 1, 43 | "description": "The target platform of the application (e.g., 'linux', 'windows')." 44 | }, 45 | "logo": { 46 | "type": "string", 47 | "format": "uri", 48 | "description": "A URI to the logo of the application." 49 | }, 50 | "image": { 51 | "type": "string", 52 | "minLength": 1, 53 | "description": "The name of the Docker image used for the application." 54 | }, 55 | "restart_policy": { 56 | "type": "string", 57 | "enum": ["always", "unless-stopped", "on-failure", "no"], 58 | "description": "The restart policy for the application." 59 | }, 60 | "ports": { 61 | "type": "array", 62 | "items": { 63 | "type": "string", 64 | "pattern": "^[0-9]+:[0-9]+(/tcp|/udp)?$", 65 | "description": "A port mapping in the format 'hostPort:containerPort/protocol'." 66 | }, 67 | "description": "An array of port mappings for the application." 68 | }, 69 | "volumes": { 70 | "type": "array", 71 | "items": { 72 | "type": "object", 73 | "properties": { 74 | "bind": { 75 | "type": "string", 76 | "minLength": 1, 77 | "description": "The host path for the volume binding." 78 | }, 79 | "container": { 80 | "type": "string", 81 | "minLength": 1, 82 | "description": "The container path for the volume binding." 83 | } 84 | }, 85 | "required": ["bind", "container"] 86 | }, 87 | "description": "An array of volume mappings for the application." 88 | }, 89 | "environment": { 90 | "type": "array", 91 | "items": { 92 | "type": "object", 93 | "properties": { 94 | "name": { 95 | "type": "string", 96 | "minLength": 1, 97 | "description": "The name of the environment variable." 98 | }, 99 | "label": { 100 | "type": "string" 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | "required": ["type", "title", "description", "categories", "platform", "logo", "image"] 107 | } 108 | } 109 | }, 110 | "required": ["version", "templates"] 111 | } 112 | 113 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Portainer Templates 4 | 5 | 6 | 7 |
8 |

Portainer Templates

9 |

Your template server is up and running! 🎉

10 |

11 | Within your Portainer instance, you can add this URL as the template source: 12 | templates.json 13 |

14 |
15 |

16 | For docs and support, visit the GitHub repo at 17 | github.com/lissy93/portainer-templates 18 |
19 | To browse the full list of apps, stats and config options, see 20 | portainer-templates.as93.net 21 |

22 | 23 | 25 |
26 | 36 | 37 | -------------------------------------------------------------------------------- /lib/combine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import json 4 | 5 | # Source: https://ask.replit.com/t/how-do-i-make-colored-text-in-python/29288/18 6 | reset_color = "\033[0m" # Important! 7 | def rgb(r, g, b): 8 | return f"\033[38;2;{r};{g};{b}m" 9 | 10 | # Get list of files in sources 11 | dir = os.path.dirname(os.path.abspath(__file__)) 12 | templates_src_dir = os.path.join(dir, '../sources/') 13 | template_dest_file = os.path.join(dir, '../templates.json') 14 | 15 | files = os.listdir(templates_src_dir) 16 | 17 | # Initialize empty list to store template objects 18 | templates = [] 19 | 20 | # For each file in sources 21 | for file in files: 22 | file_path = os.path.join(templates_src_dir, file) 23 | if os.path.isfile(file_path) and file.endswith('.json'): 24 | with open(file_path) as f: 25 | try: 26 | # Load the JSON into a variable 27 | data = json.load(f)['templates'] 28 | # Append the template object to the templates list 29 | templates = templates + data 30 | except json.decoder.JSONDecodeError as err: 31 | print(f'{rgb(255, 0, 0)}Skipping one of the sources due to an error:{reset_color} {f.name}') 32 | print(f'Error msg: {err.msg}') 33 | 34 | seen_titles = set() 35 | filtered_data = [] 36 | 37 | def normalize_string(original, lowercase = True): 38 | normalized = original.translate(str.maketrans('', '', string.punctuation)).replace(' ', '') 39 | return normalized.lower() if lowercase else normalized.capitalize() 40 | 41 | for x in templates: 42 | normalized_title = normalize_string(x['title']) 43 | if normalized_title in seen_titles: 44 | continue 45 | 46 | seen_titles.add(normalized_title) 47 | filtered_data.append(x) 48 | 49 | categories = x.get('categories', []) 50 | x['categories'] = [] 51 | 52 | for category in categories: 53 | normalized_category = normalize_string(category, lowercase = False) 54 | 55 | if normalized_category not in x['categories']: 56 | x['categories'].append(normalized_category) 57 | 58 | fileData = { 59 | 'version': '2', 60 | 'templates': filtered_data 61 | } 62 | 63 | # Open the templates.json file, and write results to it 64 | with open(template_dest_file, 'w') as f: 65 | json.dump(fileData, f, indent=2, sort_keys=False) 66 | -------------------------------------------------------------------------------- /lib/download.py: -------------------------------------------------------------------------------- 1 | import os 2 | import csv 3 | import requests 4 | import json 5 | 6 | dir = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | destination_dir = os.path.join(dir, '../sources') 9 | sources_list = os.path.join(dir, '../sources.csv') 10 | 11 | # Downloads the file from a given URL, to the local destination 12 | def download(url: str, filename: str, maintainer: str): 13 | file_path = os.path.join(destination_dir, filename) 14 | print('Downloading', url) 15 | r = requests.get(url, stream=True) 16 | if r.ok: 17 | print('saving to', os.path.abspath(file_path)) 18 | with open(file_path, 'wb') as f: 19 | for chunk in r.iter_content(chunk_size=1024 * 8): 20 | if chunk: 21 | f.write(chunk) 22 | f.flush() 23 | os.fsync(f.fileno()) 24 | 25 | sourceJson = {} 26 | with open(file_path) as f: 27 | try: 28 | sourceJson = json.load(f) 29 | # Add maintainer field to each template 30 | for t in sourceJson.get('templates', []): 31 | t['maintainer'] = maintainer 32 | 33 | except json.decoder.JSONDecodeError as err: 34 | print(f'Skipping one of the sources due to an error: {f.name}') 35 | print(f'Error msg: {err.msg}') 36 | 37 | if not sourceJson: 38 | return 39 | 40 | with open(file_path, 'w') as f: 41 | json.dump(sourceJson, f, indent=2, sort_keys=False) 42 | 43 | else: # HTTP status code 4XX/5XX 44 | print('Download failed: status code {}\n{}'.format(r.status_code, r.text)) 45 | 46 | # Gets list of URLs to download from CSV file 47 | def get_source_list(): 48 | sources=[] 49 | with open(sources_list, mode='r') as file: 50 | csvFile = csv.reader(file) 51 | for lines in csvFile: 52 | if len(lines) > 1 and lines[1].strip(): 53 | sources.append(lines) 54 | return sources 55 | 56 | # Create destination folder if not yet present 57 | if not os.path.exists(destination_dir): 58 | os.makedirs(destination_dir) 59 | 60 | # For each source, download the templates JSON file 61 | for sourceUrl in get_source_list(): 62 | download(sourceUrl[1], sourceUrl[0] + '.json', sourceUrl[2]) 63 | -------------------------------------------------------------------------------- /lib/list.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib.parse 3 | import os 4 | import csv 5 | import re 6 | 7 | current_dir = os.path.dirname(os.path.abspath(__file__)) 8 | project_dir = os.path.dirname(current_dir) 9 | readme_path = os.path.join(project_dir, '.github/README.md') 10 | templates_path = os.path.join(project_dir, 'templates.json') 11 | sources_path = os.path.join(project_dir, 'sources.csv') 12 | 13 | def load_json_file(file_path): 14 | with open(file_path, 'r') as file: 15 | return json.load(file) 16 | 17 | def load_csv_file(file_path): 18 | with open(file_path, 'r') as file: 19 | return list(csv.reader(file)) 20 | 21 | def slugify(title: str): 22 | baseUrl = 'https://portainer-templates.as93.net' 23 | return f'{baseUrl}/{re.sub(r"[^a-zA-Z ]", "", title.lower()).replace(" ", "-")}' 24 | 25 | def generate_app_list(): 26 | templates = load_json_file(templates_path)['templates'] 27 | templates.sort(key=lambda template: template['title'].lower()) 28 | markdown_content = '' 29 | for index, template in enumerate(templates): 30 | name = template['title'].title() 31 | maintainer = template.get('maintainer') 32 | maintainer_md_link = f" -- ([Report issues]({maintainer}))" if maintainer else '' 33 | description = re.sub('[^0-9a-zA-Z]+', ' ', (template['description'] or '')) 34 | if 'logo' in template and template['logo']: 35 | logo = f" " 36 | else: 37 | logo = ' ' 38 | markdown_content += f"{index+1}. {logo}**[{name}]({slugify(name)} '{description}')** {maintainer_md_link}\n" 39 | return markdown_content 40 | 41 | def generate_sources_list(): 42 | sources = load_csv_file(sources_path) 43 | markdown_content = '' 44 | 45 | for index, source in enumerate(sources): 46 | if len(source) > 1 and source[1].strip(): 47 | url = source[1].strip() 48 | parsed_url = urllib.parse.urlparse(url) 49 | username = parsed_url.path.split('/')[1] 50 | avatar = f'' 51 | markdown_content += f"{index + 1}. {avatar} [template]({url}) by [@{username}](https://github.com/{username})\n" 52 | 53 | return markdown_content 54 | 55 | def insert_content_between_markers(file_path, start_marker, end_marker, content_to_insert): 56 | with open(file_path, 'r') as file: 57 | lines = file.readlines() 58 | 59 | start_index = -1 60 | end_index = -1 61 | 62 | for i, line in enumerate(lines): 63 | if start_marker in line: 64 | start_index = i 65 | if end_marker in line: 66 | end_index = i 67 | break 68 | 69 | if start_index >= 0 and end_index >= 0: 70 | lines[start_index + 1:end_index] = [content_to_insert + '\n'] 71 | 72 | with open(file_path, 'w') as file: 73 | file.writelines(lines) 74 | 75 | # Insert sources list into readme 76 | insert_content_between_markers( 77 | readme_path, 78 | '', 79 | '', 80 | generate_sources_list(), 81 | ) 82 | 83 | # Insert app list into readme 84 | insert_content_between_markers( 85 | readme_path, 86 | '', 87 | '', 88 | generate_app_list(), 89 | ) 90 | -------------------------------------------------------------------------------- /lib/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | jsonschema -------------------------------------------------------------------------------- /lib/validate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from jsonschema import validate, ValidationError 5 | 6 | def load_json_file(file_path): 7 | with open(file_path, 'r') as file: 8 | return json.load(file) 9 | 10 | def main(): 11 | try: 12 | script_dir = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | schema_file = os.path.join(script_dir, '..', 'Schema.json') 15 | templates_file = os.path.join(script_dir, '..', 'templates.json') 16 | 17 | schema = load_json_file(schema_file) 18 | templates = load_json_file(templates_file) 19 | 20 | validate(instance=templates, schema=schema) 21 | 22 | print('✅ templates.json is valid against the schema') 23 | 24 | except ValidationError as ve: 25 | print('Validation error:', ve.message) 26 | json_obj = ve.instance 27 | identifier = json_obj.get('title') 28 | print('Title of invalid template:', identifier) 29 | sys.exit(1) 30 | except FileNotFoundError as fnfe: 31 | print(f'File not found error: {fnfe}') 32 | sys.exit(1) 33 | except json.JSONDecodeError as jde: 34 | print(f'JSON decoding error: {jde}') 35 | sys.exit(1) 36 | 37 | if __name__ == '__main__': 38 | main() 39 | -------------------------------------------------------------------------------- /sources.csv: -------------------------------------------------------------------------------- 1 | dnburgess_templates, https://raw.githubusercontent.com/dnburgess/self-hosted-template/master/template.json, https://github.com/dnburgess/self-hosted-template/ 2 | qballjos_templates, https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Template/template.json, https://github.com/Qballjos/portainer_templates/ 3 | selfhostedpro_templates, https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/portainer-2.0/Template/template.json, https://github.com/SelfhostedPro/selfhosted_templates/ 4 | technorabilia_templates, https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json, https://github.com/technorabilia/portainer-templates/ 5 | mikestraney_templates, https://raw.githubusercontent.com/mikestraney/portainer-templates/master/templates.json, https://github.com/mikestraney/portainer-templates/ 6 | xneo1_templates, https://raw.githubusercontent.com/xneo1/portainer_templates/master/Template/template.json, https://github.com/xneo1/portainer_templates/ 7 | novaspirit_templates, https://raw.githubusercontent.com/novaspirit/pi-hosted/master/pi-hosted_template/template/portainer-v2.json, https://github.com/novaspirit/pi-hosted/ 8 | donpablonow_templates, https://raw.githubusercontent.com/donpablonow/awesome-saas/master/Template/portainer-v2.json, https://github.com/donpablonow/awesome-saas/ 9 | mediadepot_templates, https://raw.githubusercontent.com/mediadepot/templates/master/portainer.json, https://github.com/mediadepot/templates/ 10 | mycroftwilde_templates, https://raw.githubusercontent.com/mycroftwilde/portainer_templates/master/Template/template.json, https://github.com/mycroftwilde/portainer_templates/ 11 | mediadepot_templates, https://raw.githubusercontent.com/mediadepot/templates/master/portainer.json, https://github.com/mediadepot/templates/ 12 | shmolf_templates, https://raw.githubusercontent.com/shmolf/portainer-templates/main/templates-2.0.json, https://github.com/shmolf/portainer-templates/ 13 | portainer_templates, https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json, https://github.com/portainer/templates/ 14 | -------------------------------------------------------------------------------- /sources/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | 3 | # Keep these 4 | !example_templates.json 5 | !lissy93_tempaltes.json 6 | -------------------------------------------------------------------------------- /sources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lissy93/portainer-templates/bef29d7c5c17c86050d0090fb5f57e80b5fd294a/sources/.gitkeep -------------------------------------------------------------------------------- /sources/example_templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "templates": [] 4 | } 5 | -------------------------------------------------------------------------------- /sources/lissy93_templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "templates": [ 4 | { 5 | "categories": [ 6 | "Productivity", 7 | "Social" 8 | ], 9 | "description": "Open source collaborative knowledge base for modern teams", 10 | "env": [ 11 | { 12 | "default": "production", 13 | "label": "NODE_ENV", 14 | "name": "NODE_ENV" 15 | }, 16 | { 17 | "default": "", 18 | "label": "SECRET_KEY", 19 | "name": "SECRET_KEY" 20 | }, 21 | { 22 | "default": "", 23 | "label": "UTILS_SECRET", 24 | "name": "UTILS_SECRET" 25 | }, 26 | { 27 | "default": "", 28 | "label": "DATABASE_URL", 29 | "name": "DATABASE_URL" 30 | }, 31 | { 32 | "default": "", 33 | "label": "DATABASE_URL_TEST", 34 | "name": "DATABASE_URL_TEST" 35 | }, 36 | { 37 | "default": "", 38 | "label": "DATABASE_CONNECTION_POOL_MIN", 39 | "name": "DATABASE_CONNECTION_POOL_MIN" 40 | }, 41 | { 42 | "default": "", 43 | "label": "DATABASE_CONNECTION_POOL_MAX", 44 | "name": "DATABASE_CONNECTION_POOL_MAX" 45 | }, 46 | { 47 | "default": "", 48 | "label": "REDIS_URL", 49 | "name": "REDIS_URL" 50 | }, 51 | { 52 | "default": "", 53 | "label": "URL", 54 | "name": "URL" 55 | }, 56 | { 57 | "default": "3000", 58 | "label": "PORT", 59 | "name": "PORT" 60 | }, 61 | { 62 | "default": "", 63 | "label": "COLLABORATION_URL", 64 | "name": "COLLABORATION_URL" 65 | }, 66 | { 67 | "default": "", 68 | "label": "GOOGLE_CLIENT_ID", 69 | "name": "GOOGLE_CLIENT_ID" 70 | }, 71 | { 72 | "default": "", 73 | "label": "GOOGLE_CLIENT_SECRET", 74 | "name": "GOOGLE_CLIENT_SECRET" 75 | }, 76 | { 77 | "default": "", 78 | "label": "SSL_KEY", 79 | "name": "SSL_KEY" 80 | }, 81 | { 82 | "default": "", 83 | "label": "SSL_CERT", 84 | "name": "SSL_CERT" 85 | }, 86 | { 87 | "default": "true", 88 | "label": "FORCE_HTTPS", 89 | "name": "FORCE_HTTPS" 90 | }, 91 | { 92 | "default": "true", 93 | "label": "ENABLE_UPDATES", 94 | "name": "ENABLE_UPDATES" 95 | }, 96 | { 97 | "default": "1", 98 | "label": "WEB_CONCURRENCY", 99 | "name": "WEB_CONCURRENCY" 100 | }, 101 | { 102 | "default": "5120000", 103 | "label": "MAXIMUM_IMPORT_SIZE", 104 | "name": "MAXIMUM_IMPORT_SIZE" 105 | }, 106 | { 107 | "default": "http", 108 | "label": "DEBUG", 109 | "name": "DEBUG" 110 | }, 111 | { 112 | "default": "info", 113 | "label": "LOG_LEVEL", 114 | "name": "LOG_LEVEL" 115 | }, 116 | { 117 | "default": "", 118 | "label": "GOOGLE_ANALYTICS_ID", 119 | "name": "GOOGLE_ANALYTICS_ID" 120 | }, 121 | { 122 | "default": "", 123 | "label": "SENTRY_DSN", 124 | "name": "SENTRY_DSN" 125 | }, 126 | { 127 | "default": "", 128 | "label": "SENTRY_TUNNEL", 129 | "name": "SENTRY_TUNNEL" 130 | }, 131 | { 132 | "default": "", 133 | "label": "SMTP_HOST", 134 | "name": "SMTP_HOST" 135 | }, 136 | { 137 | "default": "", 138 | "label": "SMTP_PORT", 139 | "name": "SMTP_PORT" 140 | }, 141 | { 142 | "default": "", 143 | "label": "SMTP_USERNAME", 144 | "name": "SMTP_USERNAME" 145 | }, 146 | { 147 | "default": "", 148 | "label": "SMTP_PASSWORD", 149 | "name": "SMTP_PASSWORD" 150 | }, 151 | { 152 | "default": "", 153 | "label": "SMTP_FROM_EMAIL", 154 | "name": "SMTP_FROM_EMAIL" 155 | }, 156 | { 157 | "default": "", 158 | "label": "SMTP_REPLY_EMAIL", 159 | "name": "SMTP_REPLY_EMAIL" 160 | }, 161 | { 162 | "default": "", 163 | "label": "SMTP_TLS_CIPHERS", 164 | "name": "SMTP_TLS_CIPHERS" 165 | }, 166 | { 167 | "default": "true", 168 | "label": "SMTP_SECURE", 169 | "name": "SMTP_SECURE" 170 | }, 171 | { 172 | "default": "en_US", 173 | "label": "DEFAULT_LANGUAGE", 174 | "name": "DEFAULT_LANGUAGE" 175 | }, 176 | { 177 | "default": "true", 178 | "label": "RATE_LIMITER_ENABLED", 179 | "name": "RATE_LIMITER_ENABLED" 180 | }, 181 | { 182 | "default": "1000", 183 | "label": "RATE_LIMITER_REQUESTS", 184 | "name": "RATE_LIMITER_REQUESTS" 185 | }, 186 | { 187 | "default": "60", 188 | "label": "RATE_LIMITER_DURATION_WINDOW", 189 | "name": "RATE_LIMITER_DURATION_WINDOW" 190 | } 191 | ], 192 | "logo": "https://avatars.githubusercontent.com/u/1765001", 193 | "name": "outline", 194 | "note": "Open source collaborative knowledge base for modern teams", 195 | "platform": "linux", 196 | "repository": { 197 | "stackfile": "sources/stacks/outline.yml", 198 | "url": "https://github.com/lissy93/portainer-templates" 199 | }, 200 | "restart_policy": "unless-stopped", 201 | "title": "Outline", 202 | "type": 3 203 | }, 204 | { 205 | "categories": [ 206 | "Web", 207 | "Network" 208 | ], 209 | "description": " The Cloud Native Application Proxy ", 210 | "env": [], 211 | "logo": "https://raw.githubusercontent.com/traefik/traefik/master/docs/content/assets/img/traefik.logo.png", 212 | "name": "traefik", 213 | "platform": "linux", 214 | "repository": { 215 | "stackfile": "sources/stacks/traefik.yml", 216 | "url": "https://github.com/lissy93/portainer-templates" 217 | }, 218 | "restart_policy": "unless-stopped", 219 | "title": "Outline", 220 | "type": 3 221 | } 222 | ] 223 | } 224 | -------------------------------------------------------------------------------- /sources/stacks/cryptpad.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | --- 6 | version: '3.8' 7 | 8 | services: 9 | cryptpad: 10 | image: "cryptpad/cryptpad:version-5.5.0" 11 | hostname: cryptpad 12 | 13 | environment: 14 | - CPAD_MAIN_DOMAIN=https://your-main-domain.com 15 | - CPAD_SANDBOX_DOMAIN=https://your-sandbox-domain.com 16 | - CPAD_CONF=/cryptpad/config/config.js 17 | 18 | volumes: 19 | - ./data/blob:/cryptpad/blob 20 | - ./data/block:/cryptpad/block 21 | - ./customize:/cryptpad/customize 22 | - ./data/data:/cryptpad/data 23 | - ./data/files:/cryptpad/datastore 24 | 25 | ports: 26 | - "3000:3000" 27 | - "3001:3001" 28 | - "3003:3003" 29 | 30 | ulimits: 31 | nofile: 32 | soft: 1000000 33 | hard: 1000000 34 | -------------------------------------------------------------------------------- /sources/stacks/databag.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | databag: 4 | container_name: databag 5 | image: balzack/databag:latest 6 | ports: 7 | - "7000:7000" 8 | volumes: 9 | - ./databag-data:/var/lib/databag 10 | -------------------------------------------------------------------------------- /sources/stacks/outline.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | outline: 5 | image: docker.getoutline.com/outlinewiki/outline:latest 6 | env_file: ./docker.env 7 | ports: 8 | - "3000:3000" 9 | depends_on: 10 | - postgres 11 | - redis 12 | - storage 13 | 14 | redis: 15 | image: redis 16 | env_file: ./docker.env 17 | ports: 18 | - "6379:6379" 19 | volumes: 20 | - ./redis.conf:/redis.conf 21 | command: ["redis-server", "/redis.conf"] 22 | healthcheck: 23 | test: ["CMD", "redis-cli", "ping"] 24 | interval: 10s 25 | timeout: 30s 26 | retries: 3 27 | 28 | postgres: 29 | image: postgres 30 | env_file: ./docker.env 31 | ports: 32 | - "5432:5432" 33 | volumes: 34 | - database-data:/var/lib/postgresql/data 35 | healthcheck: 36 | test: ["CMD", "pg_isready"] 37 | interval: 30s 38 | timeout: 20s 39 | retries: 3 40 | environment: 41 | POSTGRES_USER: 'user' 42 | POSTGRES_PASSWORD: 'pass' 43 | POSTGRES_DB: 'outline' 44 | 45 | storage: 46 | image: minio/minio 47 | env_file: ./docker.env 48 | ports: 49 | - "9000:9000" 50 | entrypoint: sh 51 | command: -c 'minio server' 52 | deploy: 53 | restart_policy: 54 | condition: on-failure 55 | volumes: 56 | - storage-data:/data 57 | healthcheck: 58 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 59 | interval: 30s 60 | timeout: 20s 61 | retries: 3 62 | 63 | https-portal: 64 | image: steveltn/https-portal 65 | env_file: ./docker.env 66 | ports: 67 | - '80:80' 68 | - '443:443' 69 | links: 70 | - outline 71 | - storage 72 | restart: always 73 | volumes: 74 | - https-portal-data:/var/lib/https-portal 75 | healthcheck: 76 | test: ["CMD", "service", "nginx", "status"] 77 | interval: 30s 78 | timeout: 20s 79 | retries: 3 80 | environment: 81 | DOMAINS: 'docs.mycompany.com -> http://outline:3000' 82 | STAGE: 'production' 83 | WEBSOCKET: 'true' 84 | 85 | volumes: 86 | https-portal-data: 87 | storage-data: 88 | database-data: -------------------------------------------------------------------------------- /sources/stacks/terraria-server.yml: -------------------------------------------------------------------------------- 1 | services: 2 | terraria-server: 3 | # Github mirror: ghcr.io/hexlo/terraria-server-docker:latest 4 | image: hexlo/terraria-server-docker:latest 5 | container_name: terraria-server 6 | restart: unless-stopped 7 | stdin_open: true 8 | tty: true 9 | ports: 10 | - 7779:7777 11 | volumes: 12 | - type: bind 13 | source: ./Worlds 14 | target: /root/.local/share/Terraria/Worlds 15 | environment: 16 | - world=/root/.local/share/Terraria/Worlds/newworld.wld 17 | - password=hello 18 | - motd="Welcome to my server! :)" 19 | -------------------------------------------------------------------------------- /sources/stacks/traefik.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | reverse-proxy: 5 | image: traefik:v2.10 6 | command: --api.insecure=true --providers.docker 7 | ports: 8 | - "80:80" 9 | - "8080:8080" 10 | - target: 80 11 | published: 80 12 | protocol: tcp 13 | mode: host 14 | - target: 8080 15 | published: 8080 16 | protocol: tcp 17 | mode: host 18 | restart: always 19 | volumes: 20 | - type: bind 21 | source: /var/run/docker.sock 22 | target: /var/run/docker.sock 23 | -------------------------------------------------------------------------------- /sources/stacks/valheim-server-docker.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | valheim: 5 | image: ghcr.io/lloesche/valheim-server 6 | cap_add: 7 | - sys_nice 8 | volumes: 9 | - $HOME/valheim-server/config:/config 10 | - $HOME/valheim-server/data:/opt/valheim 11 | ports: 12 | - "2456-2458:2456-2458/udp" 13 | - "9001:9001/tcp" 14 | env_file: 15 | - $HOME/valheim-server/valheim.env 16 | restart: always 17 | stop_grace_period: 2m 18 | -------------------------------------------------------------------------------- /sources/stacks/ztnet/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | volumes: 3 | zerotier: 4 | postgres-data: 5 | caddy_data: 6 | 7 | networks: 8 | app-network: 9 | driver: bridge 10 | ipam: 11 | driver: default 12 | config: 13 | - subnet: 172.31.255.0/29 14 | 15 | services: 16 | postgres: 17 | image: postgres:15.2-alpine 18 | container_name: postgres 19 | restart: unless-stopped 20 | environment: 21 | POSTGRES_USER: postgres 22 | POSTGRES_PASSWORD: postgres 23 | POSTGRES_DB: ztnet 24 | volumes: 25 | - postgres-data:/var/lib/postgresql/data 26 | networks: 27 | - app-network 28 | 29 | zerotier: 30 | image: zyclonite/zerotier:1.14.0 31 | hostname: zerotier 32 | container_name: zerotier 33 | restart: unless-stopped 34 | volumes: 35 | - zerotier:/var/lib/zerotier-one 36 | cap_add: 37 | - NET_ADMIN 38 | - SYS_ADMIN 39 | devices: 40 | - /dev/net/tun:/dev/net/tun 41 | networks: 42 | - app-network 43 | ports: 44 | - "9993:9993/udp" 45 | environment: 46 | - ZT_OVERRIDE_LOCAL_CONF=true 47 | - ZT_ALLOW_MANAGEMENT_FROM=172.31.255.0/29 48 | 49 | ztnet: 50 | image: sinamics/ztnet:latest 51 | container_name: ztnet 52 | working_dir: /app 53 | volumes: 54 | - zerotier:/var/lib/zerotier-one 55 | restart: unless-stopped 56 | ports: 57 | - 127.0.0.1:3000:3000 58 | environment: 59 | POSTGRES_HOST: postgres 60 | POSTGRES_PORT: 5432 61 | POSTGRES_USER: postgres 62 | POSTGRES_PASSWORD: postgres 63 | POSTGRES_DB: ztnet 64 | NEXTAUTH_URL: "http://your-url-or-ip:3000" 65 | NEXTAUTH_SECRET: "random_secret" 66 | NEXTAUTH_URL_INTERNAL: "http://ztnet:3000" # Internal NextAuth URL for 'ztnet' container on port 3000. Do not change unless modifying container name. 67 | networks: 68 | - app-network 69 | links: 70 | - postgres 71 | depends_on: 72 | - postgres 73 | - zerotier 74 | 75 | https-proxy: 76 | image: caddy:latest 77 | container_name: ztnet-https-proxy 78 | restart: unless-stopped 79 | depends_on: 80 | - ztnet 81 | command: caddy reverse-proxy --from --to ztnet:3000 82 | volumes: 83 | - caddy_data:/data 84 | networks: 85 | - app-network 86 | links: 87 | - ztnet 88 | ports: 89 | - "80:80" 90 | - "443:443" 91 | --------------------------------------------------------------------------------