├── .github └── FUNDING.yml ├── .gitignore ├── COPYING ├── ChangeLog ├── Readme.adoc ├── book ├── .Dockerfile ├── .Gemfile ├── .extensions.rb ├── .internal-build.py ├── Makefile ├── Readme.md ├── build.sh ├── img │ ├── DNS-traffic-diagram.drawio │ ├── DNS-traffic-diagram.png │ ├── DNS-traffic-diagram.svg │ ├── WAN-to-LAN-traffic.png │ ├── WAN-to-LAN-traffic.svg │ ├── YT-audit.png │ ├── YT-censor.png │ ├── bird-on-server.jpg │ ├── bread-server.png │ ├── cc-by-sa.svg │ ├── chapter-break.svg │ ├── cover.png │ ├── inside-chassis.jpg │ ├── jellyfin.png │ ├── nextcloud.png │ ├── puppy.jpg │ ├── racked-server.jpg │ ├── scratch.png │ ├── seagl-crew.jpg │ ├── service-stack.drawio │ ├── service-stack.svg │ ├── squeaky-clean-chicken.png │ ├── sunrisedata-logo.svg │ ├── traefik-architecture.png │ ├── wallabag.png │ └── watchtower.png ├── pdf-theme │ ├── print.yml │ └── screen.yml └── steadfast.asciidoc ├── issues.adoc ├── mario ├── COPYING ├── Readme.md └── ansible │ ├── ansible.cfg │ ├── hosts.yml │ ├── playbook.yml │ ├── provision.sh │ ├── roles │ ├── base │ │ ├── files │ │ │ └── enable-passwordless-sudo │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── docker │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ └── services │ │ ├── files │ │ ├── dc-bash-completion │ │ ├── dc.sh │ │ └── php-lint.sh │ │ ├── tasks │ │ └── main.yml │ │ ├── templates │ │ └── ops │ │ │ ├── jellyfin │ │ │ └── compose.yml │ │ │ ├── nextcloud │ │ │ └── compose.yml │ │ │ ├── scratch │ │ │ ├── compose.yml │ │ │ └── custom │ │ │ │ ├── Dockerfile │ │ │ │ └── webpack.config.js │ │ │ ├── traefik │ │ │ └── compose.yml │ │ │ ├── wallabag │ │ │ └── compose.yml │ │ │ └── watchtower │ │ │ └── compose.yml │ │ └── vars │ │ └── main.yml │ └── template │ └── config ├── pelican ├── .python_history ├── Dockerfile ├── Readme.md ├── build.sh ├── requirements.txt └── website │ ├── Makefile │ ├── content │ ├── extra │ │ ├── .htaccess │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-credit.txt │ │ ├── favicon.ico │ │ ├── funding.json │ │ ├── robots.txt │ │ └── site.webmanifest │ ├── images │ │ ├── premium-bread.jpg │ │ ├── premium-gopher.jpg │ │ ├── standard-bread.jpg │ │ ├── standard-gopher.jpg │ │ └── sustain204.jpg │ ├── news │ │ ├── beta.md │ │ ├── ebook.md │ │ ├── evergreen.md │ │ ├── faq.md │ │ ├── fossy.md │ │ ├── fossy2023.md │ │ ├── global.md │ │ ├── lp2024.md │ │ ├── paperback.md │ │ ├── resources.md │ │ ├── reviews.md │ │ ├── seagl2023.md │ │ ├── start.md │ │ ├── strong.md │ │ ├── toc.md │ │ └── workshops.md │ └── pages │ │ ├── 404.md │ │ ├── buy.md │ │ ├── code.md │ │ ├── contact.md │ │ ├── home.md │ │ └── rules.md │ ├── pelicanconf.py │ ├── publishconf.py │ └── theme │ ├── static │ ├── css │ │ └── main.css │ └── media │ │ └── book-cover.png │ └── templates │ ├── archives.html │ ├── article.html │ ├── author.html │ ├── authors.html │ ├── base.html │ ├── categories.html │ ├── category.html │ ├── index.html │ ├── page.html │ ├── pagination.html │ ├── period_archives.html │ ├── tag.html │ ├── tags.html │ └── translations.html └── style-guide.adoc /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: meonkeys 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | book/Gemfile.lock 3 | book/steadfast.epub 4 | book/steadfast.html 5 | book/steadfast.print.pdf 6 | book/steadfast.screen.pdf 7 | mario/ansible/.first-run-complete 8 | mario/ansible/config 9 | pelican/website/output/ 10 | pelican/.bash_history 11 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2025-04-15 Adam Monsen 2 | 3 | 1.3.3 - ebook update 4 | 5 | * Typesetting improvements from Georger Araújo. 6 | 7 | 2024-08-19 Adam Monsen 8 | 9 | 1.3.2 - ebook update 10 | 11 | * Include latest errata. 12 | 13 | 2024-06-25 Adam Monsen 14 | 15 | 1.3.1 - ebook update 16 | 17 | * Include latest errata. 18 | 19 | * Additional code snippet, so Listing count incremented. 20 | 21 | 2024-06-06 Adam Monsen 22 | 23 | 1.2.2 - ebook update 24 | 25 | * Improve index in screen-optimized PDF. 26 | 27 | 2024-06-04 Adam Monsen 28 | 29 | 1.2.1 - print edition 30 | 31 | 2024-05-30 Adam Monsen 32 | 33 | 1.2.0 - ebook release 34 | 35 | * Now available for purchase on Gumroad. 36 | 37 | * Assigned ISBNs, set up discussion forum. 38 | 39 | * HQ paperback looks great, will publish / print that soon. 40 | 41 | 2024-05-17 Adam Monsen 42 | 43 | 1.1.0 - tech review complete 44 | 45 | * HQ proofs are off to printer! 46 | 47 | * Beta in progress, going well so far. 48 | 49 | * Use CC BY-SA for book instead of CC BY-NC-SA. 50 | 51 | 2024-03-12 Adam Monsen 52 | 53 | 1.0.2 - tech review in progress 54 | 55 | * Major changes based on WIP tech review with ln1draw. 56 | 57 | * book/build.sh: port to macOS 58 | 59 | 2024-02-08 Adam Monsen 60 | 61 | 1.0.1 - manuscript / evaluation copy for publishers 62 | 63 | * Typeset print PDF to 6" x 9" (US Trade) size. Add placeholder foreword. 64 | 65 | * Overhaul and simplify build. Split print and screen PDF generation. 66 | 67 | 2024-01-14 Adam Monsen 68 | 69 | v5 - reviewers only 70 | 71 | * Add final image (squeaky-clean-chicken.png). Still accepting edits. 72 | 73 | * I'm working on a print ready PDF. I want a smaller-size A5 book rather 74 | than the larger default A4 size. 75 | 76 | 2024-01-07 Adam Monsen 77 | 78 | v4 - reviewers only 79 | 80 | * Last version for review, maybe! All the inline FIXMEs and TODOs are 81 | done. There are still some open issues. 82 | 83 | * I'm working on the index. 84 | 85 | 2023-11-06 Adam Monsen 86 | 87 | v3 - reviewers only 88 | 89 | * This includes quite a few updates. I expanded the style guide and 90 | conformed links and other aspects of look & feel. I just need to digest 91 | any last review feedback and prepare for printing. 92 | 93 | * I've expanded the review window a bit. If you haven't had a chance to 94 | review and still want to, go for it! Anything helps. 95 | 96 | 2023-11-18 Adam Monsen 97 | 98 | v2 - reviewers only 99 | 100 | * Many copy edits and revisions 101 | 102 | * Reorder some sections 103 | 104 | * Port build from Pandoc to Asciidoctor - more and better-looking outputs 105 | 106 | 2023-11-10 Adam Monsen 107 | 108 | v1 - first release, reviewers only 109 | 110 | * embed all images, etc. in HTML output 111 | -------------------------------------------------------------------------------- /Readme.adoc: -------------------------------------------------------------------------------- 1 | = 📖 Steadfast Self-Hosting 2 | :hide-uri-scheme: 3 | 4 | Source code for a most excellent book about self-hosting. 5 | 6 | image::pelican/website/theme/static/media/book-cover.png[align="center",alt="Book cover art with title text, author name, and featuring our Steadfast hero in a cartoon fantasy rolling-hills landscape on a partly cloudy day, holding up their hand in triumph with the apps they successfully self-host magically extending into the air. Our hero has brown-skin, a half-shaved head of dark hair, purple cape, teal shoulder puffs, white sleeves, blue gloves, brown shirt, tan equipment strap and belt, red upper leggings, teal lower leggings, and light brown boots."] 7 | 8 | _Steadfast_ primarily targets aspiring self-hosters looking for a quick and reliable method to get started. 9 | 10 | == 📖 Get the book 11 | 12 | Please https://selfhostbook.com/buy/[buy] or <> a <> of the book for your reading pleasure. 13 | You may also https://selfhostbook.com/buy/#donations[donate] to support my efforts. 14 | 15 | Commits here serve as errata corrected since the date and version mentioned in the "`Book version`" section of the "`Introduction`" chapter of the copy you own. 16 | 17 | == 📂 Directory structure 18 | 19 | [cols="1,2"] 20 | |=== 21 | |path |description 22 | 23 | |`book/` |sources for the book 24 | |`mario/` |sources for mario provisioning tool 25 | |`pelican/` |sources for https://selfhostbook.com 26 | |=== 27 | 28 | [#book_formats] 29 | == 🖨️ Book formats 30 | 31 | |=== 32 | |version |device |quality |index |_Cross references_ chapter 33 | 34 | |EPUB |calibre e-book viewer |great 🤩 |No |Yes 35 | |EPUB |Kobo e-reader |ok 😐 |No |Yes 36 | |EPUB |FBReader |bad ☹️ |No |Yes 37 | |EPUB |macOS Books |good 🙂 |No |Yes 38 | |EPUB |Kindle |ok 😐 |No |Yes 39 | |HTML |Firefox web browser |good 🙂 |No |Yes 40 | |raw Asciidoc |https://docs.asciidoctor.org/browser-extension/[web browser] |good 🙂 |No |No 41 | |screen PDF |Firefox web browser |good 🙂 |Yes |Yes 42 | |screen PDF |evince PDF viewer |good 🙂 |Yes |Yes 43 | |print PDF |evince PDF viewer |good 🙂 |Yes |No 44 | |print PDF |Firefox web browser |good 🙂 |Yes |No 45 | |standard quality paperback |ink & paper |good 🙂 |Yes |No 46 | |premium quality paperback |ink & paper |great 🤩 |Yes |No 47 | |=== 48 | 49 | The quality ratings are opinions/guesses by the author. 50 | 51 | The index is a section in the back of the book with references to select notable occurrences of terms and their page numbers. 52 | Asciidoctor https://docs.asciidoctor.org/asciidoc/latest/sections/user-index/[only generates an index for PDF output]. 53 | I think that's OK since the other book formats are searchable. 54 | 55 | The print PDF excludes _Cross references_ because page numbers are not rendered, reducing the usefulness of this chapter in print. See am64 in `issues.adoc`. 56 | 57 | [#how_to_build] 58 | == 🏗️ How to build the book 59 | 60 | Run `./book/build.sh` to generate your own typeset outputs. 61 | See `book/Readme.md` for details. 62 | 63 | This build should be ported to more operating systems. 64 | See am59 in `issues.adoc`. 65 | 66 | == 🪡 Patches welcome 67 | 68 | Your contributions are most welcome! 69 | When submitting a patch, please: 70 | 71 | . Heed the link:style-guide.adoc[style guide]. 72 | . Sign off every commit (`git commit --signoff`). 73 | Sorry, I know this is annoying, but it is important. 74 | It certifies you wrote or otherwise have the right to submit the patch, following https://developercertificate.org[Developer Certificate of Origin, version 1.1]. 75 | 76 | == 📜 Warranty 77 | 78 | None. 79 | 80 | == ⚖️ Copyright and license 81 | 82 | The book is (C)2024-2025 Adam Monsen. Some rights reserved. License is https://creativecommons.org/licenses/by-sa/4.0/[Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) License]. This notice also appears in `book/steadfast.asciidoc` and typeset outputs. 83 | 84 | mario is (C)2023-2025 Adam Monsen. License is https://www.gnu.org/licenses/agpl-3.0.html[GNU Affero General Public License] as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This notice also appears in `mario/Readme.md` and source code. 85 | 86 | == 🗨️ Feedback 87 | 88 | Please https://selfhostbook.com/contact/[send me feedback]! 89 | This list of questions was originally shared with early reviewers; left here since I want to continue to iterate and improve in areas covered by these questions. 90 | 91 | * What are your first impressions? Cover, title, table of contents, font, style, page size, book length, etc. 92 | * Any/all factual, spelling, grammatical, and structural errors. 93 | * Overall manuscript critique, comprehensive or line edits, copy-editing, proofreading. 94 | * Does it make sense what I'm trying to convey and how? 95 | * Consistency of voice, level of detail, narrative flow. 96 | * Anywhere a diagram or photo would significantly help to illustrate a point. 97 | * Any sections that should be rewritten, rethought, or removed. 98 | * Is there a relevant and useful technology that isn't mentioned and should be? 99 | * Test out mario on your own hardware/VM. Does it work for you as advertised? 100 | * Is this book, this code, these ideas something you actually, personally want/read/use? 101 | * Is this something you'd recommend to others? 102 | * What would you expect to pay for a print copy? 103 | * What would you expect to pay for a digital copy? 104 | * Any feedback on the https://selfhostbook.com[book website] and this readme? 105 | 106 | == 🏷️ Tags 107 | 108 | Git tags are used on notable commits. 109 | The most recent tag name is baked into formatted book outputs at build time. 110 | Hint: try `git log --simplify-by-decoration`. 111 | 112 | The most meaningful tags appear in `ChangeLog`. 113 | Here are explanations for a few others: 114 | 115 | print-NNN:: 116 | Published print books are built from these tags. 117 | `paperback-NNN`, `lulu-NNN`, and `amazon-NNN` are deprecated. 118 | 119 | ebook-NNN:: 120 | Published ebooks are built from these tags. 121 | `gumroad-NNN` is deprecated. 122 | 123 | start-tech-review:: 124 | Commit: `2ac2b035`. 125 | This tag commemorates when Lenny and I started formal tech review. 126 | -------------------------------------------------------------------------------- /book/.Dockerfile: -------------------------------------------------------------------------------- 1 | # Linux, maybe Windows Dockerfile for making an image to build the book. 2 | 3 | FROM ruby:3.3 4 | 5 | RUN apt-get update \ 6 | && apt-get --assume-yes --no-install-recommends install \ 7 | epubcheck \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | ARG WORK_DIR 11 | 12 | ENV HOME=$WORK_DIR 13 | 14 | WORKDIR $WORK_DIR 15 | 16 | COPY .Gemfile Gemfile 17 | 18 | RUN bundle install 19 | 20 | ENTRYPOINT ["/bin/python3", ".internal-build.py"] 21 | -------------------------------------------------------------------------------- /book/.Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Use exact versions since there are no automated tests. Currently I just build EPUB/PDF/HTML/etc. outputs and visually inspect them. 4 | 5 | gem 'asciidoctor', '2.0.23' 6 | 7 | gem 'asciidoctor-epub3', '2.2.0' 8 | gem 'asciidoctor-lists', '1.1.2' 9 | gem 'asciidoctor-pdf', '2.3.19' 10 | 11 | gem 'pygments.rb', '3.0.0' 12 | gem 'epubcheck-ruby', '5.2.1.0' 13 | gem 'html-proofer', '5.0.10' 14 | -------------------------------------------------------------------------------- /book/.extensions.rb: -------------------------------------------------------------------------------- 1 | class ExtendedPDFConverter < (Asciidoctor::Converter.for 'pdf') 2 | register_for 'pdf' 3 | 4 | # Add an image at the start of each chapter. 5 | # see https://docs.asciidoctor.org/pdf-converter/latest/extend/use-cases/#chapter-image 6 | def ink_chapter_title sect, title, opts 7 | image_attrs = { 'target' => 'chapter-break.svg', 'pdfwidth' => '100%' } 8 | image_block = ::Asciidoctor::Block.new sect.document, :image, content_model: :empty, attributes: image_attrs 9 | convert_image image_block, relative_to_imagesdir: true, pinned: true 10 | super 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /book/.internal-build.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | # Build _Steadfast Self-Hosting_ formatted book outputs 4 | # Copyright (C) 2024 Adam Monsen 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | ############################################################## 20 | # Build the book, generating various typeset output formats. # 21 | ############################################################## 22 | 23 | import os 24 | import subprocess 25 | import sys 26 | 27 | validBuildTargets = ['html', 'printPdf', 'screenPdf', 'epub', 'checkhtml', 'checkepub'] 28 | 29 | commonArgs = [ 30 | '--attribute', 'build_date_time=' + os.environ['BUILD_DATE_TIME'], 31 | '--attribute', 'build_locale_lang=' + os.environ['BUILD_LOCALE_LANG'], 32 | '--attribute', 'build_git_commit=' + os.environ['BUILD_GIT_COMMIT'], 33 | '--attribute', 'build_git_branch=' + os.environ['BUILD_GIT_BRANCH'], 34 | '--attribute', 'build_git_tag=' + os.environ['BUILD_GIT_TAG'], 35 | '--attribute', 'build_os_release=' + os.environ['BUILD_OS_RELEASE'], 36 | '--warnings', 37 | '--trace', 38 | '--require', '/usr/local/bundle/gems/asciidoctor-lists-1.1.2/lib/asciidoctor-lists.rb' 39 | ] 40 | 41 | title = 'steadfast' 42 | bookSrc = title + '.asciidoc' 43 | 44 | def log(msg): 45 | print(msg) 46 | 47 | args = sys.argv[1:] 48 | 49 | if len(args) == 0 or len(args) == 1 and args[0] == 'all': 50 | args = validBuildTargets 51 | 52 | for arg in args: 53 | if arg not in validBuildTargets: 54 | log('⚠️\tinvalid argument(s)') 55 | log('') 56 | log(f'Usage: {sys.argv[0]} [all|TARGET...]') 57 | log('') 58 | log('TARGET is one or more of ' + ' '.join(validBuildTargets)) 59 | sys.exit(1) 60 | 61 | printQuality = os.environ['BUILD_PRINT_QUALITY'] 62 | if printQuality not in ['standard', 'premium']: 63 | log('⚠️\tinvalid print quality') 64 | sys.exit(1) 65 | 66 | def typeset(command, outputType, outputName): 67 | log(f'🖨️\ttypeset {outputType}') 68 | subprocess.run(command, check=True) 69 | log(f'💾\twrote {outputName}') 70 | 71 | htmlOutput = f'{title}.html' 72 | epubOutput = f'{title}.epub' 73 | printPdfOutput = f'{title}.print.pdf' 74 | screenPdfOutput = f'{title}.screen.pdf' 75 | 76 | extendConverter = ['--require', './.extensions.rb'] 77 | 78 | for arg in args: 79 | match(arg): 80 | case 'html': 81 | command = ['asciidoctor'] + commonArgs + ['--attribute', 'data-uri', bookSrc] 82 | typeset(command, 'HTML', htmlOutput) 83 | case 'printPdf': 84 | command = ['asciidoctor-pdf'] + commonArgs + extendConverter + ['--attribute', 'shb-printPDF', '--attribute', f'printQuality-{printQuality}', '--out-file', printPdfOutput, bookSrc] 85 | typeset(command, f'{printQuality} print-ready PDF', printPdfOutput) 86 | case 'screenPdf': 87 | command = ['asciidoctor-pdf'] + commonArgs + extendConverter + ['--attribute', 'shb-screenPDF', '--out-file', screenPdfOutput, bookSrc] 88 | typeset(command, 'screen-optimized PDF', screenPdfOutput) 89 | case 'epub': 90 | command = ['asciidoctor-epub3'] + commonArgs + [bookSrc] 91 | typeset(command, 'EPUB', epubOutput) 92 | case 'checkhtml': 93 | if 'html' not in args: 94 | log('⚠️\tHTML was not built -- checking anyway') 95 | subprocess.run(['htmlproofer', htmlOutput], check=False) 96 | case 'checkepub': 97 | if 'epub' not in args: 98 | log('⚠️\tEPUB was not built -- checking anyway') 99 | subprocess.run(['epubcheck', epubOutput], check=False) 100 | 101 | toUid = os.environ['BUILD_OUTPUT_UID'] 102 | toGid = os.environ['BUILD_OUTPUT_GID'] 103 | subprocess.run(['chown', '-R', f'{toUid}:{toGid}', '.']) 104 | -------------------------------------------------------------------------------- /book/Makefile: -------------------------------------------------------------------------------- 1 | steadfast.print.pdf: steadfast.asciidoc 2 | ./build.sh printPdf 3 | steadfast.epub: steadfast.asciidoc 4 | ./build.sh epub 5 | steadfast.html: steadfast.asciidoc 6 | ./build.sh html 7 | steadfast.screen.pdf: steadfast.asciidoc 8 | ./build.sh screenPdf 9 | -------------------------------------------------------------------------------- /book/Readme.md: -------------------------------------------------------------------------------- 1 | # Build the book 2 | 3 | ## Usage 4 | 5 | ```bash 6 | ./build.sh 7 | ``` 8 | 9 | The `build.sh` Bash script requires Docker and a few common cli tools. 10 | It is meant to be run as a user other than `root` [with access to Docker](https://docs.docker.com/engine/install/linux-postinstall/) (e.g. `sudo usermod -aG docker $USER`, log out, log back in). 11 | It runs on Linux and macOS, maybe Windows too. 12 | A `Makefile` is also provided as a convenience to short-circuit rapid repeated builds since the fastest possible `build.sh` invocation may still take several seconds. 13 | 14 | If you're using Ubuntu 22.04 LTS and its default version of Docker, install `docker-buildx` (e.g. `sudo apt install docker-buildx`). 15 | This extra step may be eliminted when we resolve am67 (see `../issues.adoc`). 16 | 17 | ## How to build without Docker 18 | 19 | Building without Docker is unsupported, but this might make it work for you: 20 | 21 | 1. install prerequisites mentioned in `.Dockerfile` 22 | 1. set environment variables mentioned in `build.sh` 23 | 1. run `python3 .internal-build.py` 24 | -------------------------------------------------------------------------------- /book/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Typeset book _Steadfast Self-Hosting_ 4 | # Copyright (C) 2024-2025 Adam Monsen 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | ############################################################## 20 | # Performs typesetting in a container. # 21 | # Requires Docker. # 22 | ############################################################## 23 | 24 | set -o errexit 25 | set -o nounset 26 | set -o pipefail 27 | 28 | SECONDS=0 29 | echo "🏗️ start at $(date)" 30 | 31 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 32 | WORK_DIR=/usr/src/app/book 33 | GID="$(id -g)" 34 | BUILD_DATE_TIME="$(date)" 35 | BUILD_LOCALE_LANG="en_US.UTF-8" 36 | BUILD_GIT_COMMIT="$(git rev-parse --short HEAD || echo FIXME)" 37 | BUILD_GIT_BRANCH="$(git branch --show-current || echo FIXME)" 38 | # Seems to pull the most recent tag if the current commit has more than one. 39 | # We shouldn't count on this behavior, though. 40 | BUILD_GIT_TAG="$(git describe --tags --abbrev=0 || echo FIXME)" 41 | # standard or premium - only applies to print PDF 42 | BUILD_PRINT_QUALITY="${SHB_FORCE_PRINT_QUALITY:-standard}" 43 | 44 | if [[ "$OSTYPE" =~ linux-gnu ]]; then 45 | BUILD_OS_RELEASE="$(lsb_release --short --description || echo FIXME)" 46 | else 47 | # OSTYPE is intentionally coarse. 48 | # There are surely better ways to get detailed build OS info. 49 | BUILD_OS_RELEASE="${OSTYPE:-unknown}" 50 | fi 51 | 52 | echo '🚢 build image' 53 | cd "$SCRIPT_DIR" 54 | # discard container checksum 55 | docker build \ 56 | --tag shb-asciidoctor \ 57 | --build-arg WORK_DIR="$WORK_DIR" \ 58 | --file .Dockerfile \ 59 | --quiet \ 60 | . \ 61 | > /dev/null 62 | 63 | echo '🚢 start container' 64 | docker run \ 65 | --rm \ 66 | --interactive \ 67 | --tty \ 68 | --volume "$SCRIPT_DIR:$WORK_DIR" \ 69 | --env BUILD_DATE_TIME="$BUILD_DATE_TIME" \ 70 | --env BUILD_LOCALE_LANG="$BUILD_LOCALE_LANG" \ 71 | --env BUILD_GIT_COMMIT="$BUILD_GIT_COMMIT" \ 72 | --env BUILD_GIT_BRANCH="$BUILD_GIT_BRANCH" \ 73 | --env BUILD_GIT_TAG="$BUILD_GIT_TAG" \ 74 | --env BUILD_OS_RELEASE="$BUILD_OS_RELEASE" \ 75 | --env BUILD_PRINT_QUALITY="$BUILD_PRINT_QUALITY" \ 76 | --env BUILD_OUTPUT_UID="$UID" \ 77 | --env BUILD_OUTPUT_GID="$GID" \ 78 | shb-asciidoctor \ 79 | "$@" 80 | 81 | echo "🏗️ done (${SECONDS}s elapsed)" 82 | -------------------------------------------------------------------------------- /book/img/DNS-traffic-diagram.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /book/img/DNS-traffic-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/DNS-traffic-diagram.png -------------------------------------------------------------------------------- /book/img/WAN-to-LAN-traffic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/WAN-to-LAN-traffic.png -------------------------------------------------------------------------------- /book/img/WAN-to-LAN-traffic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | WANrouterInternetLANupstairs:diningroomdownstairs:garagePoEminiswitch##mainNICserverIPMINIC 80 | 81 | 82 | -------------------------------------------------------------------------------- /book/img/YT-audit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/YT-audit.png -------------------------------------------------------------------------------- /book/img/YT-censor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/YT-censor.png -------------------------------------------------------------------------------- /book/img/bird-on-server.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/bird-on-server.jpg -------------------------------------------------------------------------------- /book/img/bread-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/bread-server.png -------------------------------------------------------------------------------- /book/img/cc-by-sa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 58 | 64 | 69 | 70 | 73 | 74 | 83 | 84 | 87 | 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 102 | 106 | 107 | 111 | 112 | 113 | 114 | 117 | 121 | 122 | 126 | 127 | 128 | 129 | 132 | 133 | 142 | 143 | 146 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 | 160 | 170 | 171 | 173 | 176 | 177 | 186 | 187 | 188 | 189 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /book/img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/cover.png -------------------------------------------------------------------------------- /book/img/inside-chassis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/inside-chassis.jpg -------------------------------------------------------------------------------- /book/img/jellyfin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/jellyfin.png -------------------------------------------------------------------------------- /book/img/nextcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/nextcloud.png -------------------------------------------------------------------------------- /book/img/puppy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/puppy.jpg -------------------------------------------------------------------------------- /book/img/racked-server.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/racked-server.jpg -------------------------------------------------------------------------------- /book/img/scratch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/scratch.png -------------------------------------------------------------------------------- /book/img/seagl-crew.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/seagl-crew.jpg -------------------------------------------------------------------------------- /book/img/service-stack.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /book/img/service-stack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bare metalUbuntu LTS 64-bit serverext4 for / (root folder)ZFS for /data (optional)DockerNextcloudJellyfinWallabaghardwareOSfilesystemcontainerruntimeservices incontainers -------------------------------------------------------------------------------- /book/img/squeaky-clean-chicken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/squeaky-clean-chicken.png -------------------------------------------------------------------------------- /book/img/sunrisedata-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 18 | 42 | 50 | 51 | 56 | 63 | 70 | 77 | 84 | 91 | 98 | 105 | 112 | 119 | 126 | 133 | 140 | 147 | 154 | 161 | 168 | 175 | 182 | 189 | 196 | 203 | 210 | 217 | 224 | 231 | 238 | 245 | 252 | 259 | 266 | 273 | 280 | 287 | 294 | 301 | 308 | 315 | 322 | 329 | 336 | 343 | 350 | 357 | 364 | 371 | 378 | 385 | 392 | 399 | 406 | 413 | 420 | 427 | 434 | 441 | 442 | 443 | -------------------------------------------------------------------------------- /book/img/traefik-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/traefik-architecture.png -------------------------------------------------------------------------------- /book/img/wallabag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/wallabag.png -------------------------------------------------------------------------------- /book/img/watchtower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/book/img/watchtower.png -------------------------------------------------------------------------------- /book/pdf-theme/print.yml: -------------------------------------------------------------------------------- 1 | # smart quotes (and many other special characters) require 2 | # "-with-font-fallbacks" here 3 | extends: default-for-print-with-font-fallbacks 4 | 5 | page: 6 | size: [6in, 9in] 7 | numbering: 8 | start-at: after-toc 9 | margin-inner: 13mm 10 | margin-outer: 13mm 11 | 12 | # when to start rendering header and footer 13 | running-content: 14 | start-at: 2 15 | 16 | footer: 17 | height: 13mm 18 | padding: [2mm, 0, 3mm, 0] 19 | verso: 20 | center: 21 | content: '{document-title}' 22 | recto: 23 | center: 24 | content: '{chapter-title}' 25 | 26 | prose: 27 | margin-bottom: 6 28 | margin-inner: 0 29 | text-indent: 0 30 | text-indent-inner: 18 31 | 32 | code: 33 | font-size: $base-font-size * 0.8 34 | 35 | codespan: 36 | font-color: #007300 37 | 38 | # This styles both hyperlinks and internal links (e.g. cross-references) 39 | link: 40 | font-color: #0b04d5 41 | 42 | toc: 43 | h2-font-style: bold 44 | 45 | image: 46 | caption: 47 | align: inherit 48 | text-align: center 49 | -------------------------------------------------------------------------------- /book/pdf-theme/screen.yml: -------------------------------------------------------------------------------- 1 | # smart quotes (and many other special characters) require 2 | # "-with-font-fallbacks" here 3 | extends: default-with-font-fallbacks 4 | 5 | footer: 6 | verso: 7 | center: 8 | content: '{document-title}' 9 | recto: 10 | center: 11 | content: '{chapter-title}' 12 | 13 | image: 14 | caption: 15 | align: inherit 16 | text-align: center 17 | -------------------------------------------------------------------------------- /issues.adoc: -------------------------------------------------------------------------------- 1 | [#issues] 2 | = ℹ️ Issues 3 | 4 | (TODO: move these to GitHub issues. 5 | This was a valiant effort at avoiding vendor lock-in) 6 | 7 | Here's a quick summary of the stuff I'm aware of and working on. 8 | IDs start with `am` to avoid conflicts with IDs that might be generated by some other issue tracker, should I choose one. 9 | 10 | [cols="1,3,1,2"] 11 | |=== 12 | |id |description |status | details 13 | 14 | |am55 |improve htmlproofer |OPEN |<> 15 | |am59 |port `book/build.sh` to Windows |OPEN | 16 | |am61 |port `mario/ansible/provision.sh` to Windows |OPEN | 17 | |am62 |improve render of titled sidebars in macOS Books |OPEN |title box overlaps sidebar box in Books 3.1 with Gray and Night themes 18 | |am63 |add auto-hyphenation |OPEN |<> 19 | |am64 |add _Cross references_ to print PDF |OPEN |<> 20 | |am65 |fix sidebar deep links in EPUB |RESOLVED |see https://github.com/asciidoctor/asciidoctor-epub3/pull/480[this patch] 21 | |am66 |improve Kindle compatibility |OPEN |<> 22 | |am67 |improve book build: robust, secure |OPEN |<>, also see am59 23 | |=== 24 | 25 | [#am55_detail] 26 | == ℹ️ am55: improve htmlproofer 27 | 28 | `book/.internal-build.sh` runs `htmlproofer`, which returns some spurious failures. 29 | Currently I ignore these failures with an `|| true` statement. 30 | It would be better to sqelch or fix them. 31 | This is possible by instrumenting links in the text or adjusting the way htmlproofer is configured and run. 32 | 33 | Some recent output: 34 | 35 | .... 36 | Running 3 checks (Images, Links, Scripts) in steadfast.html on *.html files ... 37 | 38 | 39 | Checking 173 external links 40 | Checking 94 internal links 41 | Checking internal link hashes in 1 file 42 | Ran on 1 file! 43 | 44 | 45 | For the Links check, the following failures were found: 46 | 47 | * At steadfast.html:6581: 48 | 49 | http://catb.org/jargon/html/G/Good-Thing.html is not an HTTPS link 50 | 51 | For the Links > External check, the following failures were found: 52 | 53 | * At steadfast.html:5354: 54 | 55 | External link https://github.com/wallabag/docker#upgrading failed: https://github.com/wallabag/docker exists, but the hash 'upgrading' does not (status code 200) 56 | 57 | * At steadfast.html:5713: 58 | 59 | External link https://matrix.to/#/#selfhosted:matrix.org failed: https://matrix.to/ exists, but the hash '/#selfhosted:matrix.org' does not (status code 200) 60 | 61 | * At steadfast.html:5988: 62 | 63 | External link https://github.com/strukturag/nextcloud-spreed-signaling#running-with-docker failed: https://github.com/strukturag/nextcloud-spreed-signaling exists, but the hash 'running-with-docker' does not (status code 200) 64 | 65 | 66 | HTML-Proofer found 4 failures! 67 | .... 68 | 69 | [#am63_detail] 70 | == ℹ️ am63: add auto-hyphenation 71 | 72 | I want full justification in the printed book, and I want words reasonably hyphenated. 73 | Asciidoctor-PDF can do this! 74 | Just add the `text-hyphen` gem and enable it in the theme. 75 | https://github.com/meonkeys/print-this/commit/91e31471fdb848c2ff8ab8f2fc31c5adcfa0c2b8[Here's an example]. 76 | 77 | Unfortunately for me, [.line-through]#URLs and# inline code spans are also hyphenated. 78 | 79 | See https://asciidoctor.zulipchat.com/#narrow/stream/288690-users.2Fasciidoctor-pdf/topic/.E2.9C.94.20In-line.20hyphen.20overrides.20for.20.22Special.22.20text.3F[this chat thread], https://github.com/asciidoctor/asciidoctor-pdf/issues/1805[this closed issue], and https://github.com/asciidoctor/asciidoctor-pdf/pull/2513[this patch] (merged on May 26, 2024). 80 | 81 | [#am64_detail] 82 | == ℹ️ am64: add _Cross references_ to print PDF 83 | 84 | Ideally I'd like a full set of lists of cross-referenced blocks of significant/titled content like figures, tables, sidebars, and listings (code snippets) in the _Cross references_ chapter. 85 | This currently relies on the https://rubygems.org/gems/asciidoctor-lists[asciidoctor-lists] extension. 86 | 87 | The print PDF omits the entire _Cross references_ chapter because page numbers are not rendered. 88 | See: https://github.com/Alwinator/asciidoctor-lists/issues/14 89 | 90 | Links in EPUB output break for anything without an explicit ID--something about the UUIDs auto-generated by asciidoctor-lists confuses the EPUB3 backend. 91 | Otherwise I'd only need explicit IDs for special cases. 92 | See: https://github.com/Alwinator/asciidoctor-lists/issues/25 93 | 94 | [#am66_detail] 95 | == ℹ️ am66: improve Kindle compatibility 96 | 97 | The EPUB loads on Kindle devices, but there are a few issues: 98 | 99 | * cross-references to sidebars are dead links (not clickable / touchable) 100 | * emoji don't appear 101 | * definition list terms (e.g. Glossary) are italic instead of bold 102 | * figure captions aren't centered 103 | 104 | I don't use Kindle apps or devices; I've only seen these via the Kindle Previewer. 105 | 106 | Perhaps related, KDP says a KPF created from Kindle Previewer https://github.com/asciidoctor/asciidoctor-epub3/issues/481[contains only images, is not reflowable, and fixed format is required]. 107 | 108 | https://asciidoctor.zulipchat.com/#narrow/stream/346540-users.2Fasciidoctor-epub3/[Discussion about same]. 109 | 110 | See "EPUB" → "Kindle" in `Readme.adoc` for all book format quality rankings. 111 | 112 | [#am67_detail] 113 | == ℹ️ am67: more resilient, secure book build 114 | 115 | `sudo book/build.sh` fails on Linux at `addgroup` failure - The group `root` already exists. 116 | This is documented in `book/Readme.md` but is still annoying and unnecessary. 117 | 118 | Even as non-root, building the books appears to fail on Ubuntu 22.04 LTS unless the `docker-buildx` package is installed. 119 | 120 | The main thing making `book/build.sh` brittle is that it's trying to be more secure by matching the non-root host user with an in-container non-root user. 121 | Does this appreciably help security? 122 | I don't know. 123 | If it doesn't, we should just collapse `book/.Dockerfile` and `book/.Dockerfile.insecure`, use `root`, and simplify `book/build.sh`. 124 | 125 | We should also explore/recommend https://docs.docker.com/engine/security/rootless/[rootless Docker] and https://docs.docker.com/engine/security/userns-remap/[user namespaces] when we seek to improve `book/build.sh`. 126 | -------------------------------------------------------------------------------- /mario/Readme.md: -------------------------------------------------------------------------------- 1 | # mario system provisioner 2 | 3 | Your software companion for the book _Steadfast Self-Hosting: Rapid-Rise Personal Cloud_. 4 | 5 | ## Prerequisites 6 | 7 | * [Bash](https://www.gnu.org/software/bash/) version 5 or later 8 | * [Ansible](https://www.ansible.com/) core at least the `minimumAnsibleCoreVersion` value in `ansible/provision.sh` 9 | 10 | ## Optional (but helpful) dependencies 11 | 12 | * decent/recent versions of GNU `grep`, `sed`, `sort`, and `head` 13 | 14 | ## Quick start 15 | 16 | 1. `cd ansible` 17 | 1. `./provision.sh` 18 | 1. Follow prompts. 19 | 20 | Check out the mario chapter in the book for further instructions. 21 | 22 | ## Copyright and license 23 | 24 | Copyright ©2023-2025 [Adam Monsen](https://adammonsen.com) 25 | 26 | [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See `COPYING`. 27 | 28 | ## Fix for `six.moves` module error on 22.04 29 | 30 | If your admin computer is Ubuntu 22.04 LTS you may see `ModuleNotFoundError: No module named 'ansible.module_utils.six.moves'`. The root cause may be [this issue](https://github.com/ansible/ansible/issues/81946). If so, the fix is to add the [ansible PPA](https://launchpad.net/~ansible/+archive/ubuntu/ansible) and install the `ansible-core` package. 31 | 32 | ## Fix for missing `ufw` on 22.04 33 | 34 | If your admin computer is Ubuntu 22.04 LTS and you get an error that the `ufw` module is missing, make sure the `ansible` package is installed. 35 | 36 | ## Fix for US/Pacific timezone error 37 | 38 | If `timedatectl set-timezone` fails during `provision.sh`, you may be using an invalid timezone. I've specifically seen `US/Pacific` fail. If that's your timezone, use `America/Los_Angeles` instead. I believe these timezone names recently. 39 | 40 | ## Missing advanced Ansible version checks on macOS 41 | 42 | Help wanted improving `provision.sh` for macOS and Windows users. 43 | -------------------------------------------------------------------------------- /mario/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = hosts.yml 3 | 4 | [ssh_connection] 5 | pipelining = True 6 | ssh_args = -o ControlMaster=auto -o ControlPersist=18000 -o PreferredAuthentications=publickey 7 | control_path = ~/.ssh/%%C 8 | -------------------------------------------------------------------------------- /mario/ansible/hosts.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | mario_server: 4 | ansible_python_interpreter: /usr/bin/python3 5 | -------------------------------------------------------------------------------- /mario/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # provision all hosts in hosts.yml 3 | - hosts: all 4 | # Huge speedup: skip gathering info about hardware, network interfaces, etc. 5 | # See https://docs.ansible.com/ansible/glossary.html#term-gather-facts-boolean . 6 | gather_facts: no 7 | # become root (with sudo) 8 | become: yes 9 | # Ansible runs are fastest with fewer enabled roles. 10 | roles: 11 | - base 12 | - docker 13 | - services 14 | -------------------------------------------------------------------------------- /mario/ansible/provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # provision.sh - mario system provisioner 4 | # Copyright (C) 2023-2025 Adam Monsen 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | set -o errexit 20 | set -o nounset 21 | set -o pipefail 22 | 23 | minimumAnsibleCoreVersion="2.16.1" 24 | 25 | # So we can refer back to the folder where this script lives. 26 | DIR="$( builtin cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 27 | 28 | # Ensure the Ansible program we need is available. 29 | aprun="$(which ansible-playbook)" || true 30 | if ! [[ -x "$aprun" ]] 31 | then 32 | echo "ERROR: Ansible not installed or not found in path." 33 | exit 1 34 | fi 35 | 36 | if ! ansible --version | grep -q core 37 | then 38 | echo "ERROR: Ansible core $minimumAnsibleCoreVersion or later required, but was not found." 39 | exit 1 40 | fi 41 | 42 | # Only perform advanced Ansible version checks on GNU/Linux because it's more likely to have compatible versions of grep, sed, sort, etc. 43 | # I think the only real compatibility issue right here is "sort -V". 44 | # Alternatives to this verlte are available at https://stackoverflow.com/questions/4023830/how-to-compare-two-strings-in-dot-separated-version-format-in-bash 45 | # Or maybe a better solution would be to enforce GNU sort/coreutils as a dependency? 46 | # homebrew provides gsort on macOS when one installs coreutils. 47 | if [[ "$OSTYPE" =~ linux-gnu ]]; then 48 | # Ensure minimum viable Ansible core version. 49 | # Example related issue: https://github.com/ansible/ansible/issues/81946 50 | thisAnsibleCoreVersion="$(ansible --version | grep core | sed -e 's/^.\+\([0-9]\+\.[0-9]\+\.[0-9]\+\).\+$/\1/')" 51 | # verlte is from kanaka's answer to https://stackoverflow.com/questions/4023830/how-to-compare-two-strings-in-dot-separated-version-format-in-bash 52 | function verlte() { 53 | [[ "$1" = "$(echo -e "$1\n$2" | sort -V | head -n1)" ]] 54 | } 55 | if ! [[ "$thisAnsibleCoreVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] 56 | then 57 | echo "ERROR: Ansible core $minimumAnsibleCoreVersion or later required. Unable to determine version." 58 | exit 1 59 | fi 60 | if ! verlte "$minimumAnsibleCoreVersion" "$thisAnsibleCoreVersion" 61 | then 62 | echo "ERROR: Ansible core $minimumAnsibleCoreVersion or later required. Version is too old." 63 | exit 1 64 | fi 65 | fi 66 | 67 | # Create customizable config file if it doesn't exist. 68 | myConfig="$DIR/config" 69 | if ! [[ -r $myConfig ]] 70 | then 71 | echo "You don't have a config file. I'll create one for you now." 72 | echo 73 | echo "Please edit '$myConfig' and re-run this script." 74 | cp "$DIR/template/config" "$myConfig" 75 | chmod 600 "$myConfig" 76 | exit 1 77 | fi 78 | 79 | # Test connection to server Ansible will use. 80 | if ! ssh -o BatchMode=yes mario_server true 81 | then 82 | echo "Error: unable to SSH to the server in batch mode." 83 | echo 84 | echo "Please fix your SSH client config and try again." 85 | exit 1 86 | fi 87 | 88 | # shellcheck disable=SC1090 89 | source "$myConfig" 90 | 91 | # Reorient ourselves, just in case we're being run from another folder. 92 | builtin cd "$DIR" 93 | 94 | # sudo may be used to gain root privileges without a password after the first run. 95 | if [[ -r "$DIR/.first-run-complete" ]] 96 | then 97 | $aprun playbook.yml 98 | else 99 | $aprun playbook.yml --ask-become-pass 100 | touch "$DIR/.first-run-complete" 101 | fi 102 | -------------------------------------------------------------------------------- /mario/ansible/roles/base/files/enable-passwordless-sudo: -------------------------------------------------------------------------------- 1 | %sudo ALL=(ALL:ALL) NOPASSWD:ALL 2 | -------------------------------------------------------------------------------- /mario/ansible/roles/base/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: timedatectl set-timezone 3 | command: "timedatectl set-timezone {{ lookup('env', 'TZ') }}" 4 | -------------------------------------------------------------------------------- /mario/ansible/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure apt cache 3 | apt: 4 | cache_valid_time: 86400 5 | update_cache: yes 6 | 7 | - name: Install packages 8 | apt: 9 | name: 10 | - htop 11 | - ufw 12 | - zfsutils-linux 13 | - zfs-auto-snapshot 14 | state: present 15 | 16 | - name: Set timezone 17 | lineinfile: 18 | path: /etc/timezone 19 | regexp: '.*' 20 | line: "{{ lookup('env', 'TZ') }}" 21 | notify: 22 | - timedatectl set-timezone 23 | 24 | - name: Enable UFW 25 | ufw: 26 | state: enabled 27 | 28 | - name: Disable logging 29 | ufw: 30 | logging: 'off' 31 | 32 | - name: Allow SSH 33 | ufw: 34 | rule: allow 35 | name: OpenSSH 36 | 37 | # FIXME - don't need this - Docker opens it 38 | - name: Allow insecure HTTP traffic 39 | ufw: 40 | rule: allow 41 | port: http 42 | 43 | # FIXME - don't need this - Docker opens it 44 | # HTTP/3 requires TCP and UDP. This appears to allow both. 45 | - name: Allow secure HTTP traffic 46 | ufw: 47 | rule: allow 48 | port: https 49 | 50 | - name: Allow passwordless sudo 51 | copy: 52 | src: enable-passwordless-sudo 53 | dest: /etc/sudoers.d/ 54 | owner: root 55 | group: root 56 | mode: 0400 57 | 58 | # for HTTP/3 59 | # see https://github.com/traefik/traefik/issues/9272 60 | # and https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes 61 | - name: increase UDP read buffer limits 62 | sysctl: 63 | name: net.core.rmem_max 64 | value: '26214400' 65 | 66 | # for HTTP/3 67 | # see https://github.com/traefik/traefik/issues/9272 68 | # and https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes 69 | - name: increase UDP write buffer limits 70 | sysctl: 71 | name: net.core.wmem_max 72 | value: '26214400' 73 | -------------------------------------------------------------------------------- /mario/ansible/roles/docker/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart docker 3 | service: 4 | name: docker.service 5 | state: restarted 6 | -------------------------------------------------------------------------------- /mario/ansible/roles/docker/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install packages 3 | apt: 4 | name: 5 | - docker.io 6 | # kept only for /usr/share/bash-completion/completions/docker-compose 7 | - docker-compose 8 | - docker-compose-v2 9 | state: present 10 | 11 | - name: Enable service 12 | service: 13 | name: docker.service 14 | state: started 15 | enabled: yes 16 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/files/dc-bash-completion: -------------------------------------------------------------------------------- 1 | # vim: ft=sh 2 | 3 | # Bash completion for `dc` (Docker Compose) helper script. 4 | 5 | # Ugly hacks here. 💩 6 | # I think it makes sense to re-use completions for `docker compose` (v2), but I don't know how to do it gracefully. 7 | # This only sorta works, and it requires the v1 (Python) `docker-compose` package. 8 | # If we improve this past needing `docker-compose`, please also remove the package from `mario/ansible/roles/docker/tasks/main.yml`. 9 | 10 | if ! [[ $(type -t _docker_compose) == function ]]; then 11 | source /usr/share/bash-completion/completions/docker-compose 12 | fi 13 | 14 | function _dc_comp() { 15 | local cur="${COMP_WORDS[COMP_CWORD]}" 16 | if [[ "$COMP_CWORD" -eq 1 ]]; then 17 | apps="$(sudo ls /root/ops/)" 18 | COMPREPLY=($(compgen -W "$apps" -- "$cur")) 19 | fi 20 | if [[ "$COMP_CWORD" -gt 1 ]]; then 21 | _command_offset "$COMP_CWORD" 22 | _docker_compose 23 | fi 24 | } 25 | 26 | complete -F _dc_comp dc 27 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/files/dc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | service=$1 8 | shift 9 | 10 | set -o xtrace 11 | sudo docker compose --file "/root/ops/$service/compose.yml" "$@" 12 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/files/php-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | set -o xtrace 7 | 8 | # Ansible discards stdout. Write to stderr so Ansible outputs something useful when this script exits nonzero (e.g. why PHP linting failed). 9 | docker run --rm --interactive php:8.2-apache-bookworm php -l < "$1" | tr '\n' '|' > /dev/stderr 10 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Ansible will automatically create parent directories. When we we mention each 3 | # parent explicitly it is to ensure owner and group are consistent. 4 | 5 | - name: Create root-owned service directories 6 | file: 7 | path: "{{ item }}" 8 | state: directory 9 | with_items: 10 | - /data 11 | - /data/nextcloud 12 | - /data/nextcloud/db 13 | - /data/nextcloud/root 14 | - /data/traefik 15 | - /data/traefik/etc 16 | - /data/wallabag 17 | - /root/ops 18 | - /root/ops/jellyfin 19 | - /root/ops/nextcloud 20 | - /root/ops/scratch 21 | - /root/ops/scratch/custom 22 | - /root/ops/traefik 23 | - /root/ops/wallabag 24 | - /root/ops/watchtower 25 | 26 | - name: Process and sync service configs 27 | template: 28 | src: "{{ item.src }}" 29 | dest: "{{ item.dest }}" 30 | loop: 31 | - { src: 'ops/jellyfin/compose.yml', dest: '/root/ops/jellyfin/compose.yml' } 32 | - { src: 'ops/nextcloud/compose.yml', dest: '/root/ops/nextcloud/compose.yml' } 33 | - { src: 'ops/scratch/compose.yml', dest: '/root/ops/scratch/compose.yml' } 34 | - { src: 'ops/scratch/custom/Dockerfile', dest: '/root/ops/scratch/custom/Dockerfile' } 35 | - { src: 'ops/scratch/custom/webpack.config.js', dest: '/root/ops/scratch/custom/webpack.config.js' } 36 | - { src: 'ops/traefik/compose.yml', dest: '/root/ops/traefik/compose.yml' } 37 | - { src: 'ops/wallabag/compose.yml', dest: '/root/ops/wallabag/compose.yml' } 38 | - { src: 'ops/watchtower/compose.yml', dest: '/root/ops/watchtower/compose.yml' } 39 | 40 | - name: Create jellyfin group 41 | group: 42 | name: "{{ jellyfin_group }}" 43 | gid: "{{ jellyfin_gid }}" 44 | 45 | - name: Create jellyfin user 46 | user: 47 | name: jellyfin 48 | home: /data/jellyfin/home 49 | password_lock: yes 50 | uid: "{{ jellyfin_uid }}" 51 | group: "{{ jellyfin_group }}" 52 | shell: /usr/sbin/nologin 53 | 54 | - name: Create jellyfin-owned directories 55 | file: 56 | path: "{{ item }}" 57 | owner: "{{ jellyfin_user }}" 58 | group: "{{ jellyfin_group }}" 59 | state: directory 60 | with_items: 61 | - /data/jellyfin/home 62 | - /data/jellyfin/config 63 | 64 | - name: Create Wallabag-owned directories 65 | file: 66 | path: "{{ item }}" 67 | owner: "{{ wallabag_user }}" 68 | group: "{{ wallabag_group }}" 69 | state: directory 70 | with_items: 71 | - /data/wallabag/images 72 | - /data/wallabag/main 73 | - /data/wallabag/main/db 74 | 75 | - name: Create Wallabag db 76 | copy: 77 | content: "" 78 | dest: /data/wallabag/main/db/wallabag.sqlite 79 | force: false 80 | owner: "{{ wallabag_user }}" 81 | group: "{{ wallabag_group }}" 82 | 83 | - name: Create Nextcloud-owned shared media directories 84 | file: 85 | path: "{{ item }}" 86 | owner: "{{ nextcloud_user }}" 87 | group: "{{ nextcloud_group }}" 88 | state: directory 89 | with_items: 90 | - /data/shared/media/video 91 | - /data/shared/media/music 92 | 93 | # for Jellyfin. 94 | # See https://jellyfin.org/docs/general/administration/troubleshooting.html#real-time-monitoring 95 | # The number is large yet, at the time of writing, not as large as recommended 96 | # in the jellyfin documentation. I'm just trying to be conservative. I also 97 | # tried to pick a sorta unique number in case I want it to stand out later 98 | # during a search. 99 | - name: increase inotify limits 100 | sysctl: 101 | name: fs.inotify.max_user_watches 102 | value: '233118' 103 | 104 | - name: Check if Nextcloud config file exists 105 | stat: 106 | path: /data/nextcloud/root/config/config.php 107 | register: nextcloud_config 108 | 109 | - name: Install PHP lint script 110 | copy: 111 | src: php-lint.sh 112 | dest: /usr/local/bin/php-lint.sh 113 | owner: root 114 | group: root 115 | mode: 0755 116 | 117 | # 'CONFIG = array' below is an arbitrary choice, it's just something that always 118 | # should exist in the Nextcloud config file 119 | # 120 | # `occ config:system:set` is probably a better way to do these config edits. 121 | # See docker-entrypoint.sh in https://github.com/nextcloud/docker/ 122 | 123 | - name: Add phone region to Nextcloud config 124 | lineinfile: 125 | path: /data/nextcloud/root/config/config.php 126 | regexp: 'default_phone_region' 127 | line: " 'default_phone_region' => '{{ lookup('env', 'DEFAULT_PHONE_REGION') }}'," 128 | insertafter: 'CONFIG = array' 129 | validate: /usr/local/bin/php-lint.sh %s 130 | when: nextcloud_config.stat.exists 131 | 132 | - name: Define arbitrary maintenance window 133 | lineinfile: 134 | path: /data/nextcloud/root/config/config.php 135 | regexp: 'maintenance_window_start' 136 | line: " 'maintenance_window_start' => 100," 137 | insertafter: 'CONFIG = array' 138 | validate: /usr/local/bin/php-lint.sh %s 139 | when: nextcloud_config.stat.exists 140 | 141 | - name: Install docker-compose shortcut script 142 | copy: 143 | src: dc.sh 144 | dest: /usr/local/sbin/dc 145 | owner: root 146 | group: root 147 | mode: 0755 148 | 149 | - name: Install dc Bash completion 150 | copy: 151 | src: dc-bash-completion 152 | dest: /usr/share/bash-completion/completions/dc 153 | owner: root 154 | group: root 155 | mode: 0644 156 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/jellyfin/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | app: 5 | image: linuxserver/jellyfin 6 | environment: 7 | PUID: "{{ jellyfin_uid }}" 8 | PGID: "{{ jellyfin_gid }}" 9 | TZ: "{{ lookup('env', 'TZ') }}" 10 | labels: 11 | - "traefik.enable=true" 12 | - "traefik.http.routers.jellyfin-https.entrypoints=websecure" 13 | - "traefik.http.routers.jellyfin-https.rule=Host(`jellyfin.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}`)" 14 | - "traefik.http.routers.jellyfin-https.middlewares=lan-only" 15 | - "traefik.http.routers.jellyfin-https.tls.certresolver=myresolver" 16 | volumes: 17 | - /data/shared/media/video:/data/video:ro 18 | - /data/shared/media/music:/data/music:ro 19 | - /data/jellyfin/config:/config 20 | networks: 21 | - traefik_default 22 | restart: unless-stopped 23 | networks: 24 | traefik_default: 25 | external: true 26 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/nextcloud/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | app: 5 | image: nextcloud:stable 6 | environment: 7 | MYSQL_HOST: db:3306 8 | MYSQL_DATABASE: nextcloud 9 | MYSQL_USER: nextcloud 10 | MYSQL_PASSWORD: nextcloud 11 | REDIS_HOST: valkey 12 | REDIS_HOST_PASSWORD: redispassword 13 | OVERWRITEPROTOCOL: https 14 | OVERWRITECLIURL: https://cloud.{{ lookup('env', 'MARIO_DOMAIN_NAME') }} 15 | TRUSTED_PROXIES: 172.18.0.0/16 16 | labels: 17 | - "traefik.enable=true" 18 | - "traefik.http.routers.nc-https.entrypoints=websecure" 19 | - "traefik.http.routers.nc-https.rule=Host(`cloud.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}`)" 20 | - "traefik.http.routers.nc-https.tls.certresolver=myresolver" 21 | - "traefik.http.routers.nc-https.middlewares=nc-head,nc-redir,lan-only" 22 | - "traefik.http.middlewares.nc-head.headers.stsSeconds=155520011" 23 | - "traefik.http.middlewares.nc-head.headers.stsIncludeSubdomains=true" 24 | - "traefik.http.middlewares.nc-head.headers.stsPreload=true" 25 | # These are required for groupware (calendar, contacts, tasks) 26 | - "traefik.http.middlewares.nc-redir.redirectregex.regex=/.well-known/(card|cal)dav" 27 | - "traefik.http.middlewares.nc-redir.redirectregex.replacement=/remote.php/dav/" 28 | volumes: 29 | - /data/nextcloud/root:/var/www/html:rw 30 | - /data/shared/media/video:/data/video:rw 31 | - /data/shared/media/music:/data/music:rw 32 | networks: 33 | - nextcloud 34 | - traefik_default 35 | restart: unless-stopped 36 | db: 37 | image: mariadb:10.6 38 | environment: 39 | MYSQL_ROOT_PASSWORD: mysqlnextcloudrootpassword 40 | MYSQL_DATABASE: nextcloud 41 | MYSQL_USER: nextcloud 42 | MYSQL_PASSWORD: nextcloud 43 | command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW --skip-innodb-read-only-compressed 44 | networks: 45 | - nextcloud 46 | restart: unless-stopped 47 | volumes: 48 | - /data/nextcloud/db:/var/lib/mysql 49 | valkey: 50 | image: valkey/valkey 51 | restart: unless-stopped 52 | command: valkey-server --requirepass redispassword 53 | networks: 54 | - nextcloud 55 | networks: 56 | nextcloud: 57 | name: nextcloud 58 | traefik_default: 59 | external: true 60 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/scratch/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | web: 5 | build: ./custom 6 | environment: 7 | TZ: "{{ lookup('env', 'TZ') }}" 8 | labels: 9 | - "traefik.enable=true" 10 | - "traefik.http.routers.scratch-https.entrypoints=websecure" 11 | - "traefik.http.routers.scratch-https.rule=Host(`scratch.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}`)" 12 | - "traefik.http.routers.scratch-https.tls.certresolver=myresolver" 13 | - "traefik.http.routers.scratch-https.middlewares=lan-only" 14 | # hey watchtower, don't try to auto-update this custom-built container 15 | - "com.centurylinklabs.watchtower.enable=false" 16 | networks: 17 | - traefik_default 18 | restart: unless-stopped 19 | networks: 20 | traefik_default: 21 | external: true 22 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/scratch/custom/Dockerfile: -------------------------------------------------------------------------------- 1 | # see https://github.com/meonkeys/docker-scratch 2 | 3 | FROM node:21-alpine3.18 4 | 5 | ENV SCRATCH_VERSION v3.6.18 6 | 7 | RUN apk add git 8 | 9 | WORKDIR /app 10 | 11 | RUN git clone --depth=1 --branch $SCRATCH_VERSION https://github.com/LLK/scratch-gui.git \ 12 | && cd scratch-gui \ 13 | && npm install 14 | 15 | EXPOSE 8601 16 | 17 | WORKDIR /app/scratch-gui 18 | 19 | # work around https://github.com/LLK/scratch-gui/issues/5682 20 | RUN mv webpack.config.js webpack.config.js.bak 21 | COPY webpack.config.js . 22 | 23 | CMD ["npm", "start"] 24 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/scratch/custom/webpack.config.js: -------------------------------------------------------------------------------- 1 | const defaultsDeep = require('lodash.defaultsdeep'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | // Plugins 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | const TerserPlugin = require('terser-webpack-plugin'); 9 | 10 | // PostCss 11 | const autoprefixer = require('autoprefixer'); 12 | const postcssVars = require('postcss-simple-vars'); 13 | const postcssImport = require('postcss-import'); 14 | 15 | const STATIC_PATH = process.env.STATIC_PATH || '/static'; 16 | 17 | const base = { 18 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 19 | devtool: 'cheap-module-source-map', 20 | devServer: { 21 | contentBase: path.resolve(__dirname, 'build'), 22 | host: '0.0.0.0', 23 | allowedHosts: ['scratch.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}',], 24 | port: process.env.PORT || 8601 25 | }, 26 | output: { 27 | library: 'GUI', 28 | filename: '[name].js', 29 | chunkFilename: 'chunks/[name].js' 30 | }, 31 | resolve: { 32 | symlinks: false 33 | }, 34 | module: { 35 | rules: [{ 36 | test: /\.jsx?$/, 37 | loader: 'babel-loader', 38 | include: [ 39 | path.resolve(__dirname, 'src'), 40 | /node_modules[\\/]scratch-[^\\/]+[\\/]src/, 41 | /node_modules[\\/]pify/, 42 | /node_modules[\\/]@vernier[\\/]godirect/ 43 | ], 44 | options: { 45 | // Explicitly disable babelrc so we don't catch various config 46 | // in much lower dependencies. 47 | babelrc: false, 48 | plugins: [ 49 | '@babel/plugin-syntax-dynamic-import', 50 | '@babel/plugin-transform-async-to-generator', 51 | '@babel/plugin-proposal-object-rest-spread', 52 | ['react-intl', { 53 | messagesDir: './translations/messages/' 54 | }]], 55 | presets: ['@babel/preset-env', '@babel/preset-react'] 56 | } 57 | }, 58 | { 59 | test: /\.css$/, 60 | use: [{ 61 | loader: 'style-loader' 62 | }, { 63 | loader: 'css-loader', 64 | options: { 65 | modules: { 66 | localIdentName: '[name]_[local]_[hash:base64:5]' 67 | }, 68 | importLoaders: 1, 69 | localsConvention: 'camelCase' 70 | } 71 | }, { 72 | loader: 'postcss-loader', 73 | options: { 74 | ident: 'postcss', 75 | plugins: function () { 76 | return [ 77 | postcssImport, 78 | postcssVars, 79 | autoprefixer 80 | ]; 81 | } 82 | } 83 | }] 84 | }, 85 | { 86 | test: /\.hex$/, 87 | use: [{ 88 | loader: 'url-loader', 89 | options: { 90 | limit: 16 * 1024 91 | } 92 | }] 93 | }] 94 | }, 95 | optimization: { 96 | minimizer: [ 97 | new TerserPlugin({ 98 | include: /\.min\.js$/ 99 | }) 100 | ] 101 | }, 102 | plugins: [ 103 | new CopyWebpackPlugin({ 104 | patterns: [ 105 | { 106 | from: 'node_modules/scratch-blocks/media', 107 | to: 'static/blocks-media/default' 108 | }, 109 | { 110 | from: 'node_modules/scratch-blocks/media', 111 | to: 'static/blocks-media/high-contrast' 112 | }, 113 | { 114 | from: 'src/lib/themes/high-contrast/blocks-media', 115 | to: 'static/blocks-media/high-contrast', 116 | force: true 117 | } 118 | ] 119 | }) 120 | ] 121 | }; 122 | 123 | if (!process.env.CI) { 124 | base.plugins.push(new webpack.ProgressPlugin()); 125 | } 126 | 127 | module.exports = [ 128 | // to run editor examples 129 | defaultsDeep({}, base, { 130 | entry: { 131 | 'lib.min': ['react', 'react-dom'], 132 | 'gui': './src/playground/index.jsx', 133 | 'blocksonly': './src/playground/blocks-only.jsx', 134 | 'compatibilitytesting': './src/playground/compatibility-testing.jsx', 135 | 'player': './src/playground/player.jsx' 136 | }, 137 | output: { 138 | path: path.resolve(__dirname, 'build'), 139 | filename: '[name].js' 140 | }, 141 | module: { 142 | rules: base.module.rules.concat([ 143 | { 144 | test: /\.(svg|png|wav|mp3|gif|jpg)$/, 145 | loader: 'url-loader', 146 | options: { 147 | limit: 2048, 148 | outputPath: 'static/assets/' 149 | } 150 | } 151 | ]) 152 | }, 153 | optimization: { 154 | splitChunks: { 155 | chunks: 'all', 156 | name: 'lib.min' 157 | }, 158 | runtimeChunk: { 159 | name: 'lib.min' 160 | } 161 | }, 162 | plugins: base.plugins.concat([ 163 | new webpack.DefinePlugin({ 164 | 'process.env.NODE_ENV': `"${process.env.NODE_ENV}"`, 165 | 'process.env.DEBUG': Boolean(process.env.DEBUG), 166 | 'process.env.GA_ID': `"${process.env.GA_ID || 'UA-000000-01'}"` 167 | }), 168 | new HtmlWebpackPlugin({ 169 | chunks: ['lib.min', 'gui'], 170 | template: 'src/playground/index.ejs', 171 | title: 'Scratch 3.0 GUI' 172 | }), 173 | new HtmlWebpackPlugin({ 174 | chunks: ['lib.min', 'blocksonly'], 175 | template: 'src/playground/index.ejs', 176 | filename: 'blocks-only.html', 177 | title: 'Scratch 3.0 GUI: Blocks Only Example' 178 | }), 179 | new HtmlWebpackPlugin({ 180 | chunks: ['lib.min', 'compatibilitytesting'], 181 | template: 'src/playground/index.ejs', 182 | filename: 'compatibility-testing.html', 183 | title: 'Scratch 3.0 GUI: Compatibility Testing' 184 | }), 185 | new HtmlWebpackPlugin({ 186 | chunks: ['lib.min', 'player'], 187 | template: 'src/playground/index.ejs', 188 | filename: 'player.html', 189 | title: 'Scratch 3.0 GUI: Player Example' 190 | }), 191 | new CopyWebpackPlugin({ 192 | patterns: [ 193 | { 194 | from: 'static', 195 | to: 'static' 196 | } 197 | ] 198 | }), 199 | new CopyWebpackPlugin({ 200 | patterns: [ 201 | { 202 | from: 'extensions/**', 203 | to: 'static', 204 | context: 'src/examples' 205 | } 206 | ] 207 | }), 208 | new CopyWebpackPlugin({ 209 | patterns: [ 210 | { 211 | from: 'extension-worker.{js,js.map}', 212 | context: 'node_modules/scratch-vm/dist/web', 213 | noErrorOnMissing: true 214 | } 215 | ] 216 | }) 217 | ]) 218 | }) 219 | ].concat( 220 | process.env.NODE_ENV === 'production' || process.env.BUILD_MODE === 'dist' ? ( 221 | // export as library 222 | defaultsDeep({}, base, { 223 | target: 'web', 224 | entry: { 225 | 'scratch-gui': './src/index.js' 226 | }, 227 | output: { 228 | libraryTarget: 'umd', 229 | path: path.resolve('dist'), 230 | publicPath: `${STATIC_PATH}/` 231 | }, 232 | externals: { 233 | 'react': 'react', 234 | 'react-dom': 'react-dom' 235 | }, 236 | module: { 237 | rules: base.module.rules.concat([ 238 | { 239 | test: /\.(svg|png|wav|mp3|gif|jpg)$/, 240 | loader: 'url-loader', 241 | options: { 242 | limit: 2048, 243 | outputPath: 'static/assets/', 244 | publicPath: `${STATIC_PATH}/assets/` 245 | } 246 | } 247 | ]) 248 | }, 249 | plugins: base.plugins.concat([ 250 | new CopyWebpackPlugin({ 251 | patterns: [ 252 | { 253 | from: 'extension-worker.{js,js.map}', 254 | context: 'node_modules/scratch-vm/dist/web', 255 | noErrorOnMissing: true 256 | } 257 | ] 258 | }), 259 | // Include library JSON files for scratch-desktop to use for downloading 260 | new CopyWebpackPlugin({ 261 | patterns: [ 262 | { 263 | from: 'src/lib/libraries/*.json', 264 | to: 'libraries', 265 | flatten: true 266 | } 267 | ] 268 | }) 269 | ]) 270 | })) : [] 271 | ); 272 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/traefik/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | reverse-proxy: 5 | image: traefik:v3.1 6 | environment: 7 | AWS_ACCESS_KEY_ID: "{{ lookup('env', 'DNS_API_KEY') }}" 8 | AWS_SECRET_ACCESS_KEY: "{{ lookup('env', 'DNS_API_SECRET') }}" 9 | DUCKDNS_TOKEN: "{{ lookup('env', 'DUCKDNS_TOKEN') }}" 10 | NAMECHEAP_API_USER: "{{ lookup('env', 'NAMECHEAP_API_USER') }}" 11 | NAMECHEAP_API_KEY: "{{ lookup('env', 'NAMECHEAP_API_KEY') }}" 12 | DO_AUTH_TOKEN: "{{ lookup('env', 'DO_AUTH_TOKEN') }}" 13 | command: 14 | - --api.insecure=true 15 | - --providers.docker=true 16 | - --providers.docker.network=traefik_default 17 | - --providers.docker.exposedbydefault=false 18 | - --accesslog=false 19 | - --log.level=INFO 20 | - --entrypoints.web.address=:80 21 | - --entrypoints.websecure.address=:443 22 | - --entrypoints.websecure.http3 23 | - --entrypoints.web.http.redirections.entrypoint.scheme=https 24 | - --entrypoints.web.http.redirections.entrypoint.to=websecure 25 | - --certificatesresolvers.myresolver.acme.dnschallenge=true 26 | - --certificatesresolvers.myresolver.acme.dnschallenge.provider={{ lookup('env', 'DNS_API_PROVIDER') }} 27 | - --certificatesresolvers.myresolver.acme.email={{ lookup('env', 'DNS_RESOLVER_EMAIL') }} 28 | - --certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme.json 29 | - --global.checknewversion=false 30 | - --global.sendanonymoususage=false 31 | ports: 32 | - "80:80" 33 | - "443:443/tcp" 34 | - "443:443/udp" 35 | volumes: 36 | # So that Traefik can listen to Docker events and auto-configure. 37 | # This has security implications. 38 | # See https://doc.traefik.io/traefik/providers/docker/#docker-api-access 39 | - /var/run/docker.sock:/var/run/docker.sock:ro 40 | # For storing Let's Encrypt cert 41 | - /data/traefik/etc:/etc/traefik:rw 42 | labels: 43 | - "traefik.enable=true" 44 | - "traefik.http.routers.dashboard-https.entrypoints=websecure" 45 | - "traefik.http.routers.dashboard-https.rule=Host(`traefik.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}`)" 46 | - "traefik.http.routers.dashboard-https.service=api@internal" 47 | - "traefik.http.routers.dashboard-https.tls.certresolver=myresolver" 48 | - "traefik.http.routers.dashboard-https.tls.domains[0].main={{ lookup('env', 'MARIO_DOMAIN_NAME') }}" 49 | - "traefik.http.routers.dashboard-https.tls.domains[0].sans=*.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}" 50 | - "traefik.http.routers.dashboard-https.middlewares=lan-only" 51 | - "traefik.http.middlewares.lan-only.ipallowlist.sourcerange={{ lookup('env', 'LAN_ONLY_ALLOWED') }}" 52 | restart: unless-stopped 53 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/wallabag/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | app: 5 | image: wallabag/wallabag 6 | # wallabag's root/entrypoint.sh drops privileges 7 | user: root:root 8 | depends_on: 9 | - valkey 10 | environment: 11 | # detailed documentation at https://github.com/wallabag/docker#environment-variables 12 | # remove MYSQL_ROOT_PASSWORD after first startup, or you'll see this in the logs: 13 | # "WARN: MySQL database is already configured. Remove the environment variable with root password." 14 | SYMFONY__ENV__REDIS_HOST: valkey 15 | SYMFONY__ENV__REDIS_PASSWORD: redispassword 16 | SYMFONY__ENV__TWOFACTOR_AUTH: 'true' 17 | SYMFONY__ENV__FOSUSER_REGISTRATION: 'true' 18 | SYMFONY__ENV__FOSUSER_CONFIRMATION: 'true' 19 | SYMFONY__ENV__DOMAIN_NAME: "https://wallabag.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}" 20 | SYMFONY__ENV__SERVER_NAME: "{{ lookup('env', 'MARIO_DOMAIN_NAME') }}" 21 | volumes: 22 | - /data/wallabag/main:/var/www/wallabag/data 23 | - /data/wallabag/images:/var/www/wallabag/web/assets/images 24 | labels: 25 | - "traefik.enable=true" 26 | - "traefik.http.routers.wallabag-https.entrypoints=websecure" 27 | - "traefik.http.routers.wallabag-https.rule=Host(`wallabag.{{ lookup('env', 'MARIO_DOMAIN_NAME') }}`)" 28 | - "traefik.http.routers.wallabag-https.tls.certresolver=myresolver" 29 | - "traefik.http.routers.wallabag-https.middlewares=lan-only" 30 | networks: 31 | - wallabag 32 | - traefik_default 33 | restart: unless-stopped 34 | valkey: 35 | image: valkey/valkey 36 | user: {{ wallabag_user }}:{{ wallabag_group }} 37 | restart: unless-stopped 38 | command: valkey-server --requirepass redispassword 39 | networks: 40 | - wallabag 41 | networks: 42 | wallabag: 43 | name: wallabag 44 | traefik_default: 45 | external: true 46 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/templates/ops/watchtower/compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # Watchtower fetches updated Docker images and automatically restarts containers 4 | # Documentation: https://containrrr.dev/watchtower/ 5 | services: 6 | sentry: 7 | image: containrrr/watchtower 8 | environment: 9 | TZ: "{{ lookup('env', 'TZ') }}" 10 | WATCHTOWER_DEBUG: "false" 11 | WATCHTOWER_SCHEDULE: "@daily" 12 | WATCHTOWER_RUN_ONCE: "false" 13 | WATCHTOWER_CLEANUP: "true" 14 | WATCHTOWER_MONITOR_ONLY: "false" 15 | restart: unless-stopped 16 | volumes: 17 | - /var/run/docker.sock:/var/run/docker.sock:rw 18 | -------------------------------------------------------------------------------- /mario/ansible/roles/services/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jellyfin_user: 'jellyfin' 3 | jellyfin_uid: '1179' 4 | jellyfin_group: 'jellyfin' 5 | jellyfin_gid: '1179' 6 | nextcloud_user: 'www-data' 7 | nextcloud_group: 'www-data' 8 | wallabag_user: 'nobody' 9 | wallabag_group: 'nogroup' 10 | -------------------------------------------------------------------------------- /mario/ansible/template/config: -------------------------------------------------------------------------------- 1 | # Specify your DNS provider here. 2 | # Must be ONE of namecheap, digitalocean, route53, or duckdns. 3 | # This and the corresponding provider-specific variables below are only used for directly manipulating DNS records to pass the DNS challenge for obtaining HTTPS encryption certs. 4 | export DNS_API_PROVIDER='' 5 | 6 | ### Namecheap API credentials ### 7 | # If you specified namecheap for DNS_API_PROVIDER above, enter real values in this section. Otherwise leave blank. 8 | export NAMECHEAP_API_USER='' 9 | export NAMECHEAP_API_KEY='' 10 | 11 | ### DigitalOcean API credentials ### 12 | # If you specified digitalocean for DNS_API_PROVIDER above, enter real values in this section. Otherwise leave blank. 13 | export DO_AUTH_TOKEN='' 14 | 15 | ### Duck DNS API credentials ### 16 | # If you specified duckdns for DNS_API_PROVIDER above, enter real values in this section. Otherwise leave blank. 17 | export DUCKDNS_TOKEN='' 18 | 19 | ### Route 53 API credentials ### 20 | # If you specified route53 for DNS_API_PROVIDER above, enter real values in this section. Otherwise leave blank. 21 | export R53_DNS_API_KEY='' 22 | export R53_DNS_API_SECRET='' 23 | 24 | # Notification email for certificate errors / expirations. 25 | # Use a value here matching what you use with your DNS API provider. 26 | # You may receive emails from Let's Encrypt at this address. 27 | export DNS_RESOLVER_EMAIL='fixme.dns.email@example.com' 28 | 29 | # A domain name you own, or at least one you control. 30 | # Services will be named using subdomains, e.g. wallabag.example.com, jellyfin.example.com 31 | # Note: domain names are not case-sensitive. 32 | # Warning: Changing this later may be difficult. 33 | export MARIO_DOMAIN_NAME='example.com' 34 | 35 | # Required to squelch a Nextcloud warning. 36 | # Pick your code from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements . 37 | # See https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/config_sample_php_parameters.html?highlight=phone#user-experience 38 | export DEFAULT_PHONE_REGION='US' 39 | 40 | # Your server's time zone. 41 | # Must correspond with an available timezone data file in /usr/share/zoneinfo on your server. 42 | # For example, if you are in Seattle, use 'America/Los_Angeles', short for /usr/share/zoneinfo/America/Los_Angeles 43 | # Note that 'US/Pacific' is no longer valid. 44 | # Leaving this is 'Etc/Zulu' is fine too. 45 | export TZ='Etc/Zulu' 46 | 47 | # Value to use for the `lan-only` middleware, which is enabled by default. 48 | # This is a range of IP addresses in CIDR notation. 49 | # See https://en.wikipedia.org/wiki/CIDR#CIDR_notation 50 | # The default value assumes LAN addresses match 192.168.1.* 51 | export LAN_ONLY_ALLOWED='192.168.1.0/24' 52 | 53 | # vim: ft=sh 54 | -------------------------------------------------------------------------------- /pelican/.python_history: -------------------------------------------------------------------------------- 1 | import locale 2 | locale.getlocale() 3 | -------------------------------------------------------------------------------- /pelican/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | ARG WORK_DIR 4 | ARG USER 5 | ARG UID 6 | ARG GROUP 7 | ARG GID 8 | 9 | RUN apt-get update \ 10 | && apt-get --assume-yes --no-install-recommends install \ 11 | default-jre \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | COPY requirements.txt ./ 15 | RUN pip install --no-cache-dir -r requirements.txt 16 | 17 | RUN addgroup --gid $GID $USER \ 18 | && useradd --uid $UID --gid $GID --shell /bin/bash --no-create-home $USER 19 | 20 | USER "$USER:$GROUP" 21 | 22 | ENV HOME=$WORK_DIR 23 | 24 | WORKDIR $WORK_DIR/website 25 | 26 | CMD [ "/bin/bash" ] 27 | -------------------------------------------------------------------------------- /pelican/Readme.md: -------------------------------------------------------------------------------- 1 | # Pelican test 2 | 3 | ## Usage 4 | 5 | `build.sh` builds and runs [Pelican](https://getpelican.com) in a container via Bash and `website/Makefile`. 6 | 7 | Example commands: 8 | 9 | ```bash 10 | # serve to http://localhost:8000 and watch files for changes 11 | ./build.sh make devserver-global 12 | 13 | # generate HTML to website/output 14 | ./build.sh make html 15 | 16 | # show all commands 17 | ./build.sh make help 18 | ``` 19 | 20 | ## See also 21 | 22 | * https://docs.getpelican.com 23 | * https://pypi.org/project/Jinja2/ 24 | * https://realpython.com/primer-on-jinja-templating/ 25 | * https://jinja.palletsprojects.com/en/3.1.x/api/#basics 26 | * https://stackoverflow.com/questions/69979846/how-to-run-a-venv-in-the-docker 27 | -------------------------------------------------------------------------------- /pelican/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | SECONDS=0 8 | echo "🏗️ start at $(date)" 9 | 10 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 11 | WORK_DIR=/usr/src/app 12 | IMAGE_NAME=shb-website-generator 13 | GID="$(id -g)" 14 | GROUP="$(id -gn)" 15 | 16 | lockfile=/var/lock/publish-shb-website.lock 17 | ( 18 | if ! flock --nonblock 9 19 | then 20 | echo "🗝️ failed to acquire lock - check if $0 is already running" 21 | exit 1 22 | fi 23 | 24 | images="$(docker images -q $IMAGE_NAME | wc -l)" 25 | 26 | if [[ $images -eq 0 ]]; then 27 | echo '🚢 build image' 28 | docker build \ 29 | --tag $IMAGE_NAME \ 30 | --build-arg WORK_DIR="$WORK_DIR" \ 31 | --build-arg USER="$USER" \ 32 | --build-arg UID="$UID" \ 33 | --build-arg GROUP="$GROUP" \ 34 | --build-arg GID="$GID" \ 35 | --quiet \ 36 | . \ 37 | > /dev/null 38 | fi 39 | 40 | echo '🚢 start container' 41 | docker run \ 42 | --rm \ 43 | --interactive \ 44 | --tty \ 45 | --user "$USER:$GROUP" \ 46 | --publish 8000:8000 \ 47 | --volume "$SCRIPT_DIR:$WORK_DIR" \ 48 | $IMAGE_NAME \ 49 | "$@" 50 | 51 | ) 9> "$lockfile" 52 | 53 | echo "🏗️ done (${SECONDS}s elapsed)" 54 | -------------------------------------------------------------------------------- /pelican/requirements.txt: -------------------------------------------------------------------------------- 1 | html5validator 2 | markdown 3 | pelican 4 | typogrify 5 | -------------------------------------------------------------------------------- /pelican/website/Makefile: -------------------------------------------------------------------------------- 1 | PY?= 2 | PELICAN?=pelican 3 | PELICANOPTS= 4 | 5 | BASEDIR=$(CURDIR) 6 | INPUTDIR=$(BASEDIR)/content 7 | OUTPUTDIR=$(BASEDIR)/output 8 | CONFFILE=$(BASEDIR)/pelicanconf.py 9 | PUBLISHCONF=$(BASEDIR)/publishconf.py 10 | 11 | DEBUG ?= 0 12 | ifeq ($(DEBUG), 1) 13 | PELICANOPTS += -D 14 | endif 15 | 16 | RELATIVE ?= 0 17 | ifeq ($(RELATIVE), 1) 18 | PELICANOPTS += --relative-urls 19 | endif 20 | 21 | SERVER ?= "0.0.0.0" 22 | 23 | PORT ?= 0 24 | ifneq ($(PORT), 0) 25 | PELICANOPTS += -p $(PORT) 26 | endif 27 | 28 | help: 29 | @echo 'Makefile for a pelican Web site ' 30 | @echo ' ' 31 | @echo 'Usage: ' 32 | @echo ' make html (re)generate the web site ' 33 | @echo ' make clean remove the generated files ' 34 | @echo ' make regenerate regenerate files upon modification ' 35 | @echo ' make publish generate using production settings ' 36 | @echo ' make serve [PORT=8000] serve site at http://localhost:8000' 37 | @echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 ' 38 | @echo ' make devserver [PORT=8000] serve and regenerate together ' 39 | @echo ' make devserver-global regenerate and serve on 0.0.0.0 ' 40 | @echo ' make validate validate the web site ' 41 | @echo ' ' 42 | @echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html ' 43 | @echo 'Set the RELATIVE variable to 1 to enable relative urls ' 44 | @echo ' ' 45 | 46 | html: 47 | "$(PELICAN)" "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) 48 | 49 | clean: 50 | [ ! -d "$(OUTPUTDIR)" ] || rm -rf "$(OUTPUTDIR)" 51 | 52 | regenerate: 53 | "$(PELICAN)" -r "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) 54 | 55 | serve: 56 | "$(PELICAN)" -l "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) 57 | 58 | serve-global: 59 | "$(PELICAN)" -l "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) -b $(SERVER) 60 | 61 | devserver: 62 | "$(PELICAN)" -lr "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) 63 | 64 | devserver-global: 65 | "$(PELICAN)" -lr "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(CONFFILE)" $(PELICANOPTS) -b 0.0.0.0 66 | 67 | publish: 68 | "$(PELICAN)" "$(INPUTDIR)" -o "$(OUTPUTDIR)" -s "$(PUBLISHCONF)" $(PELICANOPTS) 69 | 70 | validate: publish 71 | html5validator --root $(OUTPUTDIR) 72 | 73 | .PHONY: html help clean regenerate serve serve-global devserver devserver-global publish 74 | -------------------------------------------------------------------------------- /pelican/website/content/extra/.htaccess: -------------------------------------------------------------------------------- 1 | ErrorDocument 404 /404.html 2 | 3 | RewriteEngine on 4 | 5 | RewriteCond %{REQUEST_SCHEME} ^http$ [OR] 6 | RewriteCond %{HTTP_HOST} ^www\. [NC] 7 | RewriteRule ^ https://selfhostbook.com%{REQUEST_URI} [R=301] 8 | 9 | Redirect permanent "/src" "/code" 10 | Redirect permanent "/install" "/code" 11 | Redirect permanent "/translate" "/code" 12 | Redirect permanent "/chat" "/contact" 13 | Redirect permanent "/beta" "/news/2024/02/beta" 14 | Redirect permanent "/donate" "/buy" 15 | Redirect permanent "/theme/media/book-cover.jpg" "/theme/media/book-cover.png" 16 | Redirect permanent "/news/2024/05/ebook-release" "/news/2024/05/ebook" 17 | Redirect permanent "/videos" "/news/2024/07/resources" 18 | -------------------------------------------------------------------------------- /pelican/website/content/extra/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/android-chrome-192x192.png -------------------------------------------------------------------------------- /pelican/website/content/extra/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/android-chrome-512x512.png -------------------------------------------------------------------------------- /pelican/website/content/extra/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/apple-touch-icon.png -------------------------------------------------------------------------------- /pelican/website/content/extra/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/favicon-16x16.png -------------------------------------------------------------------------------- /pelican/website/content/extra/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/favicon-32x32.png -------------------------------------------------------------------------------- /pelican/website/content/extra/favicon-credit.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following graphics from Twitter Twemoji: 2 | 3 | - Graphics Title: 1f4d6.svg 4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji) 5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f4d6.svg 6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) 7 | -------------------------------------------------------------------------------- /pelican/website/content/extra/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/extra/favicon.ico -------------------------------------------------------------------------------- /pelican/website/content/extra/funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1.0.0", 3 | "entity": { 4 | "type": "individual", 5 | "role": "maintainer", 6 | "name": "Adam Monsen", 7 | "email": "haircut@gmail.com", 8 | "description": "Adam wrote the book Steadfast Self-Hosting.", 9 | "webpageUrl": { 10 | "url": "https://selfhostbook.com" 11 | } 12 | }, 13 | "projects": [{ 14 | "guid": "shb", 15 | "name": "Steadfast Self-Hosting", 16 | "description": "This book exists! People love it so far, but not everyone knows about it that should. And it needs to be maintained. Help keep it going!", 17 | "webpageUrl": { 18 | "url": "https://selfhostbook.com" 19 | }, 20 | "repositoryUrl": { 21 | "url": "https://selfhostbook.com/code/" 22 | }, 23 | "licenses": ["spdx:AGPL-3.0-or-later", "spdx:CC-BY-SA-4.0"], 24 | "tags": ["artificial-intelligence", "cloud", "computers", "data", "devops", "knowledge", "linux", "networking", "server", "storage"] 25 | }], 26 | "funding": { 27 | "channels": [{ 28 | "guid": "paypal", 29 | "type": "payment-provider", 30 | "address": "https://paypal.me/meonkeys" 31 | }, 32 | { 33 | "guid": "github-sponsors", 34 | "type": "payment-provider", 35 | "address": "https://github.com/sponsors/meonkeys/" 36 | }, 37 | { 38 | "guid": "peercoin", 39 | "type": "other", 40 | "address": "PRHQk8tcyvBmgkm9nfbLg7LJoZ48v8nciB" 41 | }, 42 | { 43 | "guid": "bitcoin", 44 | "type": "other", 45 | "address": "18XjpBwNccX9cvC2qiUgXLeeyY1jj3g5QW" 46 | }], 47 | "plans": [{ 48 | "guid": "maintain-shb", 49 | "status": "active", 50 | "name": "Pay author to maintain the book Steadfast Self-Hosting.", 51 | "description": "Funds will be used for book promotion, marketing, maintenance, and community development.", 52 | "amount": 10000, 53 | "currency": "USD", 54 | "frequency": "yearly", 55 | "channels": ["paypal", "github-sponsors", "peercoin", "bitcoin"] 56 | }, 57 | { 58 | "guid": "next-book", 59 | "status": "active", 60 | "name": "Advance payment for author to write another book.", 61 | "description": "Writing a book is expensive! Funds will be used for new book research, developement, writing, and publishing of a new non-fiction technical book.", 62 | "amount": 10000, 63 | "currency": "USD", 64 | "frequency": "one-time", 65 | "channels": ["paypal", "github-sponsors", "peercoin", "bitcoin"] 66 | }] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pelican/website/content/extra/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /pelican/website/content/extra/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /pelican/website/content/images/premium-bread.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/images/premium-bread.jpg -------------------------------------------------------------------------------- /pelican/website/content/images/premium-gopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/images/premium-gopher.jpg -------------------------------------------------------------------------------- /pelican/website/content/images/standard-bread.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/images/standard-bread.jpg -------------------------------------------------------------------------------- /pelican/website/content/images/standard-gopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/images/standard-gopher.jpg -------------------------------------------------------------------------------- /pelican/website/content/images/sustain204.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/content/images/sustain204.jpg -------------------------------------------------------------------------------- /pelican/website/content/news/beta.md: -------------------------------------------------------------------------------- 1 | title: 👀 Beta 2 | category: announcement 3 | date: 2024-02-14 4 | modified: 2024-05-16 5 | 6 | ### Early access to *Steadfast Self-Hosting* 7 | 8 | > The beta is now full and closed to new participants. 9 | > Thank you for your interest. 10 | > Original invite text preserved for posterity, below. 11 | 12 | You're invited! 13 | Be a part of a select group of individuals with the power to make a *great* book *excellent*. 14 | This is a unique opportunity to influence the direction of the book prior to printing, publication, and general availability. 15 | I'm calling this my "beta". 16 | As a beta reader you'll get: 17 | 18 | * early access to EPUB, HTML, and PDF versions of the book 19 | * all source code and access to source control 20 | * access to chat and email with the beta group and one-on-one with the author 21 | * deep gratitude from the author 22 | 23 | Beta readers are expected to: 24 | 25 | * read the book 26 | * read the source 27 | * run the source 28 | * provide feedback on all of the above 29 | * join the chat and mailing list 30 | 31 | You may seek an exception to these expectations; just ask. 32 | 33 | Please [contact me]({filename}/pages/contact.md) and let me know you want to be a beta reader. 34 | 35 | ### How it works 36 | 37 | Yep, I really want you to read my book for free! 38 | I'm doing this to perfect the pre-print book and raise awareness about it. 39 | 40 | Joining the beta is free (gratis) and anyone may ask to join. 41 | 42 | All members of the beta must follow our [rules]({filename}/pages/rules.md). 43 | 44 | The beta book is available in US English only. 45 | -------------------------------------------------------------------------------- /pelican/website/content/news/ebook.md: -------------------------------------------------------------------------------- 1 | title: 📖 Ebook 2 | category: announcement 3 | date: 2024-05-28 4 | modified: 2024-06-05 5 | 6 | The book is done and I'm excited to set it loose! 7 | 8 | I wanted to share paper copies first but printing this baby is challenging. 9 | It's probably me: I want it to look great, I'm self-publishing, and I'm doing all this for the first time. 10 | So yeah, it's taking a while longer than I expected. 11 | Shouldn't be much longer now though, probably about a month. 12 | 13 | While I'm working on printing, I published digital copies. 14 | [Please buy one today]({filename}/pages/buy.md). 15 | I don't want money to be a barrier here; if you can't afford to buy it you can get it for free. 16 | -------------------------------------------------------------------------------- /pelican/website/content/news/evergreen.md: -------------------------------------------------------------------------------- 1 | title: 🌲 Evergreen 2 | category: announcement 3 | date: 2025-04-15 4 | 5 | Ebook readers rejoice: we just got several recent typesetting improvements thanks to [Georger](https://github.com/georgeraraujo) including centering captions and conforming the table of contents across formats. 6 | 7 | I am grateful for Georger's patches. 8 | This is exactly why I used FOSS licensing. 9 | The book is an evergreen work, ever growing and evolving. 10 | 11 | In other news: 12 | 13 | * We're working on a German translation 🇩🇪. ETA September. 14 | * I made a conscious effort to use zero exclamation marks in this post but believe me, I am _stoked_. 15 | -------------------------------------------------------------------------------- /pelican/website/content/news/faq.md: -------------------------------------------------------------------------------- 1 | title: ⁉️ FAQ 2 | category: announcement 3 | date: 2024-05-28 4 | modified: 2024-09-20 5 | 6 | ### Great job! 7 | 8 | Thank you! 9 | 10 | ### How much is it? Where do I get it? 11 | 12 | See [💵 Buy]({filename}/pages/buy.md). 13 | 14 | ### I found a bug / typo 15 | 16 | You rock. 17 | Will you please [let me know about it]({filename}/pages/contact.md) and/or [send me a patch]({filename}/pages/code.md)? 18 | 19 | ### I added ten services to mario 20 | 21 | You rock. 22 | Will you share what you did [here](https://help.selfhostbook.com/t/ext)? 23 | 24 | ### Why can't I get the book on my bookseller/platform/device? 25 | 26 | Because I have issues. 27 | It's lot of work, some promote [DRM](https://en.wikipedia.org/wiki/Digital_rights_management), they track you, it costs money, stuff like that. 28 | 29 | ### When can I get one printed on actual paper? 30 | 31 | Now. 32 | See [💵 Buy]({filename}/pages/buy.md). 33 | 34 | ### Where's the party? 35 | 36 | We should do one! 37 | I was kinda waiting for printed copies to be ready; those would be really handy for in-person gatherings. 38 | 39 | ### Who's the superhero on the cover? 40 | 41 | They don't have a name yet! 42 | Maybe suggest one to me? 43 | They are described in an image alt attribute for `book-cover.png` like so: 44 | 45 | > Book cover art with title text, author name, and featuring our Steadfast hero in a cartoon fantasy rolling-hills landscape on a partly cloudy day, holding up their hand in triumph with the apps they successfully self-host magically extending into the air. Our hero has brown-skin, a half-shaved head of dark hair, purple cape, teal shoulder puffs, white sleeves, blue gloves, brown shirt, tan equipment strap and belt, red upper leggings, teal lower leggings, and light brown boots. 46 | 47 | Thanks to my daughter for colorfully bringing them to life. 48 | 49 | ### Yay, AGPL / ugh, AGPL 50 | 51 | Yep, I hear you! 52 | 53 | ### I bought the book, it is rad, what next? 54 | 55 | Buy one to share with a friend. 56 | Talk to your local school or public librarian to request a copy or buy one for them. 57 | 58 | ### I don't like the book 59 | 60 | Dang! 61 | I tried my best. 62 | Please [let me know]({filename}/pages/contact.md) why you didn't like it. 63 | Consider [sharing your thoughts publicly](https://help.selfhostbook.com/t/review). 64 | You can also [improve it or write a better one]({filename}/pages/code.md). 65 | 66 | ### I can translate this, and I want to! 67 | 68 | Great! 69 | See [💻 Code]({filename}/pages/code.md). 70 | 71 | ### Wait, it is FOSS, I can pay for it *and* it is free? Why are you leaving money on the table? How can you afford to do this? 72 | 73 | Those are excellent questions. 74 | Briefly: 75 | 76 | 1. Yes it is FOSS and you can pay what you're able & willing to to buy it, hopefully I'm not leaving money on the table 77 | 1. if I am leaving money on the table it's because I'm prioritizing distribution over profit, and 78 | 1. I kinda can't afford to do this. 😆 But it sure is fun! 79 | 80 | The detailed explanation is, well, detailed. 81 | If you're interested I could explain it over coffee or on a podcast or something. 82 | 83 | ### Where has this been mentioned? 84 | 85 | * [Lemmy](https://lemmy.world/post/16595344){:target="_blank"} 86 | * [Hacker News](https://news.ycombinator.com/item?id=40547357){:target="_blank"} 87 | * [LinkedIn](https://www.linkedin.com/posts/chuckwolber_steadfast-self-hosting-rapid-rise-personal-activity-7202360270671814656-G9Ad){:target="_blank"} 88 | * Mastodon: [me](https://fosstodon.org/@meonkeys/112534116393793870){:target="_blank"}, [Deb](https://freeradical.zone/@baconandcoconut/112537718845451626){:target="_blank"}, [jcrabapple](https://dmv.community/@jcrabapple/112543680946006130){:target="_blank"}, [Darnell Clayton](https://one.darnell.one/@darnell/112543705880256496){:target="_blank"} 89 | * [Reddit r/selfhosted](https://www.reddit.com/r/selfhosted/comments/1d5uzhz/i_wrote_a_book_about_selfhosting_for_a_small/){:target="_blank"} 90 | * [#selfhosted:matrix.org chat](https://matrix.to/#/!IwubhcevMjjvNFdtfN:matrix.org/$o_LHAevL00Ai_8Tnxo_uG7LsZsZLW_WGP6qaBB19drQ?via=matrix.org&via=envs.net&via=tchncs.de){:target="_blank"} 91 | * [Facebook](https://www.facebook.com/adam.monsen/posts/pfbid02fegi9Jb2JJWDrz7m9Bnd4cmADecS3Epc2hjDjZC27u99K7ufS6okEbw6Zj9dgfHWl){:target="_blank"} 92 | * [𝕏](https://x.com/meonkeys/status/1797140109734838538){:target="_blank"} 93 | -------------------------------------------------------------------------------- /pelican/website/content/news/fossy.md: -------------------------------------------------------------------------------- 1 | title: 📻 Sustain 2 | category: announcement 3 | tags: audio 4 | date: 2023-10-20 5 | 6 | [Richard Littauer interviewed me](https://podcast.sustainoss.org/204) at [FOSSY 2023](https://2023.fossy.us). 7 | 8 | Check it out! 9 | 10 | [![logo for Sustain podcast #204]({static}/images/sustain204.jpg){:style="max-width: 100%"}](https://podcast.sustainoss.org/204) 11 | -------------------------------------------------------------------------------- /pelican/website/content/news/fossy2023.md: -------------------------------------------------------------------------------- 1 | title: 🎙️ FOSSY 2 | category: announcement 3 | tags: event 4 | date: 2023-07-15 5 | modified: 2024-05-30 6 | 7 | I [talked about my book](https://adammonsen.com/post/2075/) at [FOSSY 2023](https://2023.fossy.us). 8 | 9 | I felt great about it and audience feedback was positive. 10 | -------------------------------------------------------------------------------- /pelican/website/content/news/global.md: -------------------------------------------------------------------------------- 1 | title: 🌏 Global 2 | category: announcement 3 | date: 2024-06-23 4 | 5 | The standard quality paperback is now in [global distribution](https://www.lulu.com/sell/retail-distribution). 6 | That means my printer will get it out to many places folks can actually pick it up, flip through it, and buy it. 7 | 8 | I learned the hard way that self-publishing is hard (ok, I was warned, I did it anyway). 9 | One challenge is distributing the actual printed book. 10 | You usually have to interface with various places with various printing requirements. 11 | Each collects various amounts of money to get your book in front of customers. 12 | 13 | So whew, Lulu is taking care of distribution and saving me a lot of time and hassle. 14 | Jury's out on the _quality_ of their distribution... so far all I see is a listing on Amazon with many typos introduced by their distribution system. 15 | Hopefully they'll fix this soon (I'm waiting to hear back on multiple support tickets). 16 | And I'll get less money than if folks bought the print book directly from Lulu, but hopefully more readers, and I had already prioritized readership. 17 | 18 | This whole print distribution pipeline is still a bit cumbersome on my end, and this matters for errata. 19 | It takes a while to update everything so new prints have the latest improvements. 20 | If you find a potential error in your copy, check the latest git commits since your version to see if it has been fixed. 21 | If it has not yet been fixed, please let me know. 22 | -------------------------------------------------------------------------------- /pelican/website/content/news/lp2024.md: -------------------------------------------------------------------------------- 1 | title: 🎙️ LibrePlanet 2 | category: announcement 3 | tags: event 4 | date: 2024-04-17 5 | 6 | I will be delivering my talk, "Steadfast Self-Hosting" on Sunday, May 5, 2024, 16:00--16:45 EDT (20:00 UTC), at the LibrePlanet 2024 conference, and I hope you’ll check it out! 7 | 8 | LibrePlanet is a conference about software freedom, happening on May 4 & 5, 2024. The event is hosted by the Free Software Foundation (FSF), and brings together software developers, law and policy experts, activists, students, and computer users to learn skills, celebrate free software accomplishments, and face upcoming challenges. Newcomers are always welcome, and LibrePlanet 2024 will feature programming for all ages and experience levels. 9 | 10 | Please register in advance at . 11 | -------------------------------------------------------------------------------- /pelican/website/content/news/paperback.md: -------------------------------------------------------------------------------- 1 | title: 📖 Paperback 2 | category: announcement 3 | date: 2024-06-05 4 | 5 | The paperback is ready! 6 | 7 | You may [buy one today]({filename}/pages/buy.md). 8 | 9 | ### Quality 10 | 11 | I currently use Lulu [print on demand](https://en.wikipedia.org/wiki/Print_on_demand). 12 | 13 | I took my time experimenting with Lulu. 14 | They offer many choice combinations as far as materials and quality. 15 | I formatted and tested a few and landed on two versions: A "standard" economical and good-looking print, as well as a "premium" print with better ink and less print variance. 16 | 17 | Here are some photos of actual prints. 18 | I saw most obvious differences in the color photos and illustrations. 19 | The content changed a bit between prints, so ignore differences like "Figure 7" vs. "Figure 8". 20 | Ignore object distortion, too; that's me trying to line up the shots by hand. 21 | 22 |
23 | Traefik gopher logo - standard quality 24 |
Traefik gopher logo - standard quality. From the MIT-licensed Traefik source code. Credit to Peka for the gopher logo, licensed CC-BY-3.0.
25 |
26 | 27 |
28 | Traefik gopher logo - premium quality 29 |
Traefik gopher logo - premium quality. From the MIT-licensed Traefik source code. Credit to Peka for the gopher logo, licensed CC-BY-3.0.
30 |
31 | 32 |
33 | Server in the shape of a loaf of bread - standard quality 34 |
Server in the shape of a loaf of bread - standard quality.
35 |
36 | 37 |
38 | Server in the shape of a loaf of bread - premium quality 39 |
Server in the shape of a loaf of bread - premium quality.
40 |
41 | 42 | These photos capture how the premium ink shines a bit in direct light. 43 | I like the effect, but YMMV. 44 | 45 | I can't comment on variance between prints/printers, that's up to Lulu. 46 | Please see the [Lulu printing quality FAQ](https://help.lulu.com/en/support/solutions/articles/64000255474-printing-the-basics). 47 | 48 | [Offset printing](https://en.wikipedia.org/wiki/Offset_printing) looks cool too but I'm not ready for that level of scale and commitment. 49 | -------------------------------------------------------------------------------- /pelican/website/content/news/resources.md: -------------------------------------------------------------------------------- 1 | title: 🗃️ Resources 2 | category: announcement 3 | date: 2024-07-01 4 | 5 | Here are some resources I recommend for self-hosters besides what you'll find in the _More Resources_ chapter in the book. 6 | 7 | ### [selfh.st](https://selfh.st) 8 | 9 | selfh.st is a great place to keep up on self-hostable software. 10 | Ethan Sholly ships an excellent email newsletter once a week. 11 | 12 | ### videos 13 | 14 | I'm making short videos to illustrate some concepts from the book. 15 | Here's the first: 16 | 17 | #### How to create a test VM for your self-hosting homelab 18 | 19 | 23 | 24 | This demo walks through creating a server VM for use with the book. 25 | You may watch it here or [elsewhere, with ads and other distractions](https://www.youtube.com/watch?v=Qi0uq_VCxiA). 26 | 27 | In the installer I use the default auto DHCP networking option to minimize customizations. 28 | I use my LAN router to assign an IP address that doesn't change instead of configuring a static IP address in the server OS (this technique is mentioned in the book under "OS Install"). 29 | 30 | Take care to download the correct Ubuntu server ISO image for your hardware. 31 | See [this issue](https://github.com/meonkeys/shb/issues/5) if your VM host is a mac and you run into poor performance. 32 | 33 | Please forgive my terrible video cropping and editing. I wanted to make sure and leave room for improvement. 😉 34 | -------------------------------------------------------------------------------- /pelican/website/content/news/reviews.md: -------------------------------------------------------------------------------- 1 | title: 🙏 Reviews 2 | category: announcement 3 | date: 2024-10-08 4 | 5 | To my readers: 6 | 7 | 12 | 13 | Thank you for getting my book. 14 | Please leave me a review. 15 | Good reviews will attract more potential readers. 16 | They'll really make a difference. 17 | If you have negative feedback, I want that too! 18 | Maybe just [send it directly to me first]({filename}/pages/contact.md). 19 | Give me a chance to earn your five-star review. 20 | My goal is to earn 10 reviews on [Amazon](https://www.amazon.com/Steadfast-Self-Hosting-Rapid-Rise-Personal-Cloud/dp/B0D7NLJHM5/), [Goodreads](https://www.goodreads.com/book/show/215055801-steadfast-self-hosting), [Lulu](https://www.lulu.com/shop/adam-monsen-and-lenny-wondra/steadfast-self-hosting/paperback/product-w4z5ewj.html), and [Gumroad](https://meonkeys.gumroad.com/l/shb) by the end of the week. 21 | I'd appreciate your help if you have the time. 22 | Thank you. 23 | Together we can help spread the word about self-hosting! 24 | 25 | Heart,
26 | -Adam 27 | 28 | --- 29 | 30 | I also emailed this to readers and [posted it on Gumroad](https://meonkeys.gumroad.com/p/reviews-808dfd45-8056-4002-9cb3-e8e3a5b5e0e9). 31 | 32 | Looks like the book is on its way to the [Seattle Public Library](https://seattle.bibliocommons.com/v2/record/S30C3996752), too! 33 | -------------------------------------------------------------------------------- /pelican/website/content/news/seagl2023.md: -------------------------------------------------------------------------------- 1 | title: 🎙️ SeaGL 2 | category: announcement 3 | tags: event 4 | date: 2023-11-04 5 | modified: 2024-05-30 6 | 7 | I [talked about my book](https://osem.seagl.org/conferences/seagl2023/program/proposals/934) at [SeaGL 2023](https://seagl.org/archive/2023). 8 | 9 | Similar to [FOSSY]({filename}/news/fossy2023.md), I felt great about it and audience feedback was positive. 10 | -------------------------------------------------------------------------------- /pelican/website/content/news/start.md: -------------------------------------------------------------------------------- 1 | title: 🚀 Start 2 | category: announcement 3 | date: 2023-02-15 4 | 5 | I'm going to write a book! 6 | Wish me luck. 7 | 8 | I got a server running (thanks Rob!) and it has been working well for a few years now. 9 | I'm experimenting with a bunch of self-hosted services. 10 | Mostly Nextcloud, Jellyfin, and Wallabag. 11 | 12 | Seems worth writing about. 13 | -------------------------------------------------------------------------------- /pelican/website/content/news/strong.md: -------------------------------------------------------------------------------- 1 | title: 🏋️ Strong 2 | category: announcement 3 | date: 2024-07-31 4 | 5 | 📬 If you want to stay in touch, [join my mailing list](https://meonkeys.gumroad.com). Imagine the sound of me cheering as you do. 6 | 7 | 🏗️ How's your self-hosting journey going? Is your server running? Is it awesome? [I'd like to hear all about it]({filename}/pages/contact.md)! 8 | 9 | ❓ Stuck? [Come to an in person workshop]({filename}/news/workshops.md). I'll be in Portland at FOSSY this Saturday. 10 | 11 | ⁉️ Still stuck? [Check out my new howto video]({filename}/news/resources.md#videos). 12 | 13 | 17 | 18 | 🚀 Got a new service of your own to share? [I shared a Pi-hole extension to mario](https://help.selfhostbook.com/t/ext) and I intend to share more. 19 | 20 | 🐞 Found a bug? [Send me a patch]({filename}/pages/code.md). Be a part of [possibly the first] self-hosting [book's commit] history! I've done a few releases already, and I'll incorporate all the latest fixes and improvements in the next release. 21 | 22 | 💞 Loved the book? Please give me a positive rating/review wherever you buy books (not necessarily where you got it). I need your help spreading the word! 23 | 24 | 🖋️ Want me to sign your book? I'd love to. Come to a workshop, like the one this weekend at FOSSY. 25 | 26 | 📖 [Paperbacks are available direct from the printer]({filename}/pages/buy.md). You can buy a copy from many other booksellers as well or request it at your local library. 27 | 28 | Heart,
29 | -Adam 30 | 31 | --- 32 | 33 | _Author's note: I also emailed this to readers and [posted it on Gumroad](https://meonkeys.gumroad.com/p/testing-5bdb4248-258a-445c-9986-4cddfe195cb2)._ 34 | -------------------------------------------------------------------------------- /pelican/website/content/news/toc.md: -------------------------------------------------------------------------------- 1 | title: 📑 Content 2 | category: announcement 3 | date: 2024-01-12 4 | modified: 2024-05-16 5 | 6 | ### Table of contents 7 | 8 | 1. **Introduction** 9 | : Welcome, copyright, prerequisites, style. 10 | 11 | 1. **Background** 12 | : Reasoning, inspiration, motivation, author info, deeper meaning. 13 | 14 | 1. **Your journey** 15 | : Why you should or should not self-host. 16 | 17 | 1. **Practical examples** 18 | : Examples validating self-hosting. 19 | 20 | 1. **Plan** 21 | : Budget, resources, schedule, transition, user support. 22 | 23 | 1. **System design** 24 | : Service stack, security, operating system. 25 | 26 | 1. **Implementation** 27 | : How to prepare your server for mario and perform maintenance. 28 | 29 | 1. **mario** 30 | : How to provision your server (prepare it for running services). 31 | 32 | 1. **Services** 33 | : How to run Nextcloud and a few other self-hosted services. 34 | 35 | 1. **What\'s next?** 36 | : Ideas for future learning, homelab projects, improvements. 37 | 38 | 1. **More resources** 39 | : Source code, contact info, support, alternatives to this book. 40 | 41 | 1. **Discussion topics** 42 | : Conversation starters for a group of readers. 43 | 44 | 1. **Exercises** 45 | : Applied usage ideas for a group of readers. 46 | -------------------------------------------------------------------------------- /pelican/website/content/news/workshops.md: -------------------------------------------------------------------------------- 1 | title: 🛠️ Workshops 2 | category: announcement 3 | date: 2024-07-01 4 | modified: 2024-10-12 5 | 6 | Come join a hands-on session to help each other get unblocked wherever we're at, from curious consideration about standing up a server to adding and improving smooth-running services. 7 | These workshops focus on fundamental concepts, tools, and techniques from the book, although having and having read the book is not required. 8 | 9 | | What | Where | When | 10 | |------|-------|------| 11 | | [FOSSY 2024](https://2024.fossy.us/schedule/presentation/219/) conference | Portland, OR, USA and online | Sat Aug 3, 2024 | 12 | | [BELUG](https://belug.us) meeting ([Presented by Dave Compton](https://homeservernotes.info/2024/08/12/walkthrough-steadfast-self-hosting-rapid-rise-personal-cloud.html)) | Bellevue, WA, USA and online | Tue Aug 13, 2024 at 6:30pm Pacific | 13 | | [BELUG](https://belug.us) meeting | Bellevue, WA, USA and online | Tue Sep 10, 2024 at 6:30pm Pacific | 14 | | [BLU](https://blu.org) meeting | online | Wed Sep 18, 2024 at 6:30pm Eastern | 15 | | [SeaGL 2024](https://seagl.org) conference | Seattle, WA, USA and online | Sat Nov 9, 2024 at 11:00am Pacific | 16 | 17 | If you want me to speak somewhere not listed above, connect me with an organizer for the event. 18 | 19 | Attendees: Please consider [buying your own physical or digital copy of the book]({filename}/pages/buy.md). 20 | This is the best way to participate and support the author. 21 | If you're unable to afford a copy, scholarships are available. 22 | 23 | As you read the book, you're encouraged to [join in the community chat and forum to get help and share your improvements with other readers]({filename}/pages/contact.md). 24 | -------------------------------------------------------------------------------- /pelican/website/content/pages/404.md: -------------------------------------------------------------------------------- 1 | title: Not Found 2 | status: hidden 3 | save_as: 404.html 4 | 5 | The requested item could not be located. 6 | -------------------------------------------------------------------------------- /pelican/website/content/pages/buy.md: -------------------------------------------------------------------------------- 1 | title: 💵 Buy 2 | modified: 2024-06-24 3 | menu_order_key: 1 4 | 5 | ### Ebook bundle 6 | 7 | Includes beautifully formatted EPUB, A4 PDF, and HTML versions of the book. 8 | 9 | [Supporter: USD 25.00](https://app.gumroad.com/checkout?product=ytngr&option=4ymYtQpoQGxnNCS30-PCTg==&quantity=1&price=2500){:target="_blank"} 10 | 11 | This Supporter price is automatically adjusted with [purchasing power parity](https://help.gumroad.com/article/327-purchasing-power-parity){:target="_blank"}. 12 | 13 | You may also claim a [full scholarship (USD 0.00)](https://app.gumroad.com/checkout?product=ytngr&option=6rbC0iSQuez1GyQ7Px_JSw==&quantity=1&price=0){:target="_blank"} or [name your price](https://meonkeys.gumroad.com/l/shb){:target="_blank"}. 14 | 15 | ### Paperback 16 | 17 | The excellent, 6" x 9" (US Trade) paperback full-color print with durable matte cover. 18 | 160 pages of delicious paper and ink (warning: do not eat). 19 | Perfect for your offline reading pleasure. 20 | 21 | [Standard: USD 25.00](https://www.lulu.com/shop/adam-monsen-and-lenny-wondra/steadfast-self-hosting/paperback/product-w4z5ewj.html){:target="_blank"} 22 | 23 | [Premium: USD 35.00](https://www.lulu.com/shop/adam-monsen-and-lenny-wondra/steadfast-self-hosting/paperback/product-m24rye5.html){:target="_blank"} 24 | 25 | If you're not sure which one to get, go standard. 26 | If you want the best there is, go premium. 27 | The premium version promises better ink for [higher contrast and deeper saturation]({filename}/news/paperback.md), and less variance between prints/printers. 28 | 29 | ### Hardcover 30 | 31 | I don't have plans to print a hardcover edition. 32 | 33 | ### Other book versions 34 | 35 | There are many other ways to read the book. 36 | See [this chart](https://github.com/meonkeys/shb#%EF%B8%8F-book-formats) for details about each version and [discuss them](https://help.selfhostbook.com/t/read) if you like. 37 | 38 | ### Donations 39 | 40 | If you'd like to send money directly to support me, thank you, that's awesome. 41 | It's helpful if you also [send me an email]({filename}/pages/contact.md) so I have some context. 42 | Include **Gift** in the email subject. 43 | Tell me how much you're sending and why. 44 | If the "why" is "to pay for the book", please instead buy an ebook or paperback using the links above (this helps my publisher deal with taxes). 45 | If you're sending cryptocurrency, please tell me the equivalent USD amount (this is really helpful for my taxes if/when I decide to convert it to fiat money). 46 | 47 | Here are ways to send me donations, in order of preference. 48 | Peercoin is especially interesting to me. 49 | I wish I could put it at the top, but it's too volatile and difficult to exchange for the flavor of fiat money I use to buy groceries. 50 | 51 | | method | address | 52 | |----------|------------------------------------| 53 | | Zelle | haircut@gmail.com | 54 | | PayPal | haircut@gmail.com | 55 | | Venmo | haircut@gmail.com or @meonkeys | 56 | | Bitcoin | 18XjpBwNccX9cvC2qiUgXLeeyY1jj3g5QW | 57 | | Peercoin | PRHQk8tcyvBmgkm9nfbLg7LJoZ48v8nciB | 58 | 59 | I sincerely appreciate these gifts. 60 | I'm not a charity so these donations are not tax-deductible. 61 | 62 | ### Hire me 63 | 64 | Somewhat related: I'm looking for work! 65 | I'd like to write another book but I can't afford to right now. 66 | I'll either need this book to sell well, get an advance for my next book, or take on other work. 67 | If I take on other work, ideally I'd be able to do work related to the book like speaking engagements, training, FOSS homelab/homeprod install/support/maintenance, FOSS desktop computing, home personal cloud appliances, nonprofit sustainable tech, or maybe running a community data center. 68 | 69 | If you have an another/unrelated job idea please [let me know]({filename}/pages/contact.md). 70 | I'm able to do a lot of things well. 71 | -------------------------------------------------------------------------------- /pelican/website/content/pages/code.md: -------------------------------------------------------------------------------- 1 | title: 💻 Code 2 | modified: 2024-05-28 3 | menu_order_key: 1 4 | 5 | ### Source code 6 | 7 | See 8 | 9 | ### Install mario 10 | 11 | mario is a free tool I built to help you set up and maintain a server as you read the book. 12 | See `mario/Readme.md` in the source code. 13 | 14 | ### Translate 15 | 16 | I would love to make this book available in lanauges besides English. 17 | Please [contact me]({filename}/pages/contact.md) about translating the book. 18 | 19 | The entire book is contained in `steadfast.asciidoc`, a plain text file in [Asciidoc](https://en.wikipedia.org/wiki/AsciiDoc) markup. 20 | I can help with the code, config, and deploy changes necessary to accommodate the translation. 21 | I'm thinking we might want translations in forked repositories, and it might also be nice to have them all in one place... I'm not sure. 22 | Suggestions welcome (especially if you've done this before). 23 | 24 | I want translators to be paid for their work. 25 | I think it'd be awesome if you translate and distribute the book, and we share the proceeds 50/50. 26 | Let me know your thoughts. 27 | 28 | ### Improve this website 29 | 30 | See `pelican/Readme.md` in the source code. 31 | 32 | ### Contributor guidelines 33 | 34 | See: 35 | 36 | 1. [Rules]({filename}/pages/rules.md). 37 | 1. "Patches welcome" in the top-level Readme in the source code. 38 | -------------------------------------------------------------------------------- /pelican/website/content/pages/contact.md: -------------------------------------------------------------------------------- 1 | title: 📫 Contact 2 | modified: 2024-07-31 3 | menu_order_key: 1 4 | 5 | Here's how to keep in touch. 6 | These methods/resources exist to supplement (not substitue for) reading the book. 7 | 8 | ### Subscribe to updates 9 | 10 | [Join my mailing list](https://meonkeys.gumroad.com). 11 | 12 | [Subscribe to news posted on this website](https://selfhostbook.com/feeds/all.atom.xml). 13 | 14 | Follow Adam on Mastodon. 15 | 16 | ### Email the author 17 | 18 | Feedback welcome! 19 | Email `info` at (this domain name). 20 | 21 | ### Chat with other readers and fans 22 | 23 | There's an encrypted invite-only Matrix group chat. 24 | 25 | First, please read [our rules]({filename}/pages/rules.md). 26 | If you agree to those, email me your Matrix username and I'll send you an invite. 27 | 28 | ### Discuss the book 29 | 30 | We have a forum for asynchronous discussions about the book and related topics. 31 | Come share your extensions and translations, get ideas on hardware, and work through book exercises together. 32 | 33 | See [the welcome post](https://help.selfhostbook.com/d/1-welcome) for more information. 34 | 35 | ### Contact the publisher 36 | 37 | See 38 | -------------------------------------------------------------------------------- /pelican/website/content/pages/home.md: -------------------------------------------------------------------------------- 1 | title: 📖 Home 2 | modified: 2024-08-01 3 | URL: 4 | save_as: index.html 5 | menu_order_key: 0 6 | 7 | This is the home for a most excellent book about self-hosting. 8 | 9 | ### Welcome! 10 | 11 | _Steadfast Self-Hosting_ is an authentic and inspiring self-hosting tutorial by seasoned tech entrepreneur and self-hosting expert Adam Monsen. 12 | Benefit from Adam's decades of applied effort distilled into this quick and exciting initiation to data sovereignty. 13 | 14 | Save money, time, and sanity 15 | : Learn an easy and reliable method to self-host services for your small group of beloved users. 16 | 17 | Batteries included 18 | : Corresponding source code is provided under a Free and Open Source software license. 19 | 20 | ### Tools in the book 21 | 22 | The book details why and how to use these tools to self-host: 23 | 24 | * bare metal physical server 25 | * Ubuntu operating system 26 | * Ansible provisioner 27 | * Traefik reverse proxy 28 | * Docker container manager 29 | * Docker Compose service manager 30 | 31 | It comes with mario, a program to help you set up your server and install a few services recommended by the author: Nextcloud, Jellyfin, Wallabag, Watchtower, and Scratch. 32 | 33 | ### Origin story 34 | 35 | I researched and wrote the book over several years. 36 | I've been working on related technologies professionally and personally for the last couple of decades. 37 | 38 | I wrote this book after having realized the power and potential of self-hosting. 39 | I used the method it describes to provide a robust data haven for myself and my family. 40 | I haven't found other books like it. 41 | 42 | I've given lectures on self-hosting and received feedback from attendees: they are already experienced at learning tech, they are seeking the motivation to self-host along with high-quality information on how to do it well. 43 | In response to this feedback I wrote a large Background chapter. 44 | In it, I approach self-hosting with a good deal of justification for why someone should self-host at all. 45 | I use anecdotes, figures, and tenants of relatable value-based living to provide sufficient motivation for getting through the learning and implementation stages of self-hosting. 46 | 47 | ### Audience 48 | 49 | *Steadfast Self-Hosting* primarily targets aspiring self-hosters looking for a quick and reliable method to get started. 50 | Some basic IT and networking skills are required. 51 | A reader should be familiar with Linux, command-line basics, and transferring files to and from a server. 52 | 53 | Experienced self-hosters will also benefit when they read, compare, and improve their existing homelabs. 54 | 55 | ### Author 56 | 57 | Adam is a kind and savvy [FOSS](https://en.wikipedia.org/wiki/Free_and_open-source_software) enthusiast. 58 | He's been in tech for over 20 years: building, producing, coding, debugging, architecting, leading, managing, debugging some more, lecturing, writing, administering and securing systems and processes, ensuring privacy and compliance; in markets of all maturities, sizes, and scales; startups to big enterprise. 59 | He's most proud of his family, growing [Mifos](https://mifos.org), founding [SeaGL](https://seagl.org), selling [C-SATS](https://csats.com), and writing a FOSS book about self-hosting FOSS. 60 | 61 | Adam is privileged and lucky to have given talks and workshops at a handful of conferences ([LFNW](https://lfnw.org/), [SeaGL](https://seagl.org), [LibrePlanet](https://libreplanet.org), [OSCON](https://en.wikipedia.org/wiki/O%27Reilly_Open_Source_Convention), [FOSSY](https://fossy.us/)) and various other engagements. 62 | There's a full list [here](https://adammonsen.com/talks/). 63 | 64 | You can follow Adam on Mastodon. 65 | 66 | ### Identifiers 67 | 68 | * Standard quality paperback 69 | * ISBN: `979-8-9908615-1-0` 70 | * Library of Congress Control Number: `2024911437` 71 | * commit: `b0eecd5a` 72 | * Premium quality paperback 73 | * ISBN: `979-8-9908615-3-4` 74 | * commit: `b0eecd5a` 75 | * EPUB 76 | * ISBN: `979-8-9908615-0-3` 77 | * commit: `d57d4275` 78 | * PDF 79 | * ISBN: `979-8-9908615-2-7` 80 | * commit: `d57d4275` 81 | * HTML 82 | * commit: `d57d4275` 83 | -------------------------------------------------------------------------------- /pelican/website/content/pages/rules.md: -------------------------------------------------------------------------------- 1 | title: ⚖️ Rules 2 | modified: 2024-05-24 3 | menu_order_key: 1 4 | 5 | These rules embody our values. 6 | They guide how we interact with each other, with a focus on psychological safety and support, so we may best: 7 | 8 | 1. Have nuanced discussions & stay on topic. 9 | 1. Hear from a diverse set of people. 10 | 1. Learn new things together. 11 | 12 | What you see here was created with care by a powerful, tiny team, not a big corporation. 13 | The list of goals above was [inspired by Julia Evans](https://fosstodon.org/@b0rk@jvns.ca/112128315421487330). 14 | 15 | ### Community rules 16 | 17 | When we communicate with each other, we promise to: 18 | 19 | * Be kind and inclusive. 20 | * Not use bots, AI, or other automations without transparency and consent. 21 | * Act in good faith. 22 | * Not spam, stay on-topic, and gracefully/calmly accept feedback on same. 23 | * Follow the [SeaGL code of conduct](https://seagl.org/code_of_conduct). 24 | 25 | !!! note "English only" 26 | Sorry, we have a lot of [i18n and L10n](https://en.wikipedia.org/wiki/Internationalization_and_localization) work to do. 27 | We currently only support written and spoken English. 28 | 29 | ### Terms of Service 30 | 31 | Participating in our community implies agreement to our terms. 32 | 33 | As with our community rules: be kind and act in good faith. 34 | 35 | Participation in this community is a privilege, neither a right nor guarantee. 36 | 37 | ### Privacy Policy 38 | 39 | We collect only those data you directly provide and only to better serve you. 40 | We will never sell these data. 41 | 42 | Examples: 43 | 44 | * email address 45 | * name or nickname 46 | 47 | This website does not attempt to profile you. 48 | This website does not track you with cookies. 49 | 50 | The server does log your IP address. 51 | Your IP address will not be associated with your other data above, it is only used for website traffic statistics. 52 | 53 | At any time you may contact [the publisher](https://sunrisedata.io) to request a copy of your data on file and/or request your data be deleted. 54 | We will grant you the right to be forgotten regardless of where you live. 55 | -------------------------------------------------------------------------------- /pelican/website/pelicanconf.py: -------------------------------------------------------------------------------- 1 | AUTHOR = 'Adam' 2 | SITENAME = 'Steadfast Self-Hosting' 3 | SITESUBTITLE = 'Rapid-Rise Personal Cloud' 4 | SITEDESCRIPTION = 'This book will free your data and empower your users.' 5 | SITEURL = "" 6 | 7 | PATH = "content" 8 | 9 | TIMEZONE = 'US/Pacific' 10 | 11 | # I don't intend to post too often. 12 | DEFAULT_DATE_FORMAT = '%B %Y' 13 | 14 | # Feed generation is usually not desired when developing 15 | FEED_ALL_ATOM = None 16 | CATEGORY_FEED_ATOM = None 17 | TRANSLATION_FEED_ATOM = None 18 | AUTHOR_FEED_ATOM = None 19 | AUTHOR_FEED_RSS = None 20 | 21 | MENUITEMS = [ 22 | ('🗞️ News', '/news/'), 23 | ] 24 | 25 | DEFAULT_PAGINATION = 10 26 | 27 | ARTICLE_URL = 'news/{date:%Y}/{date:%m}/{slug}/' 28 | ARTICLE_SAVE_AS = 'news/{date:%Y}/{date:%m}/{slug}/index.html' 29 | 30 | PAGE_URL = "{slug}/" 31 | PAGE_SAVE_AS = "{slug}/index.html" 32 | 33 | AUTHOR_URL = "author/{slug}/" 34 | AUTHOR_SAVE_AS = "author/{slug}/index.html" 35 | 36 | CATEGORY_URL = "category/{slug}/" 37 | CATEGORY_SAVE_AS = "category/{slug}/index.html" 38 | 39 | TAG_URL = "tag/{slug}/" 40 | TAG_SAVE_AS = "tag/{slug}/index.html" 41 | 42 | INDEX_URL = "news/" 43 | INDEX_SAVE_AS = "news/index.html" 44 | 45 | YEAR_ARCHIVE_SAVE_AS = 'news/{date:%Y}/index.html' 46 | YEAR_ARCHIVE_URL = 'news/{date:%Y}/' 47 | 48 | MONTH_ARCHIVE_SAVE_AS = 'news/{date:%Y}/{date:%m}/index.html' 49 | MONTH_ARCHIVE_URL = 'news/{date:%Y}/{date:%m}/' 50 | 51 | DISPLAY_PAGES_ON_MENU = True 52 | PAGE_ORDER_BY = 'menu_order_key' 53 | 54 | DISPLAY_CATEGORIES_ON_MENU = False 55 | 56 | THEME = "theme" 57 | 58 | IGNORE_FILES = ['.#*', '.*.swp', '.*.swx', '.*.un~', '*~'] 59 | 60 | STATIC_PATHS = [ 61 | 'extra/.htaccess', 62 | 'extra/android-chrome-192x192.png', 63 | 'extra/android-chrome-512x512.png', 64 | 'extra/apple-touch-icon.png', 65 | 'extra/favicon-16x16.png', 66 | 'extra/favicon-32x32.png', 67 | 'extra/favicon-credit.txt', 68 | 'extra/favicon.ico', 69 | 'extra/funding.json', 70 | 'extra/robots.txt', 71 | 'extra/site.webmanifest', 72 | ] 73 | 74 | EXTRA_PATH_METADATA = { 75 | 'extra/.htaccess': {'path': '.htaccess'}, 76 | 'extra/android-chrome-192x192.png': {'path': 'android-chrome-192x192.png'}, 77 | 'extra/android-chrome-512x512.png': {'path': 'android-chrome-512x512.png'}, 78 | 'extra/apple-touch-icon.png': {'path': 'apple-touch-icon.png'}, 79 | 'extra/favicon-16x16.png': {'path': 'favicon-16x16.png'}, 80 | 'extra/favicon-32x32.png': {'path': 'favicon-32x32.png'}, 81 | 'extra/favicon-credit.txt': {'path': 'favicon-credit.txt'}, 82 | 'extra/favicon.ico': {'path': 'favicon.ico'}, 83 | 'extra/funding.json': {'path': 'funding.json'}, 84 | 'extra/robots.txt': {'path': 'robots.txt'}, 85 | 'extra/site.webmanifest': {'path': 'site.webmanifest'}, 86 | } 87 | 88 | TYPOGRIFY = True 89 | 90 | MARKDOWN = { 91 | 'extension_configs': { 92 | 'markdown.extensions.codehilite': {'css_class': 'highlight'}, 93 | 'markdown.extensions.extra': {}, 94 | 'markdown.extensions.meta': {}, 95 | 'markdown.extensions.toc': {'permalink': True}, 96 | 'markdown.extensions.admonition': {}, 97 | }, 98 | 'output_format': 'html5', 99 | } 100 | -------------------------------------------------------------------------------- /pelican/website/publishconf.py: -------------------------------------------------------------------------------- 1 | # This file is only used if you use `make publish` or 2 | # explicitly specify it as your config file. 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.append(os.curdir) 8 | from pelicanconf import * 9 | 10 | # If your site is available via HTTPS, make sure SITEURL begins with https:// 11 | SITEURL = "https://selfhostbook.com" 12 | RELATIVE_URLS = False 13 | 14 | FEED_ALL_ATOM = "feeds/all.atom.xml" 15 | CATEGORY_FEED_ATOM = "feeds/{slug}.atom.xml" 16 | 17 | DELETE_OUTPUT_DIRECTORY = True 18 | 19 | # Docs recommend this, I don't understand why. 20 | FEED_DOMAIN = SITEURL 21 | -------------------------------------------------------------------------------- /pelican/website/theme/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | background: white; 4 | color: black; 5 | } 6 | 7 | td, th { 8 | border: 1px solid; 9 | padding: 5px; 10 | } 11 | 12 | table { 13 | border-collapse: collapse; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | } 19 | 20 | /* navigation -- different than article/page headers */ 21 | body > header { 22 | text-align: center; 23 | padding-bottom: 1em; 24 | } 25 | 26 | main { 27 | width: 660px; 28 | } 29 | 30 | dt { 31 | font-style: italic; 32 | } 33 | 34 | nav ul, .sidebar ul { 35 | margin: 0; 36 | padding: 0; 37 | } 38 | 39 | nav li, .sidebar li { 40 | list-style: none; 41 | display: inline-block; 42 | padding-right: 0.5em; 43 | } 44 | 45 | nav li:after { 46 | content: "·"; 47 | margin-left: 0.5em; 48 | } 49 | 50 | nav li:last-child:after { 51 | content: ""; 52 | } 53 | 54 | nav li .active, .sidebar a.active { 55 | text-decoration: none; 56 | border: 1px solid; 57 | border-radius: 3px; 58 | display: inline-block; 59 | padding: 5px; 60 | } 61 | 62 | nav li :not(.active), .sidebar a:not(.active) { 63 | text-decoration: none; 64 | border: 1px solid transparent; 65 | display: inline-block; 66 | padding: 5px; 67 | } 68 | 69 | .container { 70 | display: grid; 71 | grid-template-areas: "side main main"; 72 | grid-gap: 2em; 73 | margin: 0 auto; 74 | max-width: 960px; 75 | } 76 | 77 | .sidebar { 78 | font-size: 0.9em; 79 | grid-area: side; 80 | } 81 | 82 | #book-cover { 83 | width: 256px; 84 | } 85 | 86 | .date-published, .date-modified, .article-authors, .article-category, .article-tags { 87 | font-size: smaller; 88 | font-style: italic; 89 | } 90 | 91 | .article-separator { 92 | text-align: center; 93 | } 94 | 95 | .howto-video { 96 | width: 660px; 97 | } 98 | 99 | @media screen and (max-width: 1000px) { 100 | .container { 101 | display: block; 102 | } 103 | 104 | .sidebar { 105 | text-align: center; 106 | } 107 | 108 | main { 109 | width: auto; 110 | } 111 | 112 | .howto-video { 113 | width: 320px; 114 | } 115 | } 116 | 117 | @media (prefers-color-scheme: dark) { 118 | body { 119 | background: #14131c; 120 | color: white; 121 | } 122 | 123 | a { 124 | color: #ffd662; 125 | } 126 | 127 | a:visited { 128 | color: #d9b653; 129 | } 130 | } 131 | 132 | h3 a.headerlink, h4 a.headerlink { 133 | opacity: 0; 134 | transition: opacity 0.5s ease; 135 | } 136 | 137 | h3:hover a.headerlink, h4:hover a.headerlink { 138 | opacity: 1; 139 | } 140 | 141 | code { 142 | font-size: +1.3em; 143 | } 144 | 145 | .admonition { 146 | padding: 0 15px; 147 | margin-bottom: 20px; 148 | border: 1px solid transparent; 149 | border-radius: 4px; 150 | text-align: left; 151 | } 152 | 153 | .admonition-title { 154 | font-weight: bold; 155 | text-align: left; 156 | } 157 | 158 | .admonition.note, .admonition.info, .admonition.tips { 159 | color: #1d6fa5; 160 | background-color: #a0cfee; 161 | border-color: #3498db; 162 | } 163 | 164 | .admonition.warning, .admonition.warn, .admonition.important { 165 | color: #a85913; 166 | background-color: #f3c195; 167 | border-color: #e67e22; 168 | } 169 | 170 | .admonition.danger, .admonition.critical { 171 | color: #bf2718; 172 | background-color: #f5b4ae; 173 | border-color: #e74c3c; 174 | } 175 | -------------------------------------------------------------------------------- /pelican/website/theme/static/media/book-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meonkeys/shb/cc1648e00408ef07a6f9c13932bb6067f7b7c34c/pelican/website/theme/static/media/book-cover.png -------------------------------------------------------------------------------- /pelican/website/theme/templates/archives.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - Archives{% endblock %} 4 | 5 | {% block content %} 6 |

Archives for {{ SITENAME }}

7 | 8 |
9 | {% for article in dates %} 10 |
{{ article.locale_date }}
11 |
{{ article.title }}
12 | {% endfor %} 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/article.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block html_lang %}{{ article.lang }}{% endblock %} 3 | 4 | {% block title %}{{ SITENAME|striptags }} - {{ article.title|striptags }}{% endblock %} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | 9 | {% import 'translations.html' as translations with context %} 10 | {% if translations.entry_hreflang(article) %} 11 | {{ translations.entry_hreflang(article) }} 12 | {% endif %} 13 | 14 | {% if article.description %} 15 | 16 | {% endif %} 17 | 18 | {% for tag in article.tags %} 19 | 20 | {% endfor %} 21 | 22 | {% endblock %} 23 | 24 | {% block content %} 25 |
26 |
27 |

28 | {{ article.title }}

30 | {% import 'translations.html' as translations with context %} 31 | {{ translations.translations_for(article) }} 32 |
33 | {{ article.content }} 34 |
35 |

Published:

38 | {% if article.modified %} 39 |

Last updated:

42 | {% endif %} 43 | {% if article.authors %} 44 | 49 | {% endif %} 50 | {% if article.category %} 51 | 54 | {% endif %} 55 | {% if article.tags %} 56 | 62 | {% endif %} 63 |
64 |
65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/author.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - Articles by {{ author }}{% endblock %} 4 | 5 | {% block content_title %} 6 |

Articles by {{ author }}

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/authors.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - Authors{% endblock %} 4 | 5 | {% block content %} 6 |

Authors on {{ SITENAME }}

7 |
    8 | {% for author, articles in authors|sort %} 9 |
  • {{ author }} ({{ articles|count }})
  • 10 | {% endfor %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% block title %}{{ SITENAME|striptags }}{% endblock title %} 6 | 7 | 8 | 9 | {% if SITESUBTITLE %} 10 | 11 | {% endif %} 12 | 13 | 14 | 15 | {% if FEED_ALL_ATOM %} 16 | 17 | {% endif %} 18 | 19 | {% if FEED_ALL_RSS %} 20 | 21 | {% endif %} 22 | 23 | {% if FEED_ATOM %} 24 | 25 | {% endif %} 26 | 27 | {% if FEED_RSS %} 28 | 29 | {% endif %} 30 | 31 | {% if CATEGORY_FEED_ATOM and category %} 32 | 33 | {% endif %} 34 | 35 | {% if CATEGORY_FEED_RSS and category %} 36 | 37 | {% endif %} 38 | 39 | {% if TAG_FEED_ATOM and tag %} 40 | 41 | {% endif %} 42 | 43 | {% if TAG_FEED_RSS and tag %} 44 | 45 | {% endif %} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% endblock head %} 59 | 60 | 61 | 62 |
63 | 78 |
79 | 80 |
81 |
82 | {% block content %} 83 | {% endblock %} 84 |

85 | Copyright ©2024-2025, Adam Monsen. Some rights reserved. Content on this website is licensed under a Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) License. 86 |

