mirror of
https://github.com/nickyeoman/docker-compose-cookbooks.git
synced 2026-09-03 18:36:22 +00:00
converting projects to portainer ready
This commit is contained in:
@@ -8,8 +8,13 @@ Docker Compose file collection reference with the intent of running in productio
|
|||||||
|
|
||||||
## Requirements and Support
|
## Requirements and Support
|
||||||
|
|
||||||
* Minimum Docker Engine: 20.10.0
|
I'm only one guy, I tested with
|
||||||
* Minimum Docker Compose CLI: v2.0.0
|
|
||||||
|
| Software | Version |
|
||||||
|
|---------------------|-----------|
|
||||||
|
| Debian | 13 |
|
||||||
|
| Docker Engine | 29.0.4 |
|
||||||
|
| Portainer | 2.33.4 |
|
||||||
|
|
||||||
## 🤔 Assumptions
|
## 🤔 Assumptions
|
||||||
|
|
||||||
@@ -29,6 +34,12 @@ The intended workflow is as follows:
|
|||||||
|
|
||||||
You may have to use multiple containers, such as Maria or postgres for db.
|
You may have to use multiple containers, such as Maria or postgres for db.
|
||||||
|
|
||||||
|
## 🛠 Project Directory Structure
|
||||||
|
|
||||||
|
Directories ending with _dev contain projects that are still under development or experimental. These are not yet considered production-ready.
|
||||||
|
|
||||||
|
Directories ending with _notes contain projects that usually don’t require a full Docker Compose file. They may include notes, example commands, or minimal Compose files just to illustrate setup. For example, the ollama project has a docker-compose.yml showing CPU vs GPU usage.
|
||||||
|
|
||||||
### Decisions
|
### Decisions
|
||||||
|
|
||||||
#### container_name
|
#### container_name
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -15,12 +16,6 @@ OUTPUT_FILE = REPO_ROOT / "templates.json"
|
|||||||
# Extract "Overview" section from README.md (strict)
|
# Extract "Overview" section from README.md (strict)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def extract_overview(readme_path: Path, project_name: str) -> str:
|
def extract_overview(readme_path: Path, project_name: str) -> str:
|
||||||
"""
|
|
||||||
Extracts the ## Overview section from README.md.
|
|
||||||
- Must match '## Overview' exactly (case-sensitive)
|
|
||||||
- Stops at the next line starting with '#'
|
|
||||||
- If no ## Overview section, returns '<project_name> description to come'
|
|
||||||
"""
|
|
||||||
if not readme_path.exists():
|
if not readme_path.exists():
|
||||||
return f"{project_name} description to come"
|
return f"{project_name} description to come"
|
||||||
|
|
||||||
@@ -45,37 +40,127 @@ def extract_overview(readme_path: Path, project_name: str) -> str:
|
|||||||
return " ".join(overview)
|
return " ".join(overview)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Logo URL mapping
|
# Logo URL mapping with existence check
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def find_logo_url(dir_path: Path) -> str:
|
def find_logo_url(dir_path: Path) -> str:
|
||||||
|
base_url = "https://i.4lt.ca/cookbooks/"
|
||||||
name = dir_path.name
|
name = dir_path.name
|
||||||
return f"https://i.4lt.ca/cookbooks/{name}.png"
|
logo_url = f"{base_url}{name}.png"
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.head(logo_url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return logo_url
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return f"{base_url}default.png"
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Parse first image name from docker-compose.yml
|
# Parse image from docker-compose.yml using sample.env defaults
|
||||||
# Handles Bash-style defaults like ${VAR:-default}
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def parse_image(compose_path: Path) -> str:
|
def parse_image(compose_path: Path, env_vars: list) -> str:
|
||||||
if not compose_path.exists():
|
if not compose_path.exists():
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
env_defaults = {v["name"]: v["default"] for v in env_vars}
|
||||||
|
|
||||||
for line in compose_path.read_text().splitlines():
|
for line in compose_path.read_text().splitlines():
|
||||||
if "image:" in line:
|
line = line.strip()
|
||||||
|
if line.startswith("image:"):
|
||||||
image = line.split("image:", 1)[1].strip().strip('"').strip("'")
|
image = line.split("image:", 1)[1].strip().strip('"').strip("'")
|
||||||
# Handle Bash-style ${VAR:-default} -> default
|
|
||||||
match = re.match(r"\$\{[^:]+:-([^}]+)\}", image)
|
# Handle ${VAR:-default} or ${VAR}
|
||||||
if match:
|
def replace_var(match):
|
||||||
image = match.group(1)
|
var_name = match.group(1)
|
||||||
|
bash_default = match.group(2)
|
||||||
|
return env_defaults.get(var_name, bash_default if bash_default else "")
|
||||||
|
|
||||||
|
# Regex matches ${VAR} or ${VAR:-default}
|
||||||
|
image = re.sub(r"\$\{([^:}]+)(?:[:-]([^}]+))?\}", replace_var, image)
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Parse ports from docker-compose.yml
|
||||||
|
# Returns a list of dicts: {"container": port, "published": port, "protocol": "tcp"}
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def parse_ports(compose_path: Path):
|
||||||
|
if not compose_path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
ports_list = []
|
||||||
|
lines = compose_path.read_text().splitlines()
|
||||||
|
in_ports = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("ports:"):
|
||||||
|
in_ports = True
|
||||||
|
continue
|
||||||
|
if in_ports:
|
||||||
|
if not stripped or not stripped.startswith("-"):
|
||||||
|
break
|
||||||
|
# Remove "- " prefix, quotes, and strip comments
|
||||||
|
port_str = stripped[1:].strip().split("#", 1)[0].strip().strip('"').strip("'")
|
||||||
|
if not port_str:
|
||||||
|
continue
|
||||||
|
if ":" in port_str:
|
||||||
|
host_port, container_port = port_str.split(":", 1)
|
||||||
|
else:
|
||||||
|
container_port = port_str
|
||||||
|
host_port = container_port
|
||||||
|
try:
|
||||||
|
container_port = int(container_port.split("/")[0].strip())
|
||||||
|
host_port = int(host_port.split("/")[0].strip())
|
||||||
|
except ValueError:
|
||||||
|
# Skip invalid ports
|
||||||
|
continue
|
||||||
|
ports_list.append({
|
||||||
|
"container": container_port,
|
||||||
|
"published": host_port,
|
||||||
|
"protocol": "tcp"
|
||||||
|
})
|
||||||
|
|
||||||
|
return ports_list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Parse volumes from docker-compose.yml
|
||||||
|
# Returns a list of dicts: {"container": container_path}
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def parse_volumes(compose_path: Path):
|
||||||
|
if not compose_path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
volumes_list = []
|
||||||
|
lines = compose_path.read_text().splitlines()
|
||||||
|
in_volumes = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("volumes:"):
|
||||||
|
in_volumes = True
|
||||||
|
continue
|
||||||
|
if in_volumes:
|
||||||
|
if not stripped or not stripped.startswith("-"):
|
||||||
|
break
|
||||||
|
vol_str = stripped[1:].strip()
|
||||||
|
parts = vol_str.split(":", 1)
|
||||||
|
container_path = parts[1] if len(parts) == 2 else parts[0]
|
||||||
|
volumes_list.append({"container": container_path})
|
||||||
|
|
||||||
|
return volumes_list
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Parse environment variables from sample.env or env-sample
|
# Parse environment variables from sample.env or env-sample
|
||||||
|
# Returns a list of dicts: {"name": ..., "default": ...}
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def parse_env_vars(dir_path: Path):
|
def parse_env_vars(dir_path: Path):
|
||||||
env_file = None
|
env_file = None
|
||||||
|
|
||||||
if (dir_path / "sample.env").exists():
|
if (dir_path / "sample.env").exists():
|
||||||
env_file = dir_path / "sample.env"
|
env_file = dir_path / "sample.env"
|
||||||
elif (dir_path / "env-sample").exists():
|
elif (dir_path / "env-sample").exists():
|
||||||
@@ -90,9 +175,11 @@ def parse_env_vars(dir_path: Path):
|
|||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
if "=" in line:
|
if "=" in line:
|
||||||
key = line.split("=", 1)[0].strip()
|
key, value = line.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('"').strip("'")
|
||||||
if key:
|
if key:
|
||||||
env_list.append(key)
|
env_list.append({"name": key, "default": value})
|
||||||
|
|
||||||
return env_list
|
return env_list
|
||||||
|
|
||||||
@@ -105,15 +192,14 @@ def generate_template_object(dir_path: Path):
|
|||||||
readme_file = dir_path / "README.md"
|
readme_file = dir_path / "README.md"
|
||||||
|
|
||||||
description = extract_overview(readme_file, name)
|
description = extract_overview(readme_file, name)
|
||||||
if not description.strip():
|
|
||||||
description = f"{name} Docker Compose stack"
|
|
||||||
|
|
||||||
logo = find_logo_url(dir_path)
|
logo = find_logo_url(dir_path)
|
||||||
image = parse_image(compose_file)
|
|
||||||
env_vars = parse_env_vars(dir_path)
|
env_vars = parse_env_vars(dir_path)
|
||||||
|
image = parse_image(compose_file, env_vars)
|
||||||
|
ports = parse_ports(compose_file)
|
||||||
|
volumes = parse_volumes(compose_file)
|
||||||
|
|
||||||
env_entries = [
|
env_entries = [
|
||||||
{"name": v, "label": v, "default": ""} for v in env_vars
|
{"name": v["name"], "label": v["name"], "default": v["default"]} for v in env_vars
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -130,7 +216,9 @@ def generate_template_object(dir_path: Path):
|
|||||||
"stackfile": f"{name}/docker-compose.yml",
|
"stackfile": f"{name}/docker-compose.yml",
|
||||||
"stackfile_plain": False
|
"stackfile_plain": False
|
||||||
},
|
},
|
||||||
"env": env_entries
|
"env": env_entries,
|
||||||
|
"ports": ports,
|
||||||
|
"volumes": volumes
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -144,7 +232,7 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if item.name.startswith("_"):
|
if item.name.startswith("_"):
|
||||||
continue
|
continue
|
||||||
if item.name.endswith("_dev"): # development/experimental projects
|
if item.name.endswith(("_dev", "_notes")):
|
||||||
continue
|
continue
|
||||||
if not (item / "docker-compose.yml").exists():
|
if not (item / "docker-compose.yml").exists():
|
||||||
continue
|
continue
|
||||||
|
|||||||
+11
-3
@@ -1,6 +1,14 @@
|
|||||||
# Bookstack
|
# Bookstack
|
||||||
|
|
||||||
Dockerhub: https://hub.docker.com/r/solidnerd/bookstack
|
## Overview
|
||||||
Docker Compose: https://github.com/solidnerd/docker-bookstack/blob/master/docker-compose.yml
|
|
||||||
|
BookStack is a self-hosted wiki platform for organizing and storing documentation in a simple, book/chapter/page structure. This Docker Compose stack allows easy deployment with persistent storage.
|
||||||
|
|
||||||
|
## Project Details
|
||||||
|
|
||||||
|
- **Project Repository:** [Bookstack Official](https://www.bookstackapp.com/)
|
||||||
|
- **Container Image:** [Docker Hub](https://hub.docker.com/r/solidnerd/bookstack)
|
||||||
|
- **Compose Example:** [Compose](https://github.com/solidnerd/docker-bookstack/blob/master/docker-compose.yml)
|
||||||
|
- **Documentation:** [Docs](https://www.bookstackapp.com/docs/)
|
||||||
|
- **Reverse Proxy Port:** `8080`
|
||||||
|
|
||||||
Reverse Proxy Port: 8080
|
|
||||||
|
|||||||
@@ -2,16 +2,49 @@ services:
|
|||||||
bookstack:
|
bookstack:
|
||||||
image: ${BOOKSTACK_IMAGE:-solidnerd/bookstack:latest}
|
image: ${BOOKSTACK_IMAGE:-solidnerd/bookstack:latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
volumes:
|
||||||
|
- bookstack-php-config:/usr/local/etc/php/php.ini
|
||||||
|
- bookstack-storage:/var/www/bookstack/storage/uploads
|
||||||
|
- bookstack-uploads:/var/www/bookstack/public/uploads
|
||||||
environment:
|
environment:
|
||||||
- APP_KEY=${BOOKSTACK_APP_KEY:-APP_KEY}
|
- APP_KEY=${BOOKSTACK_APP_KEY:-APP_KEY}
|
||||||
- APP_TIMEZONE=${BOOKSTACK_APP_TIMEZONE:-America/Vancouver}
|
- APP_TIMEZONE=${BOOKSTACK_APP_TIMEZONE:-America/Vancouver}
|
||||||
- APP_URL=${BOOKSTACK_APP_URL:-http://localhost:8080}
|
- APP_URL=${BOOKSTACK_APP_URL:-http://localhost:8080}
|
||||||
- DB_DATABASE=${BOOKSTACK_DB_DATABASE:-dbname}
|
- DB_DATABASE=${BOOKSTACK_DB_DATABASE:-bookstack}
|
||||||
- DB_HOST=${BOOKSTACK_DB_HOST:-bookstack-mariadb:3306}
|
- DB_HOST=mariadb:3306
|
||||||
- DB_PASSWORD=${BOOKSTACK_DB_PASSWORD:-dbpass}
|
- DB_PASSWORD=${BOOKSTACK_DB_PASSWORD:-dbpass}
|
||||||
- DB_USERNAME=${BOOKSTACK_DB_USERNAME:-dbuser}
|
- DB_USERNAME=${BOOKSTACK_DB_USERNAME:-dbuser}
|
||||||
- REVISION_LIMIT=${BOOKSTACK_REVISION_LIMIT:-false}
|
- REVISION_LIMIT=${BOOKSTACK_REVISION_LIMIT:-false}
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- internal
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: ${MARIADB_IMAGE:-mariadb:latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
|
||||||
|
environment:
|
||||||
|
- MARIADB_DATABASE=${MARIADB_MARIADB_DATABASE:-bookstack}
|
||||||
|
- MARIADB_ROOT_PASSWORD=${MARIADB_MARIADB_ROOT_PASSWORD:-ChangeThisPassword}
|
||||||
|
- MARIADB_USER=${MARIADB_MARIADB_USER:-dbuser}
|
||||||
|
- MARIADB_PASSWORD=${MARIADB_MARIADB_PASSWORD:-AlsoChangeThisPassword}
|
||||||
volumes:
|
volumes:
|
||||||
- ${VOL_CONFIG_PATH:-./config}/php.ini:/usr/local/etc/php/php.ini
|
- mariadb-data:/var/lib/mysql
|
||||||
- ${VOL_PATH:-./data}/bookstack-storage:/var/www/bookstack/storage/uploads
|
networks:
|
||||||
- ${VOL_PATH:-./data}/bookstack-uploads:/var/www/bookstack/public/uploads
|
- internal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bookstack-php-config:
|
||||||
|
bookstack-storage:
|
||||||
|
bookstack-uploads:
|
||||||
|
mariadb-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
internal:
|
||||||
|
external: true
|
||||||
|
|||||||
+13
-9
@@ -1,14 +1,18 @@
|
|||||||
COOKBOOK=/git/docker-compose-cookbooks #HELP: cookbooks
|
|
||||||
VOL_CONFIG_PATH=./config #HELP: configpath
|
|
||||||
VOL_PATH=./data #HELP: volpath
|
|
||||||
|
|
||||||
# Bookstack
|
# Bookstack
|
||||||
BOOKSTACK_APP_KEY=APP_KEY #HELP: gen32
|
BOOKSTACK_APP_KEY=APP_KEY
|
||||||
BOOKSTACK_APP_TIMEZONE=America/Vancouver
|
BOOKSTACK_APP_TIMEZONE=America/Vancouver
|
||||||
BOOKSTACK_APP_URL=http://localhost:8248
|
BOOKSTACK_APP_URL=http://localhost:8080
|
||||||
BOOKSTACK_DB_DATABASE=dbname
|
BOOKSTACK_DB_DATABASE=bookstack
|
||||||
BOOKSTACK_DB_HOST=bookstack-db:3306
|
BOOKSTACK_DB_HOST=mariadb:3306
|
||||||
BOOKSTACK_DB_PASSWORD=dbpass #HELP: gen32
|
BOOKSTACK_DB_PASSWORD=dbpass
|
||||||
BOOKSTACK_DB_USERNAME=dbuser
|
BOOKSTACK_DB_USERNAME=dbuser
|
||||||
BOOKSTACK_IMAGE=solidnerd/bookstack:latest
|
BOOKSTACK_IMAGE=solidnerd/bookstack:latest
|
||||||
BOOKSTACK_REVISION_LIMIT=false
|
BOOKSTACK_REVISION_LIMIT=false
|
||||||
|
|
||||||
|
# MariaDB
|
||||||
|
MARIADB_IMAGE=mariadb:latest
|
||||||
|
MARIADB_MARIADB_DATABASE=bookstack
|
||||||
|
MARIADB_MARIADB_ROOT_PASSWORD=ChangeThisPassword
|
||||||
|
MARIADB_MARIADB_USER=dbuser
|
||||||
|
MARIADB_MARIADB_PASSWORD=AlsoChangeThisPassword
|
||||||
|
|
||||||
|
|||||||
+1184
-778
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user