87 |
88 | 102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/categories.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - Categories{% endblock %} 4 | 5 | {% block content %} 6 |

Categories on {{ SITENAME }}

7 |
    8 | {% for category, articles in categories|sort %} 9 |
  • {{ category }} ({{ articles|count }})
  • 10 | {% endfor %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/category.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - {{ category }} category{% endblock %} 4 | 5 | {% block content_title %} 6 |

Articles in the {{ category }} category

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% block content_title %} 4 |

🗞️ News

5 | {% endblock %} 6 | 7 | 8 | {% for article in articles_page.object_list %} 9 |
10 |

{{ article.title }}

11 |
{{ article.summary }}
12 |
13 |

14 |
15 |
16 | {% if article != articles_page.object_list|last %} 17 |
— — ☼ — —
18 | {% endif %} 19 | {% endfor %} 20 | 21 | {% if articles_page.has_other_pages() %} 22 | {% include 'pagination.html' %} 23 | {% endif %} 24 | 25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block html_lang %}{{ page.lang }}{% endblock %} 3 | 4 | {% block title %}{{ SITENAME|striptags }} - {{ page.title|striptags }}{%endblock%} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | 9 | {% import 'translations.html' as translations with context %} 10 | {% if translations.entry_hreflang(page) %} 11 | {{ translations.entry_hreflang(page) }} 12 | {% endif %} 13 | {% endblock %} 14 | 15 | {% block content %} 16 |
17 |
18 |

{{ page.title }}

19 |
20 | {% import 'translations.html' as translations with context %} 21 | {{ translations.translations_for(page) }} 22 | 23 | {{ page.content }} 24 | 25 | {% if page.modified %} 26 |
27 |

Last updated: {{ page.locale_modified }}

28 |
29 | {% endif %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% if DEFAULT_PAGINATION %} 2 | {% set first_page = articles_paginator.page(1) %} 3 | {% set last_page = articles_paginator.page(articles_paginator.num_pages) %} 4 | 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/period_archives.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - {{ period | reverse | join(' ') }} archives{% endblock %} 4 | 5 | {% block content %} 6 |

Archives for {{ period | reverse | join(' ') }}

7 | 8 |
9 | {% for article in dates %} 10 |
{{ article.locale_date }}
11 |
{{ article.title }}
12 | {% endfor %} 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - {{ tag }} tag{% endblock %} 4 | 5 | {% block content_title %} 6 |

Articles tagged with {{ tag }}

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/tags.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ SITENAME|striptags }} - Tags{% endblock %} 4 | 5 | {% block content %} 6 |

Tags for {{ SITENAME }}

7 |
    8 | {% for tag, articles in tags|sort %} 9 |
  • {{ tag }} ({{ articles|count }})
  • 10 | {% endfor %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /pelican/website/theme/templates/translations.html: -------------------------------------------------------------------------------- 1 | {% macro translations_for(article) %} 2 | {% if article.translations %} 3 | Translations: 4 | {% for translation in article.translations %} 5 | {{ translation.lang }} 6 | {% endfor %} 7 | {% endif %} 8 | {% endmacro %} 9 | 10 | {% macro entry_hreflang(entry) %} 11 | {% if entry.translations %} 12 | {% for translation in entry.translations %} 13 | 14 | {% endfor %} 15 | {% endif %} 16 | {% endmacro %} 17 | -------------------------------------------------------------------------------- /style-guide.adoc: -------------------------------------------------------------------------------- 1 | * pay attention to and follow the existing style 2 | ** standardize whenever possible and formalize conventions here 3 | * images 4 | ** center most and constrain to 80% wide 5 | * exclude optional slashes at ends of hyperlinks 6 | * capitalize product/project names in prose as they appear in upstream's branding/docs 7 | * capitalize only the first letter of the first word of sections/headers 8 | ** except: follow styling of proper nouns, acronyms, etc. 9 | * lists 10 | ** ordered lists 11 | *** use captals and periods, even if using incomplete sentences 12 | * define jargon and acronym twice: 13 | ** at first appearance, immediately following the term, in parentheses or locale-appropriate delimiters 14 | ** in the glossary 15 | * footnotes 16 | ** don't use footnotes 17 | * links 18 | ** include links next to or very near context, but try to avoid breaking the flow of text 19 | ** always include typed-out URL, never link text directly 20 | *** this is to ensure consistent appearance across print and electronic versions 21 | ** exclude URL scheme from http(s) links 22 | *** this is handled automatically by asciidoc option `hide-uri-scheme` 23 | *** `https` is a safe guess/default (and hopefully people insist on `https` client-side!) 24 | ** if a link works without `www.` at the beginning of the domain name, omit it 25 | *** this is bit of a risk: we're prioritizing shorter links in favor of more reliable links (some websites redirect, adding back `www.`) 26 | ** if a link works without a SEO slug, omit it 27 | *** example w/slug: `+https://reddit.com/r/BorgBackup/comments/v3bwfg/why_should_i_switch_from_restic_to_borg/+` 28 | *** example w/o slug: `+https://reddit.com/r/BorgBackup/comments/v3bwfg/+` 29 | *** shorter is better, canonical/permalink is best (if you are forced to choose) 30 | ** use more readable version for cross references whenever possible 31 | *** no: `+<<_more_about_foss>>+` 32 | *** yes: `+<>+` 33 | * use "`command line`" to refer to a Linux text-based interactive user interface 34 | * use https://en.wikipedia.org/wiki/Serial_comma[Oxford commas] 35 | * use https://asciidoctor.org/docs/asciidoc-recommended-practices/#one-sentence-per-line[one sentence] https://sive.rs/1s[per line] 36 | * shell scripts 37 | ** prefer long form for command line flags, e.g. `--attribute` instead of `-a` 38 | ** prioritize portability and maintainability 39 | * release versioning 40 | ** use semver-like major, minor, patch version numbers 41 | * source control 42 | ** commit early and often 43 | ** group logically related changes into single commits 44 | *** consider future maintainers may wish to `git revert`: try to make that easy for them 45 | ** group a series of related changes in a branch 46 | ** squashing is OK 47 | ** before submitting patches: 48 | *** ensure build passes 49 | ** commit log messages 50 | *** the first line of a commit log message is very important: say precisely *what* change you made, save the *why* for the rest 51 | *** use infinitive verb forms, e.g. "`add -q quiet option`" 52 | *** don't wrap body text 53 | *** see also: 54 | **** https://mifosforge.jira.com/wiki/spaces/MIFOS/pages/4456742/Commit+Log+Guide 55 | **** https://lore.kernel.org/git/7vr4waoics.fsf@alter.siamese.dyndns.org/ 56 | **** https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 57 | ** ChangeLog 58 | *** one entry per release 59 | *** summarize major changes since last release 60 | *** use infinitve forms for "`xyz happened`" statements 61 | * use `shb` namespace for document attributes 62 | ** short for "`self-hosting book`" 63 | ** example: `shb-printPDF`, used when generating a PDF for printing 64 | * include a trailing slash when referencing folders, e.g. `ansible/` 65 | * indexing 66 | ** prefer https://docs.asciidoctor.org/asciidoc/latest/sections/user-index/#index-terms[flow index terms over concealed index terms] 67 | ** use your gut: index a term when it feels helpful to draw the reader's attention somewhere to read more about the term 68 | ** don't worry about indexing every occurence of a particular term -- focus on where it is specifically discussed/defined, rather than just used 69 | ** note: indexes are only generated for PDF outputs 70 | * data is plural, use context for singular (e.g. "`piece of data`") 71 | * colons: captalize word after? sometimes? *TBD* 72 | * em dash: omit space before and after 73 | --------------------------------------------------------------------------